plugin_platform_routes.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """Closed HTTP validation boundary for the inactive WP13 local fixture."""
  2. from __future__ import annotations
  3. from flask import g, jsonify, request
  4. from sqlalchemy.exc import SQLAlchemyError
  5. from app.api.system import bp
  6. from app.core.plugins.governance import PluginManifestError, normalize_plugin_manifest
  7. from app.core.plugins.persistence import (
  8. DatabasePluginPlatformService,
  9. PluginPersistenceError,
  10. )
  11. from app.core.plugins.repository import PluginLifecycleError, PluginPlatform
  12. from app.core.system.permissions import AGENTS_MANAGE, require_permissions
  13. from app.models.result import failed, success
  14. _MAX_BODY_BYTES = 8192
  15. def _body(required: set[str]) -> dict:
  16. if request.content_length is not None and request.content_length > _MAX_BODY_BYTES:
  17. raise ValueError("plugin_body_closed")
  18. value = request.get_json(silent=True)
  19. if not isinstance(value, dict) or set(value) != required:
  20. raise ValueError("plugin_body_closed")
  21. return value
  22. def _response(value: object, status: int = 200):
  23. response = jsonify(success(value, code=status))
  24. response.headers["Cache-Control"] = "no-store"
  25. return response, status
  26. def _failure(status: int = 400):
  27. response = jsonify(failed("plugin request rejected", code=status))
  28. response.headers["Cache-Control"] = "no-store"
  29. return response, status
  30. @bp.post("/plugins/local-fixture/validate")
  31. @require_permissions(AGENTS_MANAGE)
  32. def validate_local_plugin_fixture():
  33. """Validate closed metadata only; it never stores or activates a plugin."""
  34. try:
  35. body = _body({"manifest", "registry_record"})
  36. platform = PluginPlatform(now=lambda: 0)
  37. result = platform.register(body["manifest"], registry_record=body["registry_record"])
  38. return _response({"plugin_uid": result["plugin_uid"], "version": result["version"], "manifest_digest": result["manifest_digest"], "mode": "ENGINEERING_EVIDENCE_ONLY", "activation": "blocked"})
  39. except (PluginManifestError, PluginLifecycleError, TypeError, ValueError):
  40. return _failure()
  41. @bp.post("/plugins/invocations")
  42. @require_permissions(AGENTS_MANAGE)
  43. def invoke_plugin_fixture():
  44. """Invoke only an approved fixed local fixture through the DB gateway."""
  45. try:
  46. body = _body({"plugin_uid", "version", "operation", "input_digest", "idempotency_key"})
  47. return _response(DatabasePluginPlatformService().invoke(actor_ref=str(g.current_user["id"]), **body), 201)
  48. except (PluginPersistenceError, RuntimeError, TypeError, ValueError, SQLAlchemyError):
  49. return _failure()
  50. @bp.post("/plugins/registry")
  51. @require_permissions(AGENTS_MANAGE)
  52. def register_plugin_fixture():
  53. try:
  54. body = _body({"manifest", "registry_record"})
  55. # Validate executable-surface metadata before allocating a DB gateway;
  56. # this keeps malformed URL/code/credential bodies out of all backends.
  57. normalize_plugin_manifest(body["manifest"])
  58. return _response(DatabasePluginPlatformService().register(actor_ref=str(g.current_user["id"]), **body), 201)
  59. except (PluginPersistenceError, RuntimeError, TypeError, ValueError, SQLAlchemyError):
  60. return _failure()
  61. @bp.post("/plugins/<plugin_uid>/<version>/review")
  62. @require_permissions(AGENTS_MANAGE)
  63. def review_plugin_fixture(plugin_uid: str, version: str):
  64. try:
  65. _body(set())
  66. return _response(DatabasePluginPlatformService().review(plugin_uid=plugin_uid, version=version, actor_ref=str(g.current_user["id"])))
  67. except (PluginPersistenceError, RuntimeError, TypeError, ValueError, SQLAlchemyError):
  68. return _failure()
  69. @bp.post("/plugins/<plugin_uid>/<version>/approvals")
  70. @require_permissions(AGENTS_MANAGE)
  71. def approve_plugin_fixture(plugin_uid: str, version: str):
  72. try:
  73. body = _body({"approval_action"})
  74. return _response(DatabasePluginPlatformService().issue_approval(plugin_uid=plugin_uid, version=version, actor_ref=str(g.current_user["id"]), **body), 201)
  75. except (PluginPersistenceError, RuntimeError, TypeError, ValueError, SQLAlchemyError):
  76. return _failure()
  77. def _transition(plugin_uid: str, version: str, target_state: str):
  78. try:
  79. body = _body({"approval_uid", "expected_fence", "incident_uid"})
  80. return _response(DatabasePluginPlatformService().transition(plugin_uid=plugin_uid, version=version, target_state=target_state, actor_ref=str(g.current_user["id"]), **body))
  81. except (PluginPersistenceError, RuntimeError, TypeError, ValueError, SQLAlchemyError):
  82. return _failure()
  83. @bp.post("/plugins/<plugin_uid>/<version>/canary")
  84. @require_permissions(AGENTS_MANAGE)
  85. def canary_plugin_fixture(plugin_uid: str, version: str):
  86. return _transition(plugin_uid, version, "canary")
  87. @bp.post("/plugins/<plugin_uid>/<version>/activate")
  88. @require_permissions(AGENTS_MANAGE)
  89. def activate_plugin_fixture(plugin_uid: str, version: str):
  90. return _transition(plugin_uid, version, "active")
  91. @bp.post("/plugins/<plugin_uid>/<version>/pause")
  92. @require_permissions(AGENTS_MANAGE)
  93. def pause_plugin_fixture(plugin_uid: str, version: str):
  94. return _transition(plugin_uid, version, "paused")
  95. @bp.post("/plugins/<plugin_uid>/<version>/rollback")
  96. @require_permissions(AGENTS_MANAGE)
  97. def rollback_plugin_fixture(plugin_uid: str, version: str):
  98. return _transition(plugin_uid, version, "rolled_back")
  99. @bp.post("/plugins/<plugin_uid>/<version>/revoke")
  100. @require_permissions(AGENTS_MANAGE)
  101. def revoke_plugin_fixture(plugin_uid: str, version: str):
  102. return _transition(plugin_uid, version, "revoked")
  103. @bp.post("/plugins/<plugin_uid>/<version>/recover")
  104. @require_permissions(AGENTS_MANAGE)
  105. def recover_plugin_fixture(plugin_uid: str, version: str):
  106. return _transition(plugin_uid, version, "recovery")