activegraph 1.3.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. {activegraph-1.3.0/activegraph.egg-info → activegraph-1.5.0}/PKG-INFO +1 -1
  2. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/__init__.py +1 -1
  3. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/__init__.py +37 -1
  4. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/loader.py +21 -0
  5. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/manifest.py +104 -14
  6. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/runtime.py +211 -0
  7. activegraph-1.5.0/activegraph/sandbox/__init__.py +257 -0
  8. activegraph-1.5.0/activegraph/sandbox/_child.py +234 -0
  9. activegraph-1.5.0/activegraph/store/retention.py +316 -0
  10. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/sqlite.py +126 -0
  11. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/printer.py +12 -0
  12. {activegraph-1.3.0 → activegraph-1.5.0/activegraph.egg-info}/PKG-INFO +1 -1
  13. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/SOURCES.txt +6 -0
  14. {activegraph-1.3.0 → activegraph-1.5.0}/pyproject.toml +1 -1
  15. activegraph-1.5.0/tests/test_compaction.py +249 -0
  16. activegraph-1.5.0/tests/test_disable_pack.py +155 -0
  17. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pack_manifest.py +162 -1
  18. activegraph-1.5.0/tests/test_sandbox_trial.py +260 -0
  19. {activegraph-1.3.0 → activegraph-1.5.0}/LICENSE +0 -0
  20. {activegraph-1.3.0 → activegraph-1.5.0}/NOTICE +0 -0
  21. {activegraph-1.3.0 → activegraph-1.5.0}/README.md +0 -0
  22. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/__main__.py +0 -0
  23. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/_signature.py +0 -0
  24. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/__init__.py +0 -0
  25. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/base.py +0 -0
  26. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/decorators.py +0 -0
  27. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/__init__.py +0 -0
  28. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/main.py +0 -0
  29. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/quickstart.py +0 -0
  30. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/renderers.py +0 -0
  31. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/__init__.py +0 -0
  32. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/clock.py +0 -0
  33. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/event.py +0 -0
  34. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/graph.py +0 -0
  35. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/graph_store.py +0 -0
  36. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/ids.py +0 -0
  37. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/patch.py +0 -0
  38. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/view.py +0 -0
  39. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/errors.py +0 -0
  40. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/frame.py +0 -0
  41. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/__init__.py +0 -0
  42. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/anthropic.py +0 -0
  43. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/cache.py +0 -0
  44. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/embedding.py +0 -0
  45. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/errors.py +0 -0
  46. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/native.py +0 -0
  47. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/openai.py +0 -0
  48. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/parsing.py +0 -0
  49. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/prompt.py +0 -0
  50. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/provider.py +0 -0
  51. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/recorded.py +0 -0
  52. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/types.py +0 -0
  53. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/wire.py +0 -0
  54. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/__init__.py +0 -0
  55. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/logging.py +0 -0
  56. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/metrics.py +0 -0
  57. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/migration.py +0 -0
  58. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/otel.py +0 -0
  59. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/prometheus.py +0 -0
  60. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/status.py +0 -0
  61. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/__init__.py +0 -0
  62. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/behaviors.py +0 -0
  63. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  64. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  65. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/object_types.py +0 -0
  66. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  67. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  68. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  69. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  70. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/settings.py +0 -0
  71. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/tools.py +0 -0
  72. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/scaffold.py +0 -0
  73. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/policy.py +0 -0
  74. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/__init__.py +0 -0
  75. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/_live.py +0 -0
  76. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/behavior_graph.py +0 -0
  77. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/budget.py +0 -0
  78. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/config_errors.py +0 -0
  79. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/diff.py +0 -0
  80. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/errors.py +0 -0
  81. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/exec_errors.py +0 -0
  82. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/patterns.py +0 -0
  83. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/promote.py +0 -0
  84. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/queue.py +0 -0
  85. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/registration_errors.py +0 -0
  86. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/registry.py +0 -0
  87. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/scheduler.py +0 -0
  88. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/view_builder.py +0 -0
  89. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/__init__.py +0 -0
  90. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/base.py +0 -0
  91. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/conformance.py +0 -0
  92. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/errors.py +0 -0
  93. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/falkordb.py +0 -0
  94. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/graph_conformance.py +0 -0
  95. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/memory.py +0 -0
  96. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/postgres.py +0 -0
  97. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/serde.py +0 -0
  98. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/url.py +0 -0
  99. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/__init__.py +0 -0
  100. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/base.py +0 -0
  101. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/cache.py +0 -0
  102. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/context.py +0 -0
  103. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/decorators.py +0 -0
  104. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/errors.py +0 -0
  105. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/graph_query.py +0 -0
  106. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/recorded.py +0 -0
  107. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/web_fetch.py +0 -0
  108. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/__init__.py +0 -0
  109. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/causal.py +0 -0
  110. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/dependency_links.txt +0 -0
  111. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/entry_points.txt +0 -0
  112. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/requires.txt +0 -0
  113. {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/top_level.txt +0 -0
  114. {activegraph-1.3.0 → activegraph-1.5.0}/setup.cfg +0 -0
  115. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_activate_after.py +0 -0
  116. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_causal_cross_tool.py +0 -0
  117. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_cli.py +0 -0
  118. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_cli_docs_flags.py +0 -0
  119. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_clock.py +0 -0
  120. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diff.py +0 -0
  121. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diligence_pack.py +0 -0
  122. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diligence_with_tools.py +0 -0
  123. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_links.py +0 -0
  124. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_python_snippets.py +0 -0
  125. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_site_reachable.py +0 -0
  126. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_embedding_provider.py +0 -0
  127. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_errors_format.py +0 -0
  128. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_event.py +0 -0
  129. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_falkordb_store.py +0 -0
  130. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_fork.py +0 -0
  131. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_graph.py +0 -0
  132. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_graph_store.py +0 -0
  133. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_ids.py +0 -0
  134. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_license.py +0 -0
  135. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_anthropic.py +0 -0
  136. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_behavior.py +0 -0
  137. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_budget.py +0 -0
  138. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_causal.py +0 -0
  139. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_claim_extraction.py +0 -0
  140. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_default_model.py +0 -0
  141. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_determinism.py +0 -0
  142. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_failure.py +0 -0
  143. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_native_structured_output.py +0 -0
  144. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_openai.py +0 -0
  145. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_prompt.py +0 -0
  146. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_provider_fixtures.py +0 -0
  147. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_replay.py +0 -0
  148. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_tool_loop.py +0 -0
  149. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_trace.py +0 -0
  150. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_trace_snapshot.py +0 -0
  151. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_types.py +0 -0
  152. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_wire.py +0 -0
  153. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llms_txt.py +0 -0
  154. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_migration.py +0 -0
  155. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_observability_logging.py +0 -0
  156. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_observability_metrics.py +0 -0
  157. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_operate_example.py +0 -0
  158. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pack_scaffold.py +0 -0
  159. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_packs.py +0 -0
  160. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_patch.py +0 -0
  161. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_matcher.py +0 -0
  162. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_parser.py +0 -0
  163. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_subscriptions.py +0 -0
  164. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_persistence.py +0 -0
  165. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_postgres_store.py +0 -0
  166. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_promote.py +0 -0
  167. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_quickstart.py +0 -0
  168. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_quickstart_snapshot.py +0 -0
  169. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_reason_codes_docs.py +0 -0
  170. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_registration_validation.py +0 -0
  171. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_replay.py +0 -0
  172. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_replay_trace_snapshot.py +0 -0
  173. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_requeue_unfired.py +0 -0
  174. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_resume_example.py +0 -0
  175. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_runtime.py +0 -0
  176. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_runtime_status.py +0 -0
  177. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_serde.py +0 -0
  178. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_store_conformance.py +0 -0
  179. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_store_url.py +0 -0
  180. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tool_replay.py +0 -0
  181. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tool_trace_snapshot.py +0 -0
  182. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tools.py +0 -0
  183. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_trace.py +0 -0
  184. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_trace_accessors.py +0 -0
  185. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tutorial_snippets.py +0 -0
  186. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_1_patches.py +0 -0
  187. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  188. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  189. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_version_sync.py +0 -0
  190. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_view.py +0 -0
  191. {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_wheel_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License-Expression: Apache-2.0
@@ -242,4 +242,4 @@ __all__ = [
242
242
  "tool",
243
243
  ]
244
244
 
245
- __version__ = "1.3.0"
245
+ __version__ = "1.5.0"
@@ -570,10 +570,20 @@ class Pack:
570
570
  policies: tuple[Any, ...] = ()
571
571
  prompts: tuple[Any, ...] = ()
572
572
  settings_schema: type = EmptySettings
573
+ # v1.4: the DECLARATIVE half of gateway-capability registration
574
+ # (manifest spec Q8). Entries are CapabilityDecl instances from
575
+ # activegraph.packs.manifest. The runtime never registers these —
576
+ # registration stays imperative host wiring — but the declaration
577
+ # is loader-introspectable: verify_surface two-way checks it
578
+ # against the manifest, and load_pack records it in the
579
+ # pack.loaded payload so decision surfaces read a pack's declared
580
+ # outbound reach from the graph. The gateway-side check ("did the
581
+ # registering pack declare this?") is downstream's half.
582
+ capabilities: tuple[Any, ...] = ()
573
583
 
574
584
  def __post_init__(self) -> None:
575
585
  # list → tuple conversion (frozen requires object.__setattr__)
576
- for f in ("object_types", "relation_types", "behaviors", "tools", "policies", "prompts"):
586
+ for f in ("object_types", "relation_types", "behaviors", "tools", "policies", "prompts", "capabilities"):
577
587
  v = getattr(self, f)
578
588
  if isinstance(v, list):
579
589
  object.__setattr__(self, f, tuple(v))
@@ -625,6 +635,32 @@ class Pack:
625
635
  f"activegraph.packs.tool (use activegraph.packs.tool, not activegraph.tool)"
626
636
  )
627
637
 
638
+ # v1.4: capabilities are CapabilityDecl entries with a valid
639
+ # risk class; (provider, capability) pairs unique within the
640
+ # pack. Lazy import — manifest.py imports nothing from this
641
+ # module at import time, but keep the seam one-directional.
642
+ if self.capabilities:
643
+ from activegraph.packs.manifest import _RISK_CLASSES, CapabilityDecl
644
+
645
+ for c in self.capabilities:
646
+ if not isinstance(c, CapabilityDecl):
647
+ raise PackValidationError(
648
+ f"Pack {self.name!r}: capabilities entries must be "
649
+ f"activegraph.packs.manifest.CapabilityDecl, got {c!r}"
650
+ )
651
+ if c.risk_class not in _RISK_CLASSES:
652
+ raise PackValidationError(
653
+ f"Pack {self.name!r}: capability "
654
+ f"{c.provider}.{c.capability} has risk_class "
655
+ f"{c.risk_class!r}; must be one of "
656
+ f"low|medium|high|critical"
657
+ )
658
+ _check_unique(
659
+ [f"{c.provider}.{c.capability}" for c in self.capabilities],
660
+ "capability",
661
+ self.name,
662
+ )
663
+
628
664
  # identity by (name, version) — CONTRACT v0.9 #2 / #6
629
665
  def __eq__(self, other: Any) -> bool:
630
666
  if not isinstance(other, Pack):
@@ -240,6 +240,8 @@ def load_pack_into_runtime(
240
240
 
241
241
  state.loaded_packs[pack.name] = pack
242
242
  state.pack_settings[pack.name] = settings_obj
243
+ # v1.4: re-loading is how a disabled pack comes back.
244
+ state.disabled_packs.discard(pack.name)
243
245
 
244
246
  # behaviors: wrap with typed-settings injection, rename to canonical
245
247
  canonical_to_pack_behavior: dict[str, Any] = {}
@@ -258,6 +260,9 @@ def load_pack_into_runtime(
258
260
  for t in pack.tools:
259
261
  canonical = f"{pack.name}.{t.name}"
260
262
  renamed = _rename_tool(t, canonical)
263
+ # v1.4: ownership stamp, symmetric with behaviors — this is
264
+ # what disable_pack filters on.
265
+ renamed._pack_owner = pack.name # type: ignore[attr-defined]
261
266
  rt._pack_tools.append(renamed)
262
267
  state.tool_owners[canonical] = pack.name
263
268
  _add_short_name(state.tool_short_to_canonical, t.name, canonical)
@@ -336,6 +341,10 @@ class PackRuntimeState:
336
341
  # pending approvals
337
342
  self.pending_approvals: list[PendingApproval] = []
338
343
  self._next_approval_n: int = 1
344
+ # v1.4: packs deregistered by runtime.disable_pack. Names stay
345
+ # here so a second disable is an idempotent no-op instead of a
346
+ # not-loaded error; load_pack clears the flag on re-load.
347
+ self.disabled_packs: set[str] = set()
339
348
 
340
349
 
341
350
  def _ensure_pack_state(rt: "Runtime") -> PackRuntimeState:
@@ -782,6 +791,18 @@ def _build_pack_loaded_payload(pack: Pack, settings_obj: BaseModel) -> dict:
782
791
  "policies": [f"{pack.name}.{p.name}" for p in pack.policies],
783
792
  "prompts": pack.prompt_manifest(),
784
793
  "settings": _canonical_settings_dump(settings_obj),
794
+ # v1.4: the declared gateway-capability surface (manifest spec
795
+ # Q8) — graph-readable, so decision surfaces and catalogs read
796
+ # a pack's declared outbound reach without importing it.
797
+ "capabilities": [
798
+ {
799
+ "provider": c.provider,
800
+ "capability": c.capability,
801
+ "risk_class": c.risk_class,
802
+ "credential_ref": c.credential_ref,
803
+ }
804
+ for c in getattr(pack, "capabilities", ())
805
+ ],
785
806
  }
786
807
 
787
808
 
@@ -402,33 +402,58 @@ def verify_surface(manifest: PackManifest, pack: Any) -> None:
402
402
  f"settings_schema: declared {manifest.settings_schema!r}, "
403
403
  f"Pack has {actual_settings!r}"
404
404
  )
405
+ # v1.4 (spec Q8, runtime half): Pack.capabilities is declarative,
406
+ # so the loader can two-way check it exactly like behaviors/tools.
407
+ # Keyed by (provider, capability); a declared pair must also agree
408
+ # on risk_class — a relabeled risk class is precisely the swap the
409
+ # decision surface needs to catch.
410
+ declared_caps = {
411
+ (c.provider, c.capability): c.risk_class for c in manifest.capabilities
412
+ }
413
+ actual_caps = {
414
+ (c.provider, c.capability): c.risk_class
415
+ for c in getattr(pack, "capabilities", ())
416
+ }
417
+ for key in sorted(set(declared_caps) - set(actual_caps)):
418
+ violations.append(
419
+ f"capabilities: declared {key[0]}.{key[1]} not on Pack"
420
+ )
421
+ for key in sorted(set(actual_caps) - set(declared_caps)):
422
+ violations.append(
423
+ f"capabilities: Pack declares {key[0]}.{key[1]} undeclared "
424
+ f"in manifest"
425
+ )
426
+ for key in sorted(set(declared_caps) & set(actual_caps)):
427
+ if declared_caps[key] != actual_caps[key]:
428
+ violations.append(
429
+ f"capabilities: {key[0]}.{key[1]} risk_class mismatch — "
430
+ f"manifest {declared_caps[key]!r}, Pack {actual_caps[key]!r}"
431
+ )
405
432
  if violations:
406
433
  raise PackManifestError(
407
434
  source=f"{manifest.name}@{manifest.version}", violations=violations
408
435
  )
409
436
 
410
437
 
411
- def compute_content_hash(pack_root: str | Path) -> str:
412
- """The spec §4 content hash over a pack directory, byte-exact.
438
+ def _hash_pack_dir(pack_root: str | Path, *, include_manifest: bool) -> str:
439
+ """The shared §4 canonical byte stream over a pack directory.
413
440
 
414
- PROVISIONAL — this function is the normative reference for all
415
- three recomputation sites (loader, downstream CI, evolution
416
- gates). Canonicalization:
441
+ ``include_manifest=False`` is the manifest-internal content hash
442
+ (a hash cannot cover itself); ``True`` is the bundle hash external
443
+ pins use (spec amendment: the manifest is the very document the
444
+ reviewer approves, so an external pin that excludes it pins
445
+ nothing that matters). Everything else is identical:
417
446
 
418
447
  1. Every regular file under the root, recursively, excluding
419
- ``manifest.toml`` itself, ``__pycache__`` directories,
420
- ``*.pyc``, and hidden (``.``-prefixed) files and directories.
421
- 2. Symlinks — file OR directory — are rejected loudly (the spec
422
- names files; this implementation extends the rejection to
423
- directory symlinks, which could otherwise smuggle unhashed
424
- content into the walk).
448
+ ``__pycache__`` directories, ``*.pyc``, and hidden
449
+ (``.``-prefixed) files and directories.
450
+ 2. Symlinks — file OR directory — are rejected loudly (directory
451
+ symlinks could otherwise smuggle unhashed content).
425
452
  3. Paths must be UTF-8-encodable and NFC-normalized; anything
426
453
  else is rejected loudly rather than hashed ambiguously across
427
454
  platforms.
428
455
  4. Relative POSIX paths sorted lexicographically by UTF-8 bytes;
429
456
  per file: path bytes, NUL, 8-byte big-endian length, raw bytes.
430
-
431
- Returns ``"sha256:" + 64 hex chars``.
432
457
  """
433
458
  root = Path(pack_root)
434
459
  if not root.is_dir():
@@ -455,7 +480,7 @@ def compute_content_hash(pack_root: str | Path) -> str:
455
480
  continue # sockets, fifos: not regular files
456
481
  if child.name.endswith(".pyc"):
457
482
  continue
458
- if rel == "manifest.toml":
483
+ if rel == "manifest.toml" and not include_manifest:
459
484
  continue
460
485
  if unicodedata.normalize("NFC", rel) != rel:
461
486
  violations.append(f"path not NFC-normalized: {rel!r}")
@@ -481,6 +506,71 @@ def compute_content_hash(pack_root: str | Path) -> str:
481
506
  return _HASH_PREFIX + h.hexdigest()
482
507
 
483
508
 
509
+ def compute_content_hash(pack_root: str | Path) -> str:
510
+ """The spec §4 content hash over a pack directory, byte-exact.
511
+
512
+ PROVISIONAL — this function is the normative reference for every
513
+ recomputation site (loader, downstream CI, evolution gates).
514
+ ``manifest.toml`` is EXCLUDED (a hash cannot cover itself); see
515
+ :func:`_hash_pack_dir` for the full canonicalization, including
516
+ this implementation's amendments over the spec draft. This is the
517
+ hash the manifest's own ``[pack.integrity].content_hash`` pins —
518
+ internal consistency only, never authenticity. External pins use
519
+ :func:`compute_bundle_hash`.
520
+
521
+ Returns ``"sha256:" + 64 hex chars``.
522
+ """
523
+ return _hash_pack_dir(pack_root, include_manifest=False)
524
+
525
+
526
+ def compute_bundle_hash(pack_root: str | Path) -> str:
527
+ """The bundle hash external pins use: the §4 walk WITHOUT the
528
+ manifest exclusion. PROVISIONAL.
529
+
530
+ Spec-review finding, accepted downstream: the content hash
531
+ excludes ``manifest.toml`` by necessity (self-reference), so an
532
+ external pin over it leaves the manifest — the very document a
533
+ reviewer approves, carrying risk classes, ``consumes``, and
534
+ ``authored_by`` — swappable without detection. ``[load.pins]``
535
+ entries, the evolution pack's proposal pins, and any resolver
536
+ verifying a fetched pack pin THIS hash instead. Same
537
+ canonicalization, same amendments, one file more.
538
+
539
+ Returns ``"sha256:" + 64 hex chars``.
540
+ """
541
+ return _hash_pack_dir(pack_root, include_manifest=True)
542
+
543
+
544
+ def verify_bundle_hash(expected: str, pack_root: str | Path) -> None:
545
+ """Recompute the bundle hash over ``pack_root`` and compare to an
546
+ EXTERNAL pin. Raises :class:`PackManifestError` on mismatch.
547
+
548
+ PROVISIONAL. ``expected`` comes from outside the pack directory —
549
+ a ``[load.pins]`` entry, a proposal record, an approval surface —
550
+ which is what makes this an authenticity check rather than the
551
+ internal-consistency check :func:`verify_content_hash` performs.
552
+ """
553
+ if not expected.startswith(_HASH_PREFIX) or not re.fullmatch(
554
+ r"[0-9a-f]{64}", expected[len(_HASH_PREFIX):]
555
+ ):
556
+ raise PackManifestError(
557
+ source=str(pack_root),
558
+ violations=[
559
+ f"external pin {expected!r} must be 'sha256:' + 64 "
560
+ f"lowercase hex chars"
561
+ ],
562
+ )
563
+ actual = compute_bundle_hash(pack_root)
564
+ if actual != expected:
565
+ raise PackManifestError(
566
+ source=str(pack_root),
567
+ violations=[
568
+ f"bundle hash mismatch: external pin is {expected}, "
569
+ f"directory (manifest included) hashes to {actual}"
570
+ ],
571
+ )
572
+
573
+
484
574
  def verify_content_hash(
485
575
  manifest: PackManifest, pack_root: str | Path
486
576
  ) -> None:
@@ -2044,6 +2044,137 @@ class Runtime:
2044
2044
  return []
2045
2045
  return list(self._pack_state.loaded_packs.values())
2046
2046
 
2047
+ def disable_pack(self, name: str) -> bool:
2048
+ """Deregister a loaded pack: its behaviors stop firing NOW.
2049
+
2050
+ CONTRACT v1.4 #3. The pack's behaviors, tools, typed-object
2051
+ schemas, relation specs, gating policies, and settings are
2052
+ removed from this runtime's live registries and the registry
2053
+ is rebuilt — nothing pack-owned matches another event from
2054
+ this call onward. What deliberately does NOT happen:
2055
+
2056
+ - **State is untouched.** Objects and relations the pack
2057
+ created remain; disabling code never rewrites history.
2058
+ - **Memory is not reclaimed.** Python cannot honestly unload
2059
+ imported code; the objects stay in memory, inert. Restart
2060
+ to evict (hosts doing boot-time pack loading key off the
2061
+ ``pack.disabled`` event this method emits).
2062
+ - **Pending approvals remain pending** — resolvable or not,
2063
+ they are recorded state, not registry state.
2064
+
2065
+ Emits ``pack.disabled`` (queue-visible, like ``pack.loaded``)
2066
+ with the deregistered surface, so behaviors and boot loaders
2067
+ can react from the log. Idempotent: disabling an
2068
+ already-disabled pack returns ``False`` and emits nothing.
2069
+ Re-enabling is ``load_pack`` again (which clears the disabled
2070
+ flag). Raises
2071
+ :class:`~activegraph.packs.PackNotFoundError` for a name this
2072
+ runtime never loaded.
2073
+
2074
+ Returns ``True`` when the pack was live and is now disabled.
2075
+ """
2076
+ from activegraph.packs import PackNotFoundError
2077
+ from activegraph.packs.loader import AMBIGUOUS
2078
+
2079
+ state = self._pack_state
2080
+ if state is None or name not in state.loaded_packs:
2081
+ if state is not None and name in state.disabled_packs:
2082
+ return False # idempotent second disable
2083
+ loaded = tuple(state.loaded_packs.keys()) if state else ()
2084
+ raise PackNotFoundError(name, installed=loaded)
2085
+
2086
+ pack = state.loaded_packs.pop(name)
2087
+ state.pack_settings.pop(name, None)
2088
+
2089
+ removed_behaviors = sorted(
2090
+ c for c, owner in state.behavior_owners.items() if owner == name
2091
+ )
2092
+ removed_tools = sorted(
2093
+ c for c, owner in state.tool_owners.items() if owner == name
2094
+ )
2095
+ removed_object_types = sorted(
2096
+ t for t, owner in state.object_type_owners.items() if owner == name
2097
+ )
2098
+ removed_relation_types = sorted(
2099
+ t
2100
+ for t, owner in state.relation_type_owners.items()
2101
+ if owner == name
2102
+ )
2103
+ for c in removed_behaviors:
2104
+ state.behavior_owners.pop(c, None)
2105
+ for c in removed_tools:
2106
+ state.tool_owners.pop(c, None)
2107
+ for t in removed_object_types:
2108
+ state.object_type_owners.pop(t, None)
2109
+ state.object_type_schemas.pop(t, None)
2110
+ for t in removed_relation_types:
2111
+ state.relation_type_owners.pop(t, None)
2112
+ state.relation_type_specs.pop(t, None)
2113
+ for c in [
2114
+ c for c, owner in state.policy_owners.items() if owner == name
2115
+ ]:
2116
+ state.policy_owners.pop(c, None)
2117
+ prefix = f"{name}."
2118
+ for otype in list(state.gated_object_types):
2119
+ remaining = [
2120
+ p
2121
+ for p in state.gated_object_types[otype]
2122
+ if not p.startswith(prefix)
2123
+ ]
2124
+ if remaining:
2125
+ state.gated_object_types[otype] = remaining
2126
+ else:
2127
+ del state.gated_object_types[otype]
2128
+
2129
+ # Short-name maps: recompute from the surviving canonicals —
2130
+ # removal can RESOLVE an ambiguity, not just delete entries.
2131
+ def _rebuild_shorts(owners: dict[str, str]) -> dict[str, str]:
2132
+ shorts: dict[str, str] = {}
2133
+ for canonical in owners:
2134
+ short = canonical.split(".", 1)[1]
2135
+ shorts[short] = (
2136
+ AMBIGUOUS if short in shorts else canonical
2137
+ )
2138
+ return shorts
2139
+
2140
+ state.behavior_short_to_canonical = _rebuild_shorts(
2141
+ state.behavior_owners
2142
+ )
2143
+ state.tool_short_to_canonical = _rebuild_shorts(state.tool_owners)
2144
+
2145
+ self._pack_behaviors = [
2146
+ b
2147
+ for b in self._pack_behaviors
2148
+ if getattr(b, "_pack_owner", None) != name
2149
+ ]
2150
+ self._pack_tools = [
2151
+ t
2152
+ for t in self._pack_tools
2153
+ if getattr(t, "_pack_owner", None) != name
2154
+ ]
2155
+ state.disabled_packs.add(name)
2156
+ self._ensure_registry()
2157
+
2158
+ self.graph.emit(
2159
+ Event(
2160
+ id=self.graph.ids.event(),
2161
+ type="pack.disabled",
2162
+ payload={
2163
+ "name": name,
2164
+ "version": pack.version,
2165
+ "behaviors": removed_behaviors,
2166
+ "tools": removed_tools,
2167
+ "object_types": removed_object_types,
2168
+ "relation_types": removed_relation_types,
2169
+ },
2170
+ actor="runtime",
2171
+ frame_id=self.frame.id if self.frame else None,
2172
+ caused_by=None,
2173
+ timestamp=self.graph.clock.now(),
2174
+ )
2175
+ )
2176
+ return True
2177
+
2047
2178
  def get_behavior(self, name: str) -> Any:
2048
2179
  """Look up a registered behavior by canonical or short name.
2049
2180
 
@@ -2419,6 +2550,11 @@ class Runtime:
2419
2550
  store = _open_sqlite_store(path, run_id=chosen)
2420
2551
  graph = Graph(ids=IDGen(), run_id=chosen, graph_store=graph_store)
2421
2552
  events = list(store.iter_events())
2553
+ # CONTRACT v1.5 #2: a compacted run's hot log begins with its
2554
+ # runtime.snapshot event; project the snapshot state first so
2555
+ # the suffix replays against the base it was recorded on.
2556
+ if events and events[0].type == "runtime.snapshot":
2557
+ _materialize_snapshot(graph, store, events[0])
2422
2558
  for ev in events:
2423
2559
  graph._replay_event(ev) # noqa: SLF001 — internal seam
2424
2560
  graph.ids.reseed_from_events(events)
@@ -2576,6 +2712,11 @@ class Runtime:
2576
2712
  fork_store = SQLiteEventStore(store.path, run_id=new_run_id)
2577
2713
  fork_graph = Graph(ids=IDGen(), run_id=new_run_id, graph_store=graph_store)
2578
2714
  events = list(fork_store.iter_events())
2715
+ # CONTRACT v1.5 #2: forking a compacted parent copies its
2716
+ # runtime.snapshot event; the fork's base state comes from the
2717
+ # shared snapshot sidecar exactly as a load would.
2718
+ if events and events[0].type == "runtime.snapshot":
2719
+ _materialize_snapshot(fork_graph, fork_store, events[0])
2579
2720
  for ev in events:
2580
2721
  fork_graph._replay_event(ev) # noqa: SLF001
2581
2722
  fork_graph.ids.reseed_from_events(events)
@@ -3285,6 +3426,13 @@ def _verify_replay(
3285
3426
  cache = LLMCache.from_events(recorded_events)
3286
3427
 
3287
3428
  fresh = Graph(ids=IDGen(), run_id="verify_" + loaded_graph.run_id)
3429
+ # CONTRACT v1.5 #2: compacted runs verify their post-snapshot
3430
+ # suffix against the snapshot base; the archived prefix is
3431
+ # verified separately by store.retention.verify_snapshot.
3432
+ if recorded_events and recorded_events[0].type == "runtime.snapshot":
3433
+ _materialize_snapshot(
3434
+ fresh, loaded_graph.store, recorded_events[0]
3435
+ )
3288
3436
  fresh_rt = Runtime(
3289
3437
  fresh,
3290
3438
  behaviors=behaviors,
@@ -3407,6 +3555,69 @@ def _is_lifecycle(e: Event) -> bool:
3407
3555
  )
3408
3556
 
3409
3557
 
3558
+ def _materialize_snapshot(graph: Graph, store: Any, snapshot_event: Event) -> None:
3559
+ """Rebuild a compacted run's base state from its snapshot blob.
3560
+
3561
+ CONTRACT v1.5 #2: the blob is fetched from the store's snapshot
3562
+ sidecar by the hash recorded in the ``runtime.snapshot`` event,
3563
+ verified against that hash (fail-loud on mismatch — a corrupted
3564
+ sidecar must never silently produce wrong state), and projected
3565
+ into the graph before the post-snapshot suffix replays. Id
3566
+ counters recorded at compact time prime the generators so fresh
3567
+ mints can't collide with archived history.
3568
+ """
3569
+ from activegraph.core.graph import Object, Relation
3570
+ from activegraph.store.retention import (
3571
+ SnapshotIntegrityError,
3572
+ state_hash_of,
3573
+ )
3574
+
3575
+ payload = snapshot_event.payload or {}
3576
+ expected = str(payload.get("state_hash", ""))
3577
+ blob = store.get_snapshot(expected) if store is not None else None
3578
+ if blob is None:
3579
+ raise SnapshotIntegrityError(
3580
+ run_id=graph.run_id,
3581
+ expected=expected,
3582
+ detail="the snapshot blob is missing from the sidecar table.",
3583
+ )
3584
+ if state_hash_of(blob) != expected:
3585
+ raise SnapshotIntegrityError(
3586
+ run_id=graph.run_id,
3587
+ expected=expected,
3588
+ detail="the stored blob does not hash to the recorded state hash.",
3589
+ )
3590
+ state = json.loads(blob)
3591
+ for o in state.get("objects", []):
3592
+ graph._state.put_object( # noqa: SLF001 — the replay seam
3593
+ Object(
3594
+ id=o["id"],
3595
+ type=o["type"],
3596
+ data=o["data"],
3597
+ version=o.get("version", 1),
3598
+ provenance=o.get("provenance", {}),
3599
+ )
3600
+ )
3601
+ for r in state.get("relations", []):
3602
+ graph._state.put_relation( # noqa: SLF001
3603
+ Relation(
3604
+ id=r["id"],
3605
+ source=r["source"],
3606
+ target=r["target"],
3607
+ type=r["type"],
3608
+ data=r.get("data", {}),
3609
+ provenance=r.get("provenance", {}),
3610
+ )
3611
+ )
3612
+ counters = payload.get("id_counters", {})
3613
+ ids = graph.ids
3614
+ ids._object_counter = max(ids._object_counter, int(counters.get("object", 0))) # noqa: SLF001
3615
+ ids._event_counter = max(ids._event_counter, int(counters.get("event", 0))) # noqa: SLF001
3616
+ ids._relation_counter = max(ids._relation_counter, int(counters.get("relation", 0))) # noqa: SLF001
3617
+ ids._patch_counter = max(ids._patch_counter, int(counters.get("patch", 0))) # noqa: SLF001
3618
+ ids._frame_counter = max(ids._frame_counter, int(counters.get("frame", 0))) # noqa: SLF001
3619
+
3620
+
3410
3621
  def _rebuild_pending_approvals(rt: "Runtime", events: list[Event]) -> None:
3411
3622
  """Reconstruct the pending-approval queue from the event log. v1.4.
3412
3623