activegraph 1.4.0__tar.gz → 1.6.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 (192) hide show
  1. {activegraph-1.4.0/activegraph.egg-info → activegraph-1.6.0}/PKG-INFO +1 -1
  2. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/__init__.py +1 -1
  3. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/loader.py +108 -0
  4. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/runtime.py +80 -0
  5. activegraph-1.6.0/activegraph/sandbox/__init__.py +257 -0
  6. activegraph-1.6.0/activegraph/sandbox/_child.py +234 -0
  7. activegraph-1.6.0/activegraph/store/retention.py +316 -0
  8. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/sqlite.py +126 -0
  9. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/trace/printer.py +12 -0
  10. {activegraph-1.4.0 → activegraph-1.6.0/activegraph.egg-info}/PKG-INFO +1 -1
  11. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph.egg-info/SOURCES.txt +6 -0
  12. {activegraph-1.4.0 → activegraph-1.6.0}/pyproject.toml +1 -1
  13. activegraph-1.6.0/tests/test_compaction.py +249 -0
  14. activegraph-1.6.0/tests/test_manifest_warning_tier.py +171 -0
  15. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_promote.py +61 -0
  16. activegraph-1.6.0/tests/test_sandbox_trial.py +367 -0
  17. {activegraph-1.4.0 → activegraph-1.6.0}/LICENSE +0 -0
  18. {activegraph-1.4.0 → activegraph-1.6.0}/NOTICE +0 -0
  19. {activegraph-1.4.0 → activegraph-1.6.0}/README.md +0 -0
  20. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/__main__.py +0 -0
  21. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/_signature.py +0 -0
  22. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/behaviors/__init__.py +0 -0
  23. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/behaviors/base.py +0 -0
  24. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/behaviors/decorators.py +0 -0
  25. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/cli/__init__.py +0 -0
  26. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/cli/main.py +0 -0
  27. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/cli/quickstart.py +0 -0
  28. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/cli/renderers.py +0 -0
  29. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/__init__.py +0 -0
  30. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/clock.py +0 -0
  31. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/event.py +0 -0
  32. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/graph.py +0 -0
  33. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/graph_store.py +0 -0
  34. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/ids.py +0 -0
  35. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/patch.py +0 -0
  36. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/core/view.py +0 -0
  37. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/errors.py +0 -0
  38. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/frame.py +0 -0
  39. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/__init__.py +0 -0
  40. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/anthropic.py +0 -0
  41. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/cache.py +0 -0
  42. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/embedding.py +0 -0
  43. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/errors.py +0 -0
  44. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/native.py +0 -0
  45. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/openai.py +0 -0
  46. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/parsing.py +0 -0
  47. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/prompt.py +0 -0
  48. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/provider.py +0 -0
  49. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/recorded.py +0 -0
  50. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/types.py +0 -0
  51. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/llm/wire.py +0 -0
  52. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/__init__.py +0 -0
  53. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/logging.py +0 -0
  54. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/metrics.py +0 -0
  55. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/migration.py +0 -0
  56. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/otel.py +0 -0
  57. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/prometheus.py +0 -0
  58. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/observability/status.py +0 -0
  59. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/__init__.py +0 -0
  60. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/__init__.py +0 -0
  61. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/behaviors.py +0 -0
  62. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  63. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  64. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/object_types.py +0 -0
  65. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  66. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  67. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  68. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  69. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/settings.py +0 -0
  70. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/diligence/tools.py +0 -0
  71. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/manifest.py +0 -0
  72. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/packs/scaffold.py +0 -0
  73. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/policy.py +0 -0
  74. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/__init__.py +0 -0
  75. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/_live.py +0 -0
  76. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/behavior_graph.py +0 -0
  77. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/budget.py +0 -0
  78. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/config_errors.py +0 -0
  79. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/diff.py +0 -0
  80. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/errors.py +0 -0
  81. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/exec_errors.py +0 -0
  82. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/patterns.py +0 -0
  83. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/promote.py +0 -0
  84. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/queue.py +0 -0
  85. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/registration_errors.py +0 -0
  86. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/registry.py +0 -0
  87. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/scheduler.py +0 -0
  88. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/runtime/view_builder.py +0 -0
  89. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/__init__.py +0 -0
  90. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/base.py +0 -0
  91. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/conformance.py +0 -0
  92. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/errors.py +0 -0
  93. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/falkordb.py +0 -0
  94. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/graph_conformance.py +0 -0
  95. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/memory.py +0 -0
  96. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/postgres.py +0 -0
  97. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/serde.py +0 -0
  98. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/store/url.py +0 -0
  99. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/__init__.py +0 -0
  100. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/base.py +0 -0
  101. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/cache.py +0 -0
  102. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/context.py +0 -0
  103. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/decorators.py +0 -0
  104. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/errors.py +0 -0
  105. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/graph_query.py +0 -0
  106. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/recorded.py +0 -0
  107. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/tools/web_fetch.py +0 -0
  108. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/trace/__init__.py +0 -0
  109. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph/trace/causal.py +0 -0
  110. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph.egg-info/dependency_links.txt +0 -0
  111. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph.egg-info/entry_points.txt +0 -0
  112. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph.egg-info/requires.txt +0 -0
  113. {activegraph-1.4.0 → activegraph-1.6.0}/activegraph.egg-info/top_level.txt +0 -0
  114. {activegraph-1.4.0 → activegraph-1.6.0}/setup.cfg +0 -0
  115. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_activate_after.py +0 -0
  116. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_causal_cross_tool.py +0 -0
  117. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_cli.py +0 -0
  118. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_cli_docs_flags.py +0 -0
  119. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_clock.py +0 -0
  120. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_diff.py +0 -0
  121. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_diligence_pack.py +0 -0
  122. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_diligence_with_tools.py +0 -0
  123. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_disable_pack.py +0 -0
  124. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_doc_links.py +0 -0
  125. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_doc_python_snippets.py +0 -0
  126. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_doc_site_reachable.py +0 -0
  127. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_embedding_provider.py +0 -0
  128. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_errors_format.py +0 -0
  129. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_event.py +0 -0
  130. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_falkordb_store.py +0 -0
  131. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_fork.py +0 -0
  132. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_graph.py +0 -0
  133. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_graph_store.py +0 -0
  134. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_ids.py +0 -0
  135. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_license.py +0 -0
  136. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_anthropic.py +0 -0
  137. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_behavior.py +0 -0
  138. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_budget.py +0 -0
  139. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_causal.py +0 -0
  140. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_claim_extraction.py +0 -0
  141. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_default_model.py +0 -0
  142. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_determinism.py +0 -0
  143. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_failure.py +0 -0
  144. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_native_structured_output.py +0 -0
  145. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_openai.py +0 -0
  146. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_prompt.py +0 -0
  147. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_provider_fixtures.py +0 -0
  148. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_replay.py +0 -0
  149. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_tool_loop.py +0 -0
  150. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_trace.py +0 -0
  151. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_trace_snapshot.py +0 -0
  152. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_types.py +0 -0
  153. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llm_wire.py +0 -0
  154. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_llms_txt.py +0 -0
  155. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_migration.py +0 -0
  156. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_observability_logging.py +0 -0
  157. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_observability_metrics.py +0 -0
  158. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_operate_example.py +0 -0
  159. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_pack_manifest.py +0 -0
  160. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_pack_scaffold.py +0 -0
  161. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_packs.py +0 -0
  162. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_patch.py +0 -0
  163. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_pattern_matcher.py +0 -0
  164. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_pattern_parser.py +0 -0
  165. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_pattern_subscriptions.py +0 -0
  166. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_persistence.py +0 -0
  167. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_postgres_store.py +0 -0
  168. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_quickstart.py +0 -0
  169. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_quickstart_snapshot.py +0 -0
  170. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_reason_codes_docs.py +0 -0
  171. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_registration_validation.py +0 -0
  172. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_replay.py +0 -0
  173. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_replay_trace_snapshot.py +0 -0
  174. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_requeue_unfired.py +0 -0
  175. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_resume_example.py +0 -0
  176. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_runtime.py +0 -0
  177. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_runtime_status.py +0 -0
  178. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_serde.py +0 -0
  179. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_store_conformance.py +0 -0
  180. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_store_url.py +0 -0
  181. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_tool_replay.py +0 -0
  182. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_tool_trace_snapshot.py +0 -0
  183. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_tools.py +0 -0
  184. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_trace.py +0 -0
  185. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_trace_accessors.py +0 -0
  186. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_tutorial_snippets.py +0 -0
  187. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_v1_0_1_patches.py +0 -0
  188. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  189. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  190. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_version_sync.py +0 -0
  191. {activegraph-1.4.0 → activegraph-1.6.0}/tests/test_view.py +0 -0
  192. {activegraph-1.4.0 → activegraph-1.6.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.4.0
3
+ Version: 1.6.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.4.0"
245
+ __version__ = "1.6.0"
@@ -27,6 +27,9 @@ from __future__ import annotations
27
27
  import copy
28
28
  import inspect
29
29
  import json
30
+ import logging
31
+ import sys
32
+ from pathlib import Path
30
33
  from typing import TYPE_CHECKING, Any, Callable, Optional
31
34
 
32
35
  from pydantic import BaseModel, ValidationError
@@ -309,9 +312,114 @@ def load_pack_into_runtime(
309
312
  timestamp=rt.graph.clock.now(),
310
313
  )
311
314
  )
