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