activegraph 1.3.0__tar.gz → 1.4.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 (186) hide show
  1. {activegraph-1.3.0/activegraph.egg-info → activegraph-1.4.0}/PKG-INFO +1 -1
  2. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/__init__.py +1 -1
  3. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/__init__.py +37 -1
  4. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/loader.py +21 -0
  5. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/manifest.py +104 -14
  6. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/runtime.py +131 -0
  7. {activegraph-1.3.0 → activegraph-1.4.0/activegraph.egg-info}/PKG-INFO +1 -1
  8. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph.egg-info/SOURCES.txt +1 -0
  9. {activegraph-1.3.0 → activegraph-1.4.0}/pyproject.toml +1 -1
  10. activegraph-1.4.0/tests/test_disable_pack.py +155 -0
  11. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_pack_manifest.py +162 -1
  12. {activegraph-1.3.0 → activegraph-1.4.0}/LICENSE +0 -0
  13. {activegraph-1.3.0 → activegraph-1.4.0}/NOTICE +0 -0
  14. {activegraph-1.3.0 → activegraph-1.4.0}/README.md +0 -0
  15. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/__main__.py +0 -0
  16. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/_signature.py +0 -0
  17. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/behaviors/__init__.py +0 -0
  18. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/behaviors/base.py +0 -0
  19. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/behaviors/decorators.py +0 -0
  20. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/cli/__init__.py +0 -0
  21. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/cli/main.py +0 -0
  22. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/cli/quickstart.py +0 -0
  23. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/cli/renderers.py +0 -0
  24. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/__init__.py +0 -0
  25. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/clock.py +0 -0
  26. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/event.py +0 -0
  27. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/graph.py +0 -0
  28. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/graph_store.py +0 -0
  29. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/ids.py +0 -0
  30. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/patch.py +0 -0
  31. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/core/view.py +0 -0
  32. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/errors.py +0 -0
  33. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/frame.py +0 -0
  34. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/__init__.py +0 -0
  35. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/anthropic.py +0 -0
  36. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/cache.py +0 -0
  37. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/embedding.py +0 -0
  38. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/errors.py +0 -0
  39. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/native.py +0 -0
  40. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/openai.py +0 -0
  41. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/parsing.py +0 -0
  42. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/prompt.py +0 -0
  43. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/provider.py +0 -0
  44. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/recorded.py +0 -0
  45. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/types.py +0 -0
  46. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/llm/wire.py +0 -0
  47. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/__init__.py +0 -0
  48. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/logging.py +0 -0
  49. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/metrics.py +0 -0
  50. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/migration.py +0 -0
  51. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/otel.py +0 -0
  52. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/prometheus.py +0 -0
  53. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/observability/status.py +0 -0
  54. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/__init__.py +0 -0
  55. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/behaviors.py +0 -0
  56. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  57. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  58. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/object_types.py +0 -0
  59. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  60. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  61. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  62. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  63. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/settings.py +0 -0
  64. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/diligence/tools.py +0 -0
  65. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/packs/scaffold.py +0 -0
  66. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/policy.py +0 -0
  67. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/__init__.py +0 -0
  68. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/_live.py +0 -0
  69. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/behavior_graph.py +0 -0
  70. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/budget.py +0 -0
  71. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/config_errors.py +0 -0
  72. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/diff.py +0 -0
  73. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/errors.py +0 -0
  74. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/exec_errors.py +0 -0
  75. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/patterns.py +0 -0
  76. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/promote.py +0 -0
  77. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/queue.py +0 -0
  78. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/registration_errors.py +0 -0
  79. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/registry.py +0 -0
  80. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/scheduler.py +0 -0
  81. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/runtime/view_builder.py +0 -0
  82. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/__init__.py +0 -0
  83. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/base.py +0 -0
  84. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/conformance.py +0 -0
  85. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/errors.py +0 -0
  86. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/falkordb.py +0 -0
  87. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/graph_conformance.py +0 -0
  88. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/memory.py +0 -0
  89. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/postgres.py +0 -0
  90. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/serde.py +0 -0
  91. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/sqlite.py +0 -0
  92. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/store/url.py +0 -0
  93. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/__init__.py +0 -0
  94. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/base.py +0 -0
  95. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/cache.py +0 -0
  96. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/context.py +0 -0
  97. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/decorators.py +0 -0
  98. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/errors.py +0 -0
  99. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/graph_query.py +0 -0
  100. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/recorded.py +0 -0
  101. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/tools/web_fetch.py +0 -0
  102. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/trace/__init__.py +0 -0
  103. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/trace/causal.py +0 -0
  104. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph/trace/printer.py +0 -0
  105. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph.egg-info/dependency_links.txt +0 -0
  106. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph.egg-info/entry_points.txt +0 -0
  107. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph.egg-info/requires.txt +0 -0
  108. {activegraph-1.3.0 → activegraph-1.4.0}/activegraph.egg-info/top_level.txt +0 -0
  109. {activegraph-1.3.0 → activegraph-1.4.0}/setup.cfg +0 -0
  110. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_activate_after.py +0 -0
  111. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_causal_cross_tool.py +0 -0
  112. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_cli.py +0 -0
  113. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_cli_docs_flags.py +0 -0
  114. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_clock.py +0 -0
  115. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_diff.py +0 -0
  116. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_diligence_pack.py +0 -0
  117. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_diligence_with_tools.py +0 -0
  118. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_doc_links.py +0 -0
  119. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_doc_python_snippets.py +0 -0
  120. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_doc_site_reachable.py +0 -0
  121. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_embedding_provider.py +0 -0
  122. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_errors_format.py +0 -0
  123. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_event.py +0 -0
  124. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_falkordb_store.py +0 -0
  125. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_fork.py +0 -0
  126. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_graph.py +0 -0
  127. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_graph_store.py +0 -0
  128. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_ids.py +0 -0
  129. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_license.py +0 -0
  130. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_anthropic.py +0 -0
  131. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_behavior.py +0 -0
  132. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_budget.py +0 -0
  133. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_causal.py +0 -0
  134. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_claim_extraction.py +0 -0
  135. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_default_model.py +0 -0
  136. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_determinism.py +0 -0
  137. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_failure.py +0 -0
  138. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_native_structured_output.py +0 -0
  139. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_openai.py +0 -0
  140. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_prompt.py +0 -0
  141. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_provider_fixtures.py +0 -0
  142. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_replay.py +0 -0
  143. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_tool_loop.py +0 -0
  144. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_trace.py +0 -0
  145. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_trace_snapshot.py +0 -0
  146. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_types.py +0 -0
  147. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llm_wire.py +0 -0
  148. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_llms_txt.py +0 -0
  149. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_migration.py +0 -0
  150. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_observability_logging.py +0 -0
  151. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_observability_metrics.py +0 -0
  152. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_operate_example.py +0 -0
  153. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_pack_scaffold.py +0 -0
  154. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_packs.py +0 -0
  155. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_patch.py +0 -0
  156. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_pattern_matcher.py +0 -0
  157. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_pattern_parser.py +0 -0
  158. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_pattern_subscriptions.py +0 -0
  159. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_persistence.py +0 -0
  160. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_postgres_store.py +0 -0
  161. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_promote.py +0 -0
  162. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_quickstart.py +0 -0
  163. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_quickstart_snapshot.py +0 -0
  164. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_reason_codes_docs.py +0 -0
  165. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_registration_validation.py +0 -0
  166. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_replay.py +0 -0
  167. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_replay_trace_snapshot.py +0 -0
  168. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_requeue_unfired.py +0 -0
  169. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_resume_example.py +0 -0
  170. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_runtime.py +0 -0
  171. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_runtime_status.py +0 -0
  172. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_serde.py +0 -0
  173. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_store_conformance.py +0 -0
  174. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_store_url.py +0 -0
  175. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_tool_replay.py +0 -0
  176. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_tool_trace_snapshot.py +0 -0
  177. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_tools.py +0 -0
  178. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_trace.py +0 -0
  179. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_trace_accessors.py +0 -0
  180. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_tutorial_snippets.py +0 -0
  181. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_v1_0_1_patches.py +0 -0
  182. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  183. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  184. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_version_sync.py +0 -0
  185. {activegraph-1.3.0 → activegraph-1.4.0}/tests/test_view.py +0 -0
  186. {activegraph-1.3.0 → activegraph-1.4.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.4.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.4.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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -111,6 +111,7 @@ tests/test_clock.py
111
111
  tests/test_diff.py
112
112
  tests/test_diligence_pack.py
113
113
  tests/test_diligence_with_tools.py
114
+ tests/test_disable_pack.py
114
115
  tests/test_doc_links.py
115
116
  tests/test_doc_python_snippets.py
116
117
  tests/test_doc_site_reachable.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "activegraph"
10
- version = "1.3.0"
10
+ version = "1.4.0"
11
11
  description = "An event-sourced reactive graph runtime for long-running, auditable, agentic systems."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,155 @@
1
+ """runtime.disable_pack: deregistration, not unload (CONTRACT v1.4 #3).
2
+
3
+ Behaviors stop firing immediately, tools stop resolving, typed
4
+ validation reverts to untyped, state stays, memory is not reclaimed
5
+ (restart to evict), re-enable = load_pack, idempotent second disable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import pytest
11
+ from pydantic import BaseModel
12
+
13
+ from activegraph import Graph, Runtime, clear_registry
14
+ from activegraph import ToolNotFoundError
15
+ from activegraph.packs import (
16
+ ObjectType,
17
+ Pack,
18
+ PackNotFoundError,
19
+ behavior as pack_behavior,
20
+ )
21
+
22
+
23
+ class _NoteSchema(BaseModel):
24
+ text: str
25
+
26
+
27
+ def _candidate_pack(hits: list[str]) -> Pack:
28
+ @pack_behavior(name="echo", on=["goal.created"])
29
+ def echo(event, graph, ctx):
30
+ hits.append(event.payload.get("goal", ""))
31
+ graph.add_object("echo_note", {"text": "hi"})
32
+
33
+ from activegraph.packs import tool as pack_tool
34
+
35
+ @pack_tool(name="shout")
36
+ def shout(args, ctx):
37
+ return {"ok": True}
38
+
39
+ return Pack(
40
+ name="candidate",
41
+ version="1.0",
42
+ object_types=(ObjectType(name="typed_note", schema=_NoteSchema),),
43
+ behaviors=(echo,),
44
+ tools=(shout,),
45
+ )
46
+
47
+
48
+ def test_disable_stops_behaviors_and_tools_immediately():
49
+ clear_registry()
50
+ hits: list[str] = []
51
+ rt = Runtime(Graph())
52
+ rt.load_pack(_candidate_pack(hits))
53
+
54
+ rt.run_goal("first")
55
+ assert hits == ["first"]
56
+ assert rt.get_tool("candidate.shout").name == "candidate.shout"
57
+
58
+ assert rt.disable_pack("candidate") is True
59
+ rt.run_goal("second")
60
+ assert hits == ["first"] # behavior no longer fires
61
+ with pytest.raises(ToolNotFoundError):
62
+ rt.get_tool("candidate.shout")
63
+ with pytest.raises(ToolNotFoundError):
64
+ rt.get_tool("shout")
65
+ assert rt.loaded_packs() == []
66
+
67
+
68
+ def test_disable_leaves_state_and_reverts_typing():
69
+ clear_registry()
70
+ hits: list[str] = []
71
+ rt = Runtime(Graph())
72
+ rt.load_pack(_candidate_pack(hits))
73
+ rt.run_goal("go")
74
+ created = next(
75
+ o for o in rt.graph.all_objects() if o.type == "echo_note"
76
+ )
77
+
78
+ # Typed while loaded: schema enforced.
79
+ from activegraph.packs import PackSchemaViolation
80
+
81
+ with pytest.raises(PackSchemaViolation):
82
+ rt.graph.add_object("typed_note", {"wrong_field": 1})
83
+
84
+ rt.disable_pack("candidate")
85
+ # Pack-created state is untouched...
86
+ assert rt.graph.get_object(created.id) is not None
87
+ # ...and the type reverts to v0.9 untyped semantics.
88
+ obj = rt.graph.add_object("typed_note", {"wrong_field": 1})
89
+ assert rt.graph.get_object(obj.id) is not None
90
+
91
+
92
+ def test_disable_emits_event_and_is_idempotent():
93
+ clear_registry()
94
+ rt = Runtime(Graph())
95
+ rt.load_pack(_candidate_pack([]))
96
+
97
+ assert rt.disable_pack("candidate") is True
98
+ ev = next(e for e in rt.graph.events if e.type == "pack.disabled")
99
+ assert ev.payload["name"] == "candidate"
100
+ assert ev.payload["behaviors"] == ["candidate.echo"]
101
+ assert ev.payload["tools"] == ["candidate.shout"]
102
+ assert ev.payload["object_types"] == ["typed_note"]
103
+
104
+ # Idempotent second call: False, no second event.
105
+ assert rt.disable_pack("candidate") is False
106
+ assert (
107
+ len([e for e in rt.graph.events if e.type == "pack.disabled"]) == 1
108
+ )
109
+
110
+
111
+ def test_disable_unknown_pack_is_loud():
112
+ clear_registry()
113
+ rt = Runtime(Graph())
114
+ with pytest.raises(PackNotFoundError):
115
+ rt.disable_pack("never_loaded")
116
+
117
+
118
+ def test_reload_reenables():
119
+ clear_registry()
120
+ hits: list[str] = []
121
+ pack = _candidate_pack(hits)
122
+ rt = Runtime(Graph())
123
+ rt.load_pack(pack)
124
+ rt.disable_pack("candidate")
125
+
126
+ assert rt.load_pack(pack) is True # fresh load, not idempotent skip
127
+ rt.run_goal("after re-enable")
128
+ assert hits == ["after re-enable"]
129
+ assert rt.get_tool("candidate.shout").name == "candidate.shout"
130
+ # And it can be disabled again.
131
+ assert rt.disable_pack("candidate") is True
132
+
133
+
134
+ def test_disable_resolves_short_name_ambiguity():
135
+ # Two packs export a behavior with the same short name; disabling
136
+ # one must RESOLVE the ambiguity, not leave a stale sentinel.
137
+ clear_registry()
138
+
139
+ def mk(pack_name):
140
+ @pack_behavior(name="worker", on=["goal.created"])
141
+ def worker(event, graph, ctx):
142
+ pass
143
+
144
+ return Pack(name=pack_name, version="1.0", behaviors=(worker,))
145
+
146
+ rt = Runtime(Graph())
147
+ rt.load_pack(mk("alpha"))
148
+ rt.load_pack(mk("beta"))
149
+ from activegraph.runtime.registration_errors import AmbiguousBehaviorError
150
+
151
+ with pytest.raises(AmbiguousBehaviorError):
152
+ rt.get_behavior("worker")
153
+
154
+ rt.disable_pack("alpha")
155
+ assert rt.get_behavior("worker").name == "beta.worker"