315
+
316
+ # ---- 6. manifest warning tier (CONTRACT v1.6 #1) ----------------------
317
+ _warn_on_manifest_violations(pack)
312
318
  return True
313
319
 
314
320
 
321
+ # --------------------------------------------- manifest warning tier
322
+ # CONTRACT v1.6 #1: when a manifest.toml is discoverable next to the
323
+ # pack's defining modules, load_pack runs load_manifest +
324
+ # verify_surface and WARNs on violations — structured, once per pack
325
+ # per process, never an error before 2.0. Absent manifest: silent.
326
+ # Hash verification (content/bundle) stays host/CI territory: the
327
+ # loader never re-hashes a pack directory on the hot path.
328
+
329
+ _manifest_log = logging.getLogger("activegraph.packs.manifest")
330
+
331
+ # (pack name, pack version, manifest path) triples already validated
332
+ # this process — clean or not, the tier runs once per pack.
333
+ _manifest_checked: set[tuple[str, str, str]] = set()
334
+
335
+
336
+ def _locate_pack_manifest(pack: Pack) -> Optional[Path]:
337
+ """Best-effort pack-root discovery: resolve the modules that define
338
+ the pack's components and walk up (while still inside the package)
339
+ looking for a sibling ``manifest.toml``. Returns None when the
340
+ pack has no discoverable manifest — which is not a violation.
341
+ """
342
+ components: list[Any] = [getattr(b, "fn", None) for b in pack.behaviors]
343
+ components += [getattr(t, "fn", None) for t in pack.tools]
344
+ if pack.settings_schema is not EmptySettings:
345
+ components.append(pack.settings_schema)
346
+ components += [ot.schema for ot in pack.object_types]
347
+
348
+ seen_modules: set[str] = set()
349
+ for comp in components:
350
+ mod_name = getattr(comp, "__module__", None)
351
+ if not mod_name or mod_name == "__main__" or mod_name in seen_modules:
352
+ continue
353
+ seen_modules.add(mod_name)
354
+ mod_file = getattr(sys.modules.get(mod_name), "__file__", None)
355
+ if not mod_file:
356
+ continue
357
+ directory = Path(mod_file).resolve().parent
358
+ while True:
359
+ candidate = directory / "manifest.toml"
360
+ if candidate.is_file():
361
+ return candidate
362
+ # Stop once we step outside the package: the manifest
363
+ # lives at the pack root, and the pack root is a package.
364
+ if not (directory / "__init__.py").is_file():
365
+ break
366
+ if directory.parent == directory:
367
+ break
368
+ directory = directory.parent
369
+ return None
370
+
371
+
372
+ def _warn_on_manifest_violations(pack: Pack) -> None:
373
+ """The warning tier itself. Never raises: a broken manifest (or a
374
+ bug in discovery) must not break ``load_pack`` before 2.0.
375
+ """
376
+ try:
377
+ manifest_path = _locate_pack_manifest(pack)
378
+ if manifest_path is None:
379
+ return # no manifest: silent, per the Q2 schedule
380
+ key = (pack.name, pack.version, str(manifest_path))
381
+ if key in _manifest_checked:
382
+ return
383
+ _manifest_checked.add(key)
384
+ from activegraph.packs.manifest import (
385
+ PackManifestError,
386
+ load_manifest,
387
+ verify_surface,
388
+ )
389
+
390
+ try:
391
+ manifest = load_manifest(manifest_path)
392
+ verify_surface(manifest, pack)
393
+ except PackManifestError as err:
394
+ _manifest_log.warning(
395
+ "pack %s@%s: manifest.toml found at %s but validation "
396
+ "failed with %d violation(s). The pack still loads — "
397
+ "this stays a warning until activegraph 2.0. First "
398
+ "violation: %s",
399
+ pack.name,
400
+ pack.version,
401
+ manifest_path,
402
+ len(err.violations),
403
+ err.violations[0] if err.violations else "",
404
+ extra={
405
+ "pack": pack.name,
406
+ "pack_version": pack.version,
407
+ "manifest_path": str(manifest_path),
408
+ "violations": list(err.violations),
409
+ "reason": "pack.manifest_invalid",
410
+ },
411
+ )
412
+ except Exception:
413
+ # The tier is advisory; discovery/IO surprises are debug noise,
414
+ # not load failures.
415
+ _manifest_log.debug(
416
+ "manifest warning tier errored for pack %s@%s",
417
+ pack.name,
418
+ pack.version,
419
+ exc_info=True,
420
+ )
421
+
422
+
315
423
  # ---------------------------------------------------------------- state
