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.
- {activegraph-1.3.0/activegraph.egg-info → activegraph-1.5.0}/PKG-INFO +1 -1
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/__init__.py +1 -1
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/__init__.py +37 -1
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/loader.py +21 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/manifest.py +104 -14
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/runtime.py +211 -0
- activegraph-1.5.0/activegraph/sandbox/__init__.py +257 -0
- activegraph-1.5.0/activegraph/sandbox/_child.py +234 -0
- activegraph-1.5.0/activegraph/store/retention.py +316 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/sqlite.py +126 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/printer.py +12 -0
- {activegraph-1.3.0 → activegraph-1.5.0/activegraph.egg-info}/PKG-INFO +1 -1
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/SOURCES.txt +6 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/pyproject.toml +1 -1
- activegraph-1.5.0/tests/test_compaction.py +249 -0
- activegraph-1.5.0/tests/test_disable_pack.py +155 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pack_manifest.py +162 -1
- activegraph-1.5.0/tests/test_sandbox_trial.py +260 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/LICENSE +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/NOTICE +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/README.md +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/__main__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/_signature.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/base.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/behaviors/decorators.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/main.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/cli/renderers.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/clock.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/event.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/graph.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/graph_store.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/ids.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/patch.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/core/view.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/frame.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/anthropic.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/cache.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/embedding.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/native.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/openai.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/prompt.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/provider.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/recorded.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/types.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/llm/wire.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/logging.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/migration.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/otel.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/prometheus.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/observability/status.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/settings.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/policy.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/_live.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/diff.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/exec_errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/patterns.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/promote.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/registration_errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/base.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/conformance.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/falkordb.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/graph_conformance.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/memory.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/postgres.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/serde.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/store/url.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/base.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/cache.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/context.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/decorators.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/errors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/graph_query.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph/trace/causal.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/setup.cfg +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_activate_after.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_cli.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_cli_docs_flags.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_clock.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diff.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_links.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_python_snippets.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_embedding_provider.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_errors_format.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_event.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_falkordb_store.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_fork.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_graph.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_graph_store.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_ids.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_license.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_anthropic.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_behavior.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_budget.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_causal.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_default_model.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_failure.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_native_structured_output.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_openai.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_replay.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_trace.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_types.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llm_wire.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_llms_txt.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_migration.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_observability_logging.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_operate_example.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_packs.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_patch.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_persistence.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_postgres_store.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_promote.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_quickstart.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_reason_codes_docs.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_registration_validation.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_replay.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_requeue_unfired.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_resume_example.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_runtime.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_runtime_status.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_serde.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_store_conformance.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_store_url.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tool_replay.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tools.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_trace.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_trace_accessors.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_version_sync.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_view.py +0 -0
- {activegraph-1.3.0 → activegraph-1.5.0}/tests/test_wheel_completeness.py +0 -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
|
|
412
|
-
"""The
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
``
|
|
420
|
-
|
|
421
|
-
2. Symlinks — file OR directory — are rejected loudly (
|
|
422
|
-
|
|
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
|
|