persistence.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """Database adapters for the fixed WP13 control and runtime gateways."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. from typing import Any
  6. from sqlalchemy import create_engine, text
  7. from sqlalchemy.pool import NullPool
  8. from app.config.database_urls import validate_postgresql_url
  9. from app.core.mcp.governed_invocation import normalize_mcp_invocation
  10. from .governance import fixture_signature_digest, normalize_plugin_manifest
  11. from .runtime import BuiltinFixtureDispatcher
  12. class PluginPersistenceError(ValueError):
  13. """The persistent gateway rejected a closed plugin request."""
  14. def _digest(value: Any, label: str) -> str:
  15. if not isinstance(value, str) or len(value) != 64 or set(value) - set("0123456789abcdef"):
  16. raise PluginPersistenceError(f"{label}_invalid")
  17. return value
  18. class DatabasePluginPlatformService:
  19. """Uses only fixed SECURITY DEFINER functions; never direct fact SQL."""
  20. def __init__(self) -> None:
  21. self._runtime_url = os.environ.get("DATABASE_URL", "")
  22. self._control_url = os.environ.get("PLUGIN_PLATFORM_CONTROL_DATABASE_URL", os.environ.get("BI_AI_CATALOG_CONTROL_DATABASE_URL", ""))
  23. runtime = validate_postgresql_url(self._runtime_url, "DATABASE_URL")
  24. control = validate_postgresql_url(self._control_url, "PLUGIN_PLATFORM_CONTROL_DATABASE_URL")
  25. if runtime.username == control.username:
  26. raise RuntimeError("plugin control and runtime identities must differ")
  27. @staticmethod
  28. def _scope() -> dict[str, str]:
  29. tenant = os.environ.get("TRUSTED_PLUGIN_TENANT", "")
  30. domain = os.environ.get("TRUSTED_PLUGIN_DOMAIN", "")
  31. if not tenant or not domain or not tenant.isascii() or not domain.isascii():
  32. raise PluginPersistenceError("trusted_plugin_scope_missing")
  33. return {"tenant_ref": tenant, "domain_ref": domain}
  34. def _control(self, payload: dict[str, Any]) -> dict[str, Any]:
  35. with create_engine(self._control_url, poolclass=NullPool, pool_pre_ping=True).begin() as connection:
  36. value = connection.execute(text("SELECT public.plugin_platform_control_v5(CAST(:payload AS jsonb))"), {"payload": json.dumps(payload, sort_keys=True, separators=(",", ":"))}).scalar_one()
  37. return dict(value)
  38. def _claim(self, payload: dict[str, Any]) -> dict[str, Any]:
  39. with create_engine(self._control_url, poolclass=NullPool, pool_pre_ping=True).begin() as connection:
  40. value = connection.execute(text("SELECT public.plugin_platform_issue_claim_v3(CAST(:payload AS jsonb))"), {"payload": json.dumps(payload, sort_keys=True, separators=(",", ":"))}).scalar_one()
  41. return dict(value)
  42. def _runtime(self, payload: dict[str, Any]) -> dict[str, Any]:
  43. with create_engine(self._runtime_url, poolclass=NullPool, pool_pre_ping=True).begin() as connection:
  44. value = connection.execute(text("SELECT public.plugin_platform_runtime_v3(CAST(:payload AS jsonb))"), {"payload": json.dumps(payload, sort_keys=True, separators=(",", ":"))}).scalar_one()
  45. return dict(value)
  46. def register(self, *, manifest: Any, registry_record: Any, actor_ref: str) -> dict[str, Any]:
  47. normalized = normalize_plugin_manifest(manifest)
  48. if not isinstance(registry_record, dict) or set(registry_record) != {"artifact_digest", "signature", "sbom_digest", "license_digest", "vulnerability_digest", "provenance_digest"}:
  49. raise PluginPersistenceError("registry_record_closed")
  50. signature = registry_record["signature"]
  51. if not isinstance(signature, dict) or set(signature) != {"trust_store_key_id", "signature_digest"} or signature["trust_store_key_id"] != "local-fixture-key-v1":
  52. raise PluginPersistenceError("trust_store_denied")
  53. payload = {
  54. "action": "register", "plugin_uid": normalized["plugin_uid"], "version": normalized["version"], "actor_ref": actor_ref,
  55. "plugin_type": normalized["type"], "manifest_digest": normalized["manifest_digest"],
  56. "artifact_digest": _digest(registry_record["artifact_digest"], "artifact_digest"),
  57. "signature_digest": _digest(signature["signature_digest"], "signature_digest"),
  58. "sbom_digest": _digest(registry_record["sbom_digest"], "sbom_digest"), "license_digest": _digest(registry_record["license_digest"], "license_digest"),
  59. "vulnerability_digest": _digest(registry_record["vulnerability_digest"], "vulnerability_digest"), "provenance_digest": _digest(registry_record["provenance_digest"], "provenance_digest"),
  60. "capabilities": normalized["capabilities"], "resource": normalized["resource"],
  61. }
  62. if payload["artifact_digest"] != normalized["distribution"]["artifact_digest"]:
  63. raise PluginPersistenceError("artifact_digest_mismatch")
  64. if payload["signature_digest"] != fixture_signature_digest(payload["artifact_digest"]):
  65. raise PluginPersistenceError("signature_binding_denied")
  66. return self._control(payload)
  67. def review(self, *, plugin_uid: str, version: str, actor_ref: str) -> dict[str, Any]:
  68. return self._control({"action": "review", "plugin_uid": plugin_uid, "version": version, "actor_ref": actor_ref})
  69. def issue_approval(self, *, plugin_uid: str, version: str, actor_ref: str, approval_action: str) -> dict[str, Any]:
  70. return self._control({"action": "issue_approval", "plugin_uid": plugin_uid, "version": version, "actor_ref": actor_ref, "approval_action": approval_action, **self._scope(), "expires_in_seconds": 300})
  71. def transition(self, *, plugin_uid: str, version: str, actor_ref: str, target_state: str, approval_uid: str, expected_fence: int, incident_uid: str = "") -> dict[str, Any]:
  72. if not isinstance(expected_fence, int) or expected_fence < 0:
  73. raise PluginPersistenceError("expected_fence_invalid")
  74. return self._control({"action": "transition", "plugin_uid": plugin_uid, "version": version, "actor_ref": actor_ref, **self._scope(), "approval_uid": approval_uid, "target_state": target_state, "expected_fence": expected_fence, "incident_uid": incident_uid})
  75. def invoke(self, *, plugin_uid: str, version: str, actor_ref: str, operation: str, input_digest: str, idempotency_key: str) -> dict[str, Any]:
  76. _digest(input_digest, "input_digest")
  77. if not isinstance(idempotency_key, str) or not idempotency_key.isascii() or not 1 <= len(idempotency_key) <= 120:
  78. raise PluginPersistenceError("idempotency_key_invalid")
  79. scope = self._scope()
  80. description = self._control({"action": "describe", "plugin_uid": plugin_uid, "version": version, "actor_ref": actor_ref})
  81. if description["plugin_type"] == "agent_mcp":
  82. normalize_mcp_invocation({"interface_type": "mcp", "tool_name": "governed_plugin_fixture", "action": operation, "arguments_digest": input_digest, "evidence_refs": []})
  83. claim = self._claim({"plugin_uid": plugin_uid, "version": version, "principal_ref": actor_ref, "operation_name": operation, "input_digest": input_digest, "idempotency_key": idempotency_key, **scope})
  84. queued = self._runtime({"action": "enqueue", "claim_uid": claim["claim_uid"], "idempotency_key": idempotency_key, "input_digest": input_digest, "operation_name": operation})
  85. lease = self._runtime({"action": "claim", "run_uid": queued["run_uid"], "worker": "fixed-builtin"})
  86. fixture_manifest = {"distribution": {"kind": "builtin_fixture", "fixture_id": "ENGINEERING_EVIDENCE_ONLY"}, "permissions": {"child_process": False, "file": False, "network": False, "secret": False}, "type": description["plugin_type"], "capabilities": description["capabilities"], "resource": description["resource"], "manifest_digest": description["manifest_digest"]}
  87. result = BuiltinFixtureDispatcher().execute(fixture_manifest, operation=operation, input_digest=input_digest)
  88. settled = self._runtime({"action": "settle", "run_uid": queued["run_uid"], "worker": "fixed-builtin", "fence": lease["fence"], "success": True, "output_digest": result["output_digest"], "failure_digest": "0" * 64})
  89. return {"run_uid": queued["run_uid"], **result, **settled}
  90. __all__ = ["DatabasePluginPlatformService", "PluginPersistenceError"]