316
424
 
317
425
 
@@ -2550,6 +2550,11 @@ class Runtime:
2550
2550
  store = _open_sqlite_store(path, run_id=chosen)
2551
2551
  graph = Graph(ids=IDGen(), run_id=chosen, graph_store=graph_store)
2552
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])
2553
2558
  for ev in events:
2554
2559
  graph._replay_event(ev) # noqa: SLF001 — internal seam
2555
2560
  graph.ids.reseed_from_events(events)
@@ -2707,6 +2712,11 @@ class Runtime:
2707
2712
  fork_store = SQLiteEventStore(store.path, run_id=new_run_id)
2708
2713
  fork_graph = Graph(ids=IDGen(), run_id=new_run_id, graph_store=graph_store)
2709
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])
2710
2720
  for ev in events:
2711
2721
  fork_graph._replay_event(ev) # noqa: SLF001
2712
2722
  fork_graph.ids.reseed_from_events(events)
@@ -3416,6 +3426,13 @@ def _verify_replay(
3416
3426
  cache = LLMCache.from_events(recorded_events)
3417
3427
 
3418
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
+ )
3419
3436
  fresh_rt = Runtime(
3420
3437
  fresh,
3421
3438
  behaviors=behaviors,
@@ -3538,6 +3555,69 @@ def _is_lifecycle(e: Event) -> bool:
3538
3555
  )
