runtime_server.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """Server-derived invocation contract for the hardened WP09 runtime path."""
  2. from __future__ import annotations
  3. import hashlib
  4. from dataclasses import dataclass
  5. from decimal import Decimal, InvalidOperation
  6. from typing import Any
  7. class RuntimeServerError(ValueError):
  8. pass
  9. @dataclass(frozen=True)
  10. class RuntimeServerContext:
  11. agent_uid: str
  12. actor_uid: str
  13. roles: frozenset[str]
  14. _PAYLOAD_FIELDS = frozenset(
  15. {
  16. "grant_uid", "idempotency_key", "input_text", "evidence_refs",
  17. "estimated_tokens", "estimated_cost_micros", "requested_time_ms",
  18. }
  19. )
  20. _OPTIONAL_APPROVAL_FIELD = "approval_task_uid"
  21. _SCOPE_FIELDS = frozenset(
  22. {
  23. "tenant_id", "principal_id", "business_domain_uid", "environment", "provider",
  24. "model", "prompt_version", "generation", "interface_type", "tool_name", "action",
  25. "risk_level", "agent_uid",
  26. }
  27. )
  28. def _positive_integer(value: Any) -> int:
  29. if isinstance(value, bool) or not isinstance(value, int) or not 0 < value <= 1_000_000:
  30. raise RuntimeServerError("budget_value_invalid")
  31. return value
  32. def _cost_micros(value: Any) -> int:
  33. if isinstance(value, bool) or not isinstance(value, str) or len(value) > 32:
  34. raise RuntimeServerError("budget_value_invalid")
  35. try:
  36. decimal = Decimal(value)
  37. except (InvalidOperation, ValueError) as error:
  38. raise RuntimeServerError("budget_value_invalid") from error
  39. if not decimal.is_finite() or decimal <= 0 or decimal != decimal.to_integral_value():
  40. raise RuntimeServerError("budget_value_invalid")
  41. result = int(decimal)
  42. if result > 1_000_000_000:
  43. raise RuntimeServerError("budget_value_invalid")
  44. return result
  45. class ServerGovernedInvocationService:
  46. """Reject client-supplied scope, deriving every authority field server-side."""
  47. def __init__(self, repository):
  48. self.repository = repository
  49. def authorize(self, context: RuntimeServerContext, payload: Any) -> dict[str, Any]:
  50. if not isinstance(payload, dict):
  51. raise RuntimeServerError("payload_invalid")
  52. if set(payload) & _SCOPE_FIELDS:
  53. raise RuntimeServerError("payload_scope_forbidden")
  54. if set(payload) not in {_PAYLOAD_FIELDS, _PAYLOAD_FIELDS | {_OPTIONAL_APPROVAL_FIELD}}:
  55. raise RuntimeServerError("payload_schema_invalid")
  56. if not context.agent_uid or not context.actor_uid or not context.roles:
  57. raise RuntimeServerError("server_identity_invalid")
  58. grant_uid = payload["grant_uid"]
  59. if not isinstance(grant_uid, str) or not grant_uid:
  60. raise RuntimeServerError("payload_invalid")
  61. idempotency_key = payload["idempotency_key"]
  62. if not isinstance(idempotency_key, str) or not idempotency_key or len(idempotency_key) > 160:
  63. raise RuntimeServerError("payload_invalid")
  64. input_text = payload["input_text"]
  65. if not isinstance(input_text, str) or not input_text.strip() or len(input_text) > 8000:
  66. raise RuntimeServerError("payload_invalid")
  67. evidence_refs = payload["evidence_refs"]
  68. if not isinstance(evidence_refs, list) or len(evidence_refs) > 20:
  69. raise RuntimeServerError("payload_invalid")
  70. derived = self.repository.grant_context(
  71. agent_uid=context.agent_uid, grant_uid=grant_uid, actor_uid=context.actor_uid
  72. )
  73. requires_approval = derived.get("risk_level") != "low" or derived.get("action") == "execute"
  74. approval_task_uid = payload.get(_OPTIONAL_APPROVAL_FIELD)
  75. if requires_approval and (not isinstance(approval_task_uid, str) or len(approval_task_uid) != 36):
  76. raise RuntimeServerError("human_approval_required")
  77. if not requires_approval and approval_task_uid is not None:
  78. raise RuntimeServerError("approval_not_applicable")
  79. request = {
  80. **derived,
  81. "agent_uid": context.agent_uid,
  82. "grant_uid": grant_uid,
  83. "idempotency_key": idempotency_key,
  84. "request_digest": hashlib.sha256(
  85. (input_text.strip() + "|" + repr(evidence_refs)).encode("utf-8")
  86. ).hexdigest(),
  87. "input_hash": hashlib.sha256(input_text.strip().encode("utf-8")).hexdigest(),
  88. "evidence_digests": [],
  89. "estimated_tokens": _positive_integer(payload["estimated_tokens"]),
  90. "estimated_cost_micros": _cost_micros(payload["estimated_cost_micros"]),
  91. "requested_time_ms": _positive_integer(payload["requested_time_ms"]),
  92. }
  93. if requires_approval:
  94. request["approval_task_uid"] = approval_task_uid
  95. return self.repository.authorize_claim(request)