context.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Deterministic TaskContextProvider resolution into existing ContextRefs."""
  2. from __future__ import annotations
  3. from hashlib import sha256
  4. from typing import Any
  5. from cyber_agent.application.models import canonical_json
  6. from cyber_agent.application.ports import (
  7. ContextMaterial,
  8. ContextRequest,
  9. ContextResourceDescriptor,
  10. )
  11. from cyber_agent.core.context_policy import (
  12. MAX_CONTEXT_REFS,
  13. MAX_CONTEXT_SNAPSHOT_CHARS,
  14. MAX_CONTEXT_SNAPSHOTS_CHARS,
  15. ContextPolicyError,
  16. ContextSnapshot,
  17. create_context_snapshot,
  18. ensure_context_access,
  19. replace_context_access,
  20. )
  21. from cyber_agent.core.task_protocol import RootTaskAnchor, TaskBrief
  22. def _descriptor_identity(
  23. descriptor: ContextResourceDescriptor,
  24. ) -> tuple[str, str]:
  25. return descriptor.source_id, descriptor.source_version
  26. def _authorize(
  27. request: ContextRequest,
  28. descriptor: ContextResourceDescriptor,
  29. ) -> None:
  30. expected = (
  31. request.application_ref,
  32. request.root_trace_id,
  33. request.trace_id,
  34. request.uid,
  35. request.role_id,
  36. )
  37. actual = (
  38. descriptor.application_ref,
  39. descriptor.root_trace_id,
  40. descriptor.trace_id,
  41. descriptor.uid,
  42. descriptor.role_id,
  43. )
  44. if actual != expected:
  45. raise ContextPolicyError(
  46. "Application context descriptor belongs to another run, task, or role"
  47. )
  48. async def load_application_context(
  49. binding: Any,
  50. context: dict[str, Any],
  51. request: ContextRequest,
  52. *,
  53. root_task_anchor: RootTaskAnchor,
  54. task_brief: TaskBrief | dict[str, Any] | None,
  55. granted_at_sequence: int,
  56. ) -> list[ContextSnapshot]:
  57. """Resolve, verify, and atomically add a stable bounded resource set."""
  58. provider = binding.services.context_provider
  59. if provider is None:
  60. return []
  61. raw_descriptors = await provider.list_context(request)
  62. descriptors = [ContextResourceDescriptor.model_validate(item) for item in raw_descriptors]
  63. unique: dict[tuple[str, str], ContextResourceDescriptor] = {}
  64. for descriptor in descriptors:
  65. _authorize(request, descriptor)
  66. key = _descriptor_identity(descriptor)
  67. prior = unique.get(key)
  68. if prior is not None and prior.content_hash != descriptor.content_hash:
  69. raise ContextPolicyError(
  70. f"Application context identity has conflicting hashes: {key}"
  71. )
  72. unique[key] = descriptor
  73. ordered = sorted(
  74. unique.values(),
  75. key=lambda item: (-item.priority, item.source_id, item.source_version),
  76. )
  77. access = ensure_context_access(context)
  78. existing = [
  79. ContextSnapshot.model_validate(item)
  80. for item in access["snapshots"].values()
  81. ]
  82. available_slots = max(0, MAX_CONTEXT_REFS - len(existing))
  83. current_chars = sum(len(canonical_json(item.content)) for item in existing)
  84. selected: list[ContextSnapshot] = []
  85. for descriptor in ordered:
  86. if len(selected) >= available_slots:
  87. break
  88. if descriptor.estimated_chars > MAX_CONTEXT_SNAPSHOT_CHARS:
  89. continue
  90. material = ContextMaterial.model_validate(
  91. await provider.resolve_context(request, descriptor)
  92. )
  93. if material.descriptor != descriptor:
  94. raise ContextPolicyError(
  95. f"Application context resolver changed descriptor: {descriptor.source_id}"
  96. )
  97. encoded_content = canonical_json(material.content)
  98. actual_hash = sha256(encoded_content.encode("utf-8")).hexdigest()
  99. if actual_hash != descriptor.content_hash:
  100. raise ContextPolicyError(
  101. f"Application context hash mismatch: {descriptor.source_id}"
  102. )
  103. wrapped = {
  104. "application_resource": {
  105. "application_ref": request.application_ref.model_dump(mode="json"),
  106. "source_id": descriptor.source_id,
  107. "source_version": descriptor.source_version,
  108. "kind": descriptor.kind,
  109. "inheritance": descriptor.inheritance,
  110. "authorization": descriptor.authorization,
  111. },
  112. "content": material.content,
  113. }
  114. chars = len(canonical_json(wrapped))
  115. if chars > MAX_CONTEXT_SNAPSHOT_CHARS:
  116. continue
  117. if current_chars + chars > MAX_CONTEXT_SNAPSHOTS_CHARS:
  118. continue
  119. source_trace_id = (
  120. f"application:{request.application_ref.application_id}:"
  121. f"{descriptor.source_id}@{descriptor.source_version}"
  122. )
  123. selected.append(create_context_snapshot(
  124. kind="application_resource",
  125. summary=descriptor.summary,
  126. source_trace_id=source_trace_id,
  127. root_trace_id=request.root_trace_id,
  128. uid=request.uid,
  129. content=wrapped,
  130. granted_at_sequence=granted_at_sequence,
  131. ))
  132. current_chars += chars
  133. replace_context_access(
  134. context,
  135. [*existing, *selected],
  136. root_task_anchor=root_task_anchor,
  137. task_brief=task_brief,
  138. )
  139. return selected