3539
3556
 
3540
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
+
3541
3621
  def _rebuild_pending_approvals(rt: "Runtime", events: list[Event]) -> None:
3542
3622
  """Reconstruct the pending-approval queue from the event log. v1.4.
3543
3623
 
@@ -0,0 +1,257 @@
1
+ """Subprocess fork-trial isolation. CONTRACT v1.5 #1.
2
+
3
+ Implements ``trial-isolation-design.md``: candidate pack code trials
4
+ run in a **fresh interpreter**, against a fork of the parent run,
5
+ materialized from artifacts pinned by the bundle hash — so the parent
6
+ process is out of the blast radius of the T1 accident class (runaway
7
+ memory/CPU, corrupted in-process state) and the bytes trialed are the
8
+ bytes the proposal recorded.
9
+
10
+ The division of authority is the design's:
11
+
12
+ * **The parent forks.** The child receives only the fork's run id —
13
+ it never gets fork authority, and it appends only to that run.
14
+ * **The child is a fresh interpreter** (``sys.executable -m
15
+ activegraph.sandbox._child``), not ``os.fork()`` — no inherited
16
+ Python state, no shared clients. Environment is allow-list only.
17
+ * **Three independent nets**: ``resource.setrlimit`` (address
18
+ space + CPU, POSIX; degrade to the other two nets on Windows),
19
+ parent-side wall-clock kill, and the runtime's own event/LLM
20
+ budgets inside the child.
21
+ * **The store is the record.** The child's stdout tail is a signal;
22
+ the parent re-reads the fork run from the store and the store's
23
+ numbers win any disagreement.
24
+
25
+ HONEST LIMITS (the design's §5, restated where users will read it):
26
+ this is crash/state isolation, **not a security sandbox**. A fresh
27
+ interpreter with rlimits does not stop a malicious candidate from
28
+ opening sockets or reading the filesystem — syscall and network
29
+ confinement are host territory (containers, seccomp), and this module
30
+ does not imitate them. The child can also, in principle, open the
31
+ shared SQLite file directly and touch other runs; per-run
32
+ authorization inside one SQLite file is not something SQLite gives us
33
+ honestly, so that caveat is stated rather than solved. The evolution
34
+ pack's static gates remain the pre-execution filter.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import os
41
+ import subprocess
42
+ import sys
43
+ from dataclasses import dataclass, field
44
+ from pathlib import Path
45
+ from typing import Optional
46
+
47
+
48
+ #: The closed outcome set. ``crashed`` is the implementation's one
49
+ #: addition over the design draft: a child that dies without a
50
+ #: parseable tail (segfault, os._exit, kill) is reported as what it
51
+ #: is, never guessed into a neater bucket.
52
+ TRIAL_OUTCOMES = (
53
+ "completed",
54
+ "scenario_failed",
55
+ "limits_exceeded",
56
+ "materialization_failed",
57
+ "crashed",
58
+ )
59
+
60
+ # Child exit codes, mirrored in _child.py. Anything else → crashed.
61
+ _EXIT_TO_OUTCOME = {
62
+ 0: "completed",
63
+ 30: "scenario_failed",
64
+ 40: "limits_exceeded",
65
+ 50: "materialization_failed",
66
+ }
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class PackSource:
71
+ """Where the child materializes the candidate pack from.
72
+
73
+ ``expected_bundle_hash`` is the external pin (the §4 walk WITH
74
+ ``manifest.toml``, per the v1.4 bundle-hash amendment) — the child
75
+ verifies it via ``activegraph.packs.manifest.verify_bundle_hash``
76
+ before importing anything, so the bytes trialed are the bytes the
77
+ proposal recorded. ``manifest_required=True`` additionally runs
78
+ ``load_manifest`` + ``verify_surface`` against the live ``Pack``,
79
+ making the trial child the first consumer of the full manifest
80
+ chain end-to-end.
81
+ """
82
+
83
+ root_dir: str
84
+ expected_bundle_hash: str = ""
85
+ manifest_required: bool = True
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class TrialLimits:
90
+ """The trial's resource nets. Zero/None disables a given net.
91
+
92
+ ``max_llm_calls=0`` (the default) means key-freedom is
93
+ STRUCTURAL: the child configures no LLM provider at all, so a
94
+ candidate with LLM behaviors fails loud at registration
95
+ (``MissingProviderError``) rather than reaching a network. A
96
+ positive cap is accepted and recorded for a future
97
+ provider-wiring seam; the v1 child never configures a live
98
+ provider either way. ``env_passthrough`` is the ONLY parent environment
99
+ forwarded beyond interpreter mechanics (PATH/PYTHONPATH) — the
100
+ helper never forwards the environment wholesale, so parent API
101
+ keys don't leak into candidate code by default.
102
+ """
103
+
104
+ wall_clock_seconds: float = 120.0
105
+ max_rss_bytes: Optional[int] = None
106
+ max_events: Optional[int] = 2_000
107
+ max_llm_calls: Optional[int] = 0
108
+ env_passthrough: tuple[str, ...] = ()
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class TrialReport:
113
+ """What a trial produced. Store-derived numbers, child-signaled shape.
114
+
115
+ ``events_appended`` and ``behavior_failures`` are re-read from the
116
+ fork's run in the store by the parent AFTER the child exits — the
117
+ stdout tail is a signal only, and any disagreement resolves in
118
+ favor of the store. Richer evidence (tracebacks, diffs) is read
119
+ from the fork's log directly: ``Runtime.load(store, run_id=
120
+ report.fork_run_id)`` then ``trace.failures()`` / ``diff()``.
121
+ """
122
+
123
+ outcome: str
124
+ fork_run_id: str
125
+ events_appended: int
126
+ behavior_failures: int
127
+ detail: str
128
+ exit_code: Optional[int]
129
+
130
+
131
+ def run_forked_trial(
132
+ store_path: str,
133
+ *,
134
+ parent_run_id: str,
135
+ at_event: str,
136
+ pack_source: PackSource,
137
+ scenario: str = "",
138
+ limits: TrialLimits = TrialLimits(),
139
+ label: str = "trial",
140
+ ) -> TrialReport:
141
+ """Fork the parent at ``at_event`` and trial the candidate pack in
142
+ a fresh subprocess. CONTRACT v1.5 #1.
143
+
144
+ The fork is created HERE, in the parent process, with full
145
+ ``fork()`` semantics (lineage recorded, promote-block cut guard);
146
+ the child receives only the fork's run id. ``scenario`` is
147
+ ``"relative/path.py"`` or ``"relative/path.py::func"`` inside the
148
+ pack root — the function (default ``main``) is called with the
149
+ fork's ``Runtime`` and drives the trial; empty means just
150
+ ``run_until_idle()``. The scenario contract is
151
+ ``def main(rt): ...``.
152
+
153
+ Deterministic and key-free by default (``max_llm_calls=0``, empty
154
+ environment pass-through). Returns a :class:`TrialReport`; never
155
+ raises for in-trial failures — those are outcomes. Raises only
156
+ for parent-side setup problems (bad store, bad fork point), with
157
+ the same errors ``Runtime.load`` / ``fork()`` raise.
158
+ """
159
+ from activegraph.runtime.runtime import Runtime
160
+
161
+ parent_rt = Runtime.load(store_path, run_id=parent_run_id, behaviors=[])
162
+ fork_rt = parent_rt.fork(at_event=at_event, label=label, behaviors=[])
163
+ fork_run_id = fork_rt.run_id
164
+ initial_events = len(fork_rt.graph.events)
165
+ del fork_rt # the child owns the fork from here
166
+
167
+ job = {
168
+ "store_path": store_path,
169
+ "fork_run_id": fork_run_id,
170
+ "initial_events": initial_events,
171
+ "pack_root": str(Path(pack_source.root_dir).resolve()),
172
+ "expected_bundle_hash": pack_source.expected_bundle_hash,
173
+ "manifest_required": pack_source.manifest_required,
174
+ "scenario": scenario,
175
+ "limits": {
176
+ "max_rss_bytes": limits.max_rss_bytes,
177
+ "max_events": limits.max_events,
178
+ "max_llm_calls": limits.max_llm_calls,
179
+ "cpu_seconds": int(limits.wall_clock_seconds) + 5,
180
+ },
181
+ }
182
+ env = {
183
+ key: os.environ[key]
184
+ for key in ("PATH", "PYTHONPATH", "HOME", "LANG")
185
+ if key in os.environ
186
+ }
187
+ for key in limits.env_passthrough:
188
+ if key in os.environ:
189
+ env[key] = os.environ[key]
190
+
191
+ proc = subprocess.Popen(
192
+ [sys.executable, "-m", "activegraph.sandbox._child"],
193
+ stdin=subprocess.PIPE,
194
+ stdout=subprocess.PIPE,
195
+ stderr=subprocess.DEVNULL,
196
+ env=env,
197
+ text=True,
198
+ )
199
+ try:
200
+ stdout, _ = proc.communicate(
201
+ input=json.dumps(job), timeout=limits.wall_clock_seconds
202
+ )
203
+ exit_code: Optional[int] = proc.returncode
204
+ timed_out = False
205
+ except subprocess.TimeoutExpired:
206
+ proc.kill()
207
+ stdout, _ = proc.communicate()
208
+ exit_code = proc.returncode
209
+ timed_out = True
210
+
211
+ tail: dict[str, object] = {}
212
+ for line in reversed((stdout or "").strip().splitlines()):
213
+ try:
214
+ parsed = json.loads(line)
215
+ if isinstance(parsed, dict) and "outcome" in parsed:
216
+ tail = parsed
217
+ break
218
+ except json.JSONDecodeError:
219
+ continue
220
+
221
+ if timed_out:
222
+ outcome = "limits_exceeded"
223
+ detail = (
224
+ f"wall clock exceeded {limits.wall_clock_seconds}s; child killed"
225
+ )
226
+ elif tail:
227
+ outcome = str(tail.get("outcome", "crashed"))
228
+ if outcome not in TRIAL_OUTCOMES:
229
+ outcome = "crashed"
230
+ detail = str(tail.get("detail", ""))
231
+ else:
232
+ outcome = _EXIT_TO_OUTCOME.get(exit_code or -1, "crashed")
233
+ if outcome == "completed":
234
+ # Exit 0 with no tail is itself suspicious; say so.
235
+ outcome = "crashed"
236
+ detail = f"child exited {exit_code} with no report tail"
237
+
238
+ # The store is the record: re-read the fork run for the numbers.
239
+ events_appended = 0
240
+ behavior_failures = 0
241
+ try:
242
+ fork_view = Runtime.load(store_path, run_id=fork_run_id, behaviors=[])
243
+ events_appended = max(
244
+ 0, len(fork_view.graph.events) - initial_events
245
+ )
246
+ behavior_failures = len(fork_view.trace.failures())
247
+ except Exception as e: # noqa: BLE001 — report, never mask the trial
248
+ detail = f"{detail} (store re-read failed: {e})".strip()
249
+
250
+ return TrialReport(
251
+ outcome=outcome,
252
+ fork_run_id=fork_run_id,
253
+ events_appended=events_appended,
254
+ behavior_failures=behavior_failures,
255
+ detail=detail,
256
+ exit_code=exit_code,
257
+ )