activegraph 1.5.0__tar.gz → 1.7.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.5.0/activegraph.egg-info → activegraph-1.7.0}/PKG-INFO +1 -1
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/__init__.py +1 -1
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/loader.py +108 -0
- activegraph-1.7.0/activegraph/sandbox/__init__.py +440 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/sandbox/_child.py +24 -5
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/retention.py +55 -3
- {activegraph-1.5.0 → activegraph-1.7.0/activegraph.egg-info}/PKG-INFO +1 -1
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph.egg-info/SOURCES.txt +1 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/pyproject.toml +1 -1
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_compaction.py +35 -0
- activegraph-1.7.0/tests/test_manifest_warning_tier.py +171 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_promote.py +61 -0
- activegraph-1.7.0/tests/test_sandbox_trial.py +600 -0
- activegraph-1.5.0/activegraph/sandbox/__init__.py +0 -257
- activegraph-1.5.0/tests/test_sandbox_trial.py +0 -260
- {activegraph-1.5.0 → activegraph-1.7.0}/LICENSE +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/NOTICE +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/README.md +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/__main__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/_signature.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/behaviors/base.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/behaviors/decorators.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/cli/main.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/cli/renderers.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/clock.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/event.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/graph.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/graph_store.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/ids.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/patch.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/core/view.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/frame.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/anthropic.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/cache.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/embedding.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/native.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/openai.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/prompt.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/provider.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/recorded.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/types.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/llm/wire.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/logging.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/migration.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/otel.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/prometheus.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/observability/status.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/settings.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/manifest.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/policy.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/_live.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/diff.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/exec_errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/patterns.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/promote.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/registration_errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/runtime.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/base.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/conformance.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/falkordb.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/graph_conformance.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/memory.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/postgres.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/serde.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/sqlite.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/store/url.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/base.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/cache.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/context.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/decorators.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/errors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/graph_query.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/trace/causal.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph/trace/printer.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/setup.cfg +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_activate_after.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_cli.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_cli_docs_flags.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_clock.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_diff.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_disable_pack.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_doc_links.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_doc_python_snippets.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_embedding_provider.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_errors_format.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_event.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_falkordb_store.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_fork.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_graph.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_graph_store.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_ids.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_license.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_anthropic.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_behavior.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_budget.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_causal.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_default_model.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_failure.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_native_structured_output.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_openai.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_replay.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_trace.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_types.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llm_wire.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_llms_txt.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_migration.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_observability_logging.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_operate_example.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_pack_manifest.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_packs.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_patch.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_persistence.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_postgres_store.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_quickstart.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_reason_codes_docs.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_registration_validation.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_replay.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_requeue_unfired.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_resume_example.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_runtime.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_runtime_status.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_serde.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_store_conformance.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_store_url.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_tool_replay.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_tools.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_trace.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_trace_accessors.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_version_sync.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.0}/tests/test_view.py +0 -0
- {activegraph-1.5.0 → activegraph-1.7.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
|
|
|
@@ -0,0 +1,440 @@
|
|
|
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
|
+
TWO CHANNELS, KEPT SEPARATE (v1.7, the Replit soak fix):
|
|
26
|
+
|
|
27
|
+
* **The environment allow-list stays CLOSED.** Only ``PATH``,
|
|
28
|
+
``HOME``, ``LANG`` plus an explicit ``env_passthrough`` cross the
|
|
29
|
+
boundary. No ambient parent env — secrets, API keys, platform
|
|
30
|
+
config like ``REPLIT_*`` — leaks into a child running hostile
|
|
31
|
+
candidate code. This is a security control and does not widen.
|
|
32
|
+
* **Code location is an EXPLICIT channel, not an ambient one.** A
|
|
33
|
+
child must be able to import ``activegraph`` (and its deps) on any
|
|
34
|
+
box where the *parent* could — but on some platforms (Replit,
|
|
35
|
+
Nix) the package is discoverable only via a platform env var the
|
|
36
|
+
allow-list correctly strips, so a naive child dies at import
|
|
37
|
+
before running. The parent therefore RESOLVES the ``sys.path``
|
|
38
|
+
entries that make its own code importable and passes them to the
|
|
39
|
+
child as a computed ``PYTHONPATH`` (see :func:`_child_env`).
|
|
40
|
+
``PYTHONPATH`` is thus derived from the parent's resolved
|
|
41
|
+
``sys.path``, never forwarded from ambient env — code locations
|
|
42
|
+
are not secrets, and making the child's discovery of its own code
|
|
43
|
+
an explicit input keeps the allow-list closed while fixing the
|
|
44
|
+
restricted-env break. Call :func:`preflight` once at boot to
|
|
45
|
+
verify a child can start under this env and fail loud if not.
|
|
46
|
+
|
|
47
|
+
HONEST LIMITS (the design's §5, restated where users will read it):
|
|
48
|
+
this is crash/state isolation, **not a security sandbox**. A fresh
|
|
49
|
+
interpreter with rlimits does not stop a malicious candidate from
|
|
50
|
+
opening sockets or reading the filesystem — syscall and network
|
|
51
|
+
confinement are host territory (containers, seccomp), and this module
|
|
52
|
+
does not imitate them. The child can also, in principle, open the
|
|
53
|
+
shared SQLite file directly and touch other runs; per-run
|
|
54
|
+
authorization inside one SQLite file is not something SQLite gives us
|
|
55
|
+
honestly, so that caveat is stated rather than solved. The evolution
|
|
56
|
+
pack's static gates remain the pre-execution filter.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import json
|
|
62
|
+
import os
|
|
63
|
+
import subprocess
|
|
64
|
+
import sys
|
|
65
|
+
from dataclasses import dataclass, field
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import Optional
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#: The closed outcome set. ``crashed`` is the implementation's one
|
|
71
|
+
#: addition over the design draft: a child that dies without a
|
|
72
|
+
#: parseable tail (segfault, os._exit, kill) is reported as what it
|
|
73
|
+
#: is, never guessed into a neater bucket.
|
|
74
|
+
TRIAL_OUTCOMES = (
|
|
75
|
+
"completed",
|
|
76
|
+
"scenario_failed",
|
|
77
|
+
"limits_exceeded",
|
|
78
|
+
"materialization_failed",
|
|
79
|
+
"crashed",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Child exit codes, mirrored in _child.py. Anything else → crashed.
|
|
83
|
+
_EXIT_TO_OUTCOME = {
|
|
84
|
+
0: "completed",
|
|
85
|
+
30: "scenario_failed",
|
|
86
|
+
40: "limits_exceeded",
|
|
87
|
+
50: "materialization_failed",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class PackSource:
|
|
93
|
+
"""Where the child materializes the candidate pack from.
|
|
94
|
+
|
|
95
|
+
``expected_bundle_hash`` is the external pin (the §4 walk WITH
|
|
96
|
+
``manifest.toml``, per the v1.4 bundle-hash amendment) — the child
|
|
97
|
+
verifies it via ``activegraph.packs.manifest.verify_bundle_hash``
|
|
98
|
+
before importing anything, so the bytes trialed are the bytes the
|
|
99
|
+
proposal recorded. ``manifest_required=True`` additionally runs
|
|
100
|
+
``load_manifest`` + ``verify_surface`` against the live ``Pack``,
|
|
101
|
+
making the trial child the first consumer of the full manifest
|
|
102
|
+
chain end-to-end.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
root_dir: str
|
|
106
|
+
expected_bundle_hash: str = ""
|
|
107
|
+
manifest_required: bool = True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class TrialLimits:
|
|
112
|
+
"""The trial's resource nets. Zero/None disables a given net.
|
|
113
|
+
|
|
114
|
+
``max_llm_calls=0`` (the default) means key-freedom is
|
|
115
|
+
STRUCTURAL: the child configures no LLM provider at all, so a
|
|
116
|
+
candidate with LLM behaviors fails loud at registration
|
|
117
|
+
(``MissingProviderError``) rather than reaching a network. A
|
|
118
|
+
positive cap is accepted and recorded for a future
|
|
119
|
+
provider-wiring seam; the v1 child never configures a live
|
|
120
|
+
provider either way. ``env_passthrough`` is the ONLY ambient parent
|
|
121
|
+
environment forwarded beyond the closed allow-list
|
|
122
|
+
(``PATH``/``HOME``/``LANG``) — the helper never forwards the
|
|
123
|
+
environment wholesale, so parent API keys don't leak into
|
|
124
|
+
candidate code by default. The child's ``PYTHONPATH`` is NOT an
|
|
125
|
+
ambient forward: it is computed from the parent's resolved
|
|
126
|
+
``sys.path`` so the child can import its own code (see the module
|
|
127
|
+
docstring's "two channels").
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
wall_clock_seconds: float = 120.0
|
|
131
|
+
max_rss_bytes: Optional[int] = None
|
|
132
|
+
max_events: Optional[int] = 2_000
|
|
133
|
+
max_llm_calls: Optional[int] = 0
|
|
134
|
+
env_passthrough: tuple[str, ...] = ()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class TrialReport:
|
|
139
|
+
"""What a trial produced. Store-derived numbers, child-signaled shape.
|
|
140
|
+
|
|
141
|
+
``events_appended`` and ``behavior_failures`` are re-read from the
|
|
142
|
+
fork's run in the store by the parent AFTER the child exits — the
|
|
143
|
+
stdout tail is a signal only, and any disagreement resolves in
|
|
144
|
+
favor of the store. Richer evidence (tracebacks, diffs) is read
|
|
145
|
+
from the fork's log directly: ``Runtime.load(store, run_id=
|
|
146
|
+
report.fork_run_id)`` then ``trace.failures()`` / ``diff()``.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
outcome: str
|
|
150
|
+
fork_run_id: str
|
|
151
|
+
events_appended: int
|
|
152
|
+
behavior_failures: int
|
|
153
|
+
detail: str
|
|
154
|
+
exit_code: Optional[int]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SandboxStartupError(RuntimeError):
|
|
158
|
+
"""A trial child could not START under the sandbox env.
|
|
159
|
+
|
|
160
|
+
Raised by :func:`preflight` when the child fails before it can run
|
|
161
|
+
any trial — the common cause is the sandbox env being unable to
|
|
162
|
+
import ``activegraph`` on this box (the exact restricted-env break
|
|
163
|
+
the explicit package-path channel exists to prevent). The message
|
|
164
|
+
carries the child's stderr tail so the cause is never opaque.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _child_code_paths() -> list[str]:
|
|
169
|
+
"""The ``sys.path`` entries a child needs to import the parent's
|
|
170
|
+
code, resolved in the parent (v1.7 explicit package-path channel).
|
|
171
|
+
|
|
172
|
+
``activegraph``'s own root comes first, resolved from
|
|
173
|
+
``activegraph.__file__`` — this is what an editable install or a
|
|
174
|
+
platform that surfaces packages via a stripped env var (Replit's
|
|
175
|
+
``REPLIT_PYTHONPATH``) would otherwise hide from the child. The
|
|
176
|
+
parent's real ``sys.path`` directories follow so third-party deps
|
|
177
|
+
resolve too. Non-directory entries (namespace-finder hooks, the
|
|
178
|
+
empty CWD entry, zip imports) are dropped: they either don't help
|
|
179
|
+
a fresh child or would import from its CWD, which we do not want.
|
|
180
|
+
"""
|
|
181
|
+
import activegraph
|
|
182
|
+
|
|
183
|
+
ordered: list[str] = []
|
|
184
|
+
ag_root = str(Path(activegraph.__file__).resolve().parent.parent)
|
|
185
|
+
ordered.append(ag_root)
|
|
186
|
+
for entry in sys.path:
|
|
187
|
+
if entry and os.path.isdir(entry) and entry not in ordered:
|
|
188
|
+
ordered.append(entry)
|
|
189
|
+
return ordered
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _child_env(limits: TrialLimits) -> dict[str, str]:
|
|
193
|
+
"""Build the child's environment: closed allow-list + the computed
|
|
194
|
+
``PYTHONPATH`` code channel. See the module docstring's "two
|
|
195
|
+
channels".
|
|
196
|
+
|
|
197
|
+
The allow-list is ``PATH``/``HOME``/``LANG`` plus the caller's
|
|
198
|
+
explicit ``env_passthrough`` — no other ambient env crosses. Then
|
|
199
|
+
``PYTHONPATH`` is set from :func:`_child_code_paths` (the parent's
|
|
200
|
+
resolved code locations), prepended before any ``PYTHONPATH`` the
|
|
201
|
+
caller opted to pass through, so the child imports the same code
|
|
202
|
+
the parent can regardless of platform package discovery.
|
|
203
|
+
"""
|
|
204
|
+
env = {
|
|
205
|
+
key: os.environ[key]
|
|
206
|
+
for key in ("PATH", "HOME", "LANG")
|
|
207
|
+
if key in os.environ
|
|
208
|
+
}
|
|
209
|
+
for key in limits.env_passthrough:
|
|
210
|
+
if key in os.environ:
|
|
211
|
+
env[key] = os.environ[key]
|
|
212
|
+
channel = os.pathsep.join(_child_code_paths())
|
|
213
|
+
passed_through = env.get("PYTHONPATH", "")
|
|
214
|
+
env["PYTHONPATH"] = (
|
|
215
|
+
channel + os.pathsep + passed_through if passed_through else channel
|
|
216
|
+
)
|
|
217
|
+
return env
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _stderr_tail(stderr: str, *, max_lines: int = 20, max_chars: int = 1500) -> str:
|
|
221
|
+
"""The last few lines of a child's stderr, capped — the actual
|
|
222
|
+
cause of a pre-``_report`` crash the DEVNULL default used to lose.
|
|
223
|
+
"""
|
|
224
|
+
text = (stderr or "").strip()
|
|
225
|
+
if not text:
|
|
226
|
+
return ""
|
|
227
|
+
lines = text.splitlines()[-max_lines:]
|
|
228
|
+
tail = "\n".join(lines)
|
|
229
|
+
return tail[-max_chars:]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_report_tail(stdout: str) -> dict[str, object]:
|
|
233
|
+
"""The child's last valid JSON report line, or ``{}``."""
|
|
234
|
+
for line in reversed((stdout or "").strip().splitlines()):
|
|
235
|
+
try:
|
|
236
|
+
parsed = json.loads(line)
|
|
237
|
+
except json.JSONDecodeError:
|
|
238
|
+
continue
|
|
239
|
+
if isinstance(parsed, dict) and (
|
|
240
|
+
"outcome" in parsed or "preflight" in parsed
|
|
241
|
+
):
|
|
242
|
+
return parsed
|
|
243
|
+
return {}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _run_child(
|
|
247
|
+
job: dict[str, object],
|
|
248
|
+
*,
|
|
249
|
+
env: dict[str, str],
|
|
250
|
+
wall_clock: float,
|
|
251
|
+
python_flags: tuple[str, ...] = (),
|
|
252
|
+
) -> tuple[Optional[int], str, str, bool]:
|
|
253
|
+
"""Spawn the trial child, feed it ``job`` on stdin, and return
|
|
254
|
+
``(exit_code, stdout, stderr, timed_out)``.
|
|
255
|
+
|
|
256
|
+
Shared by :func:`run_forked_trial` and :func:`preflight`. stderr is
|
|
257
|
+
PIPED (never DEVNULL) so a pre-``_report`` crash's cause survives.
|
|
258
|
+
``python_flags`` is an interpreter-flag seam used by tests to
|
|
259
|
+
reproduce a restricted env (``-S``); production passes none.
|
|
260
|
+
"""
|
|
261
|
+
proc = subprocess.Popen(
|
|
262
|
+
[sys.executable, *python_flags, "-m", "activegraph.sandbox._child"],
|
|
263
|
+
stdin=subprocess.PIPE,
|
|
264
|
+
stdout=subprocess.PIPE,
|
|
265
|
+
stderr=subprocess.PIPE,
|
|
266
|
+
env=env,
|
|
267
|
+
text=True,
|
|
268
|
+
)
|
|
269
|
+
try:
|
|
270
|
+
stdout, stderr = proc.communicate(
|
|
271
|
+
input=json.dumps(job), timeout=wall_clock
|
|
272
|
+
)
|
|
273
|
+
return proc.returncode, stdout, stderr, False
|
|
274
|
+
except subprocess.TimeoutExpired:
|
|
275
|
+
proc.kill()
|
|
276
|
+
stdout, stderr = proc.communicate()
|
|
277
|
+
return proc.returncode, stdout, stderr, True
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _preflight_with(
|
|
281
|
+
env: dict[str, str],
|
|
282
|
+
*,
|
|
283
|
+
python_flags: tuple[str, ...] = (),
|
|
284
|
+
timeout: float = 30.0,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""The preflight check against a specific ``env`` (the testable
|
|
287
|
+
seam under :func:`preflight`). Raises :class:`SandboxStartupError`
|
|
288
|
+
if a child cannot start and report ``preflight: ok``.
|
|
289
|
+
"""
|
|
290
|
+
exit_code, stdout, stderr, timed_out = _run_child(
|
|
291
|
+
{"preflight": True}, env=env, wall_clock=timeout, python_flags=python_flags
|
|
292
|
+
)
|
|
293
|
+
tail = _parse_report_tail(stdout)
|
|
294
|
+
if not timed_out and exit_code == 0 and tail.get("preflight") == "ok":
|
|
295
|
+
return
|
|
296
|
+
cause = _stderr_tail(stderr) or (
|
|
297
|
+
"child timed out" if timed_out else "no preflight tail on stdout"
|
|
298
|
+
)
|
|
299
|
+
raise SandboxStartupError(
|
|
300
|
+
f"trial child could not start (exit {exit_code}): {cause}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def preflight(*, limits: TrialLimits = TrialLimits(), timeout: float = 30.0) -> None:
|
|
305
|
+
"""Verify a trial child can START under the sandbox env on this box.
|
|
306
|
+
|
|
307
|
+
Spawns the child with a null job (no fork, no store, no candidate):
|
|
308
|
+
it imports ``activegraph``, echoes ``preflight: ok``, and exits 0.
|
|
309
|
+
Returns ``None`` on success; raises :class:`SandboxStartupError`
|
|
310
|
+
with the child's stderr tail otherwise. Consumers call this once at
|
|
311
|
+
boot so a broken sandbox env (e.g. a platform whose package
|
|
312
|
+
discovery the allow-list strips) fails LOUD here instead of showing
|
|
313
|
+
up as an opaque ``crashed`` on the first real trial.
|
|
314
|
+
"""
|
|
315
|
+
_preflight_with(_child_env(limits), timeout=timeout)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def run_forked_trial(
|
|
319
|
+
store_path: str,
|
|
320
|
+
*,
|
|
321
|
+
parent_run_id: str,
|
|
322
|
+
at_event: str,
|
|
323
|
+
pack_source: PackSource,
|
|
324
|
+
scenario: str = "",
|
|
325
|
+
limits: TrialLimits = TrialLimits(),
|
|
326
|
+
label: str = "trial",
|
|
327
|
+
extra_packs: tuple[PackSource, ...] = (),
|
|
328
|
+
) -> TrialReport:
|
|
329
|
+
"""Fork the parent at ``at_event`` and trial the candidate pack in
|
|
330
|
+
a fresh subprocess. CONTRACT v1.5 #1.
|
|
331
|
+
|
|
332
|
+
The fork is created HERE, in the parent process, with full
|
|
333
|
+
``fork()`` semantics (lineage recorded, promote-block cut guard);
|
|
334
|
+
the child receives only the fork's run id. ``scenario`` is
|
|
335
|
+
``"relative/path.py"`` or ``"relative/path.py::func"`` inside the
|
|
336
|
+
CANDIDATE's pack root — the function (default ``main``) is called
|
|
337
|
+
with the fork's ``Runtime`` and drives the trial; empty means
|
|
338
|
+
just ``run_until_idle()``. The scenario contract is
|
|
339
|
+
``def main(rt): ...``.
|
|
340
|
+
|
|
341
|
+
**The default is candidate-only isolation**: the child loads
|
|
342
|
+
nothing but the candidate, so the trial exercises it apart from
|
|
343
|
+
every other pack's behaviors. ``extra_packs`` is the opt-in for
|
|
344
|
+
cross-pack interaction trials (CONTRACT v1.5 #1 addendum 1b):
|
|
345
|
+
each entry is materialized in the child exactly like the
|
|
346
|
+
candidate — bundle hash verified before import, manifest schema
|
|
347
|
+
+ two-way surface check when required — and loaded, in order,
|
|
348
|
+
BEFORE the candidate. Any extra pack failing its pins is
|
|
349
|
+
``materialization_failed`` for the whole trial; the scenario
|
|
350
|
+
still resolves inside the candidate's root only.
|
|
351
|
+
|
|
352
|
+
Deterministic and key-free by default (``max_llm_calls=0``, empty
|
|
353
|
+
environment pass-through). Returns a :class:`TrialReport`; never
|
|
354
|
+
raises for in-trial failures — those are outcomes. Raises only
|
|
355
|
+
for parent-side setup problems (bad store, bad fork point), with
|
|
356
|
+
the same errors ``Runtime.load`` / ``fork()`` raise.
|
|
357
|
+
"""
|
|
358
|
+
from activegraph.runtime.runtime import Runtime
|
|
359
|
+
|
|
360
|
+
parent_rt = Runtime.load(store_path, run_id=parent_run_id, behaviors=[])
|
|
361
|
+
fork_rt = parent_rt.fork(at_event=at_event, label=label, behaviors=[])
|
|
362
|
+
fork_run_id = fork_rt.run_id
|
|
363
|
+
initial_events = len(fork_rt.graph.events)
|
|
364
|
+
del fork_rt # the child owns the fork from here
|
|
365
|
+
|
|
366
|
+
job = {
|
|
367
|
+
"store_path": store_path,
|
|
368
|
+
"fork_run_id": fork_run_id,
|
|
369
|
+
"initial_events": initial_events,
|
|
370
|
+
"pack_root": str(Path(pack_source.root_dir).resolve()),
|
|
371
|
+
"expected_bundle_hash": pack_source.expected_bundle_hash,
|
|
372
|
+
"manifest_required": pack_source.manifest_required,
|
|
373
|
+
"extra_packs": [
|
|
374
|
+
{
|
|
375
|
+
"pack_root": str(Path(p.root_dir).resolve()),
|
|
376
|
+
"expected_bundle_hash": p.expected_bundle_hash,
|
|
377
|
+
"manifest_required": p.manifest_required,
|
|
378
|
+
}
|
|
379
|
+
for p in extra_packs
|
|
380
|
+
],
|
|
381
|
+
"scenario": scenario,
|
|
382
|
+
"limits": {
|
|
383
|
+
"max_rss_bytes": limits.max_rss_bytes,
|
|
384
|
+
"max_events": limits.max_events,
|
|
385
|
+
"max_llm_calls": limits.max_llm_calls,
|
|
386
|
+
"cpu_seconds": int(limits.wall_clock_seconds) + 5,
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
exit_code, stdout, stderr, timed_out = _run_child(
|
|
390
|
+
job, env=_child_env(limits), wall_clock=limits.wall_clock_seconds
|
|
391
|
+
)
|
|
392
|
+
tail = _parse_report_tail(stdout)
|
|
393
|
+
|
|
394
|
+
if timed_out:
|
|
395
|
+
outcome = "limits_exceeded"
|
|
396
|
+
detail = (
|
|
397
|
+
f"wall clock exceeded {limits.wall_clock_seconds}s; child killed"
|
|
398
|
+
)
|
|
399
|
+
elif tail:
|
|
400
|
+
outcome = str(tail.get("outcome", "crashed"))
|
|
401
|
+
if outcome not in TRIAL_OUTCOMES:
|
|
402
|
+
outcome = "crashed"
|
|
403
|
+
detail = str(tail.get("detail", ""))
|
|
404
|
+
else:
|
|
405
|
+
outcome = _EXIT_TO_OUTCOME.get(exit_code or -1, "crashed")
|
|
406
|
+
if outcome == "completed":
|
|
407
|
+
# Exit 0 with no tail is itself suspicious; say so.
|
|
408
|
+
outcome = "crashed"
|
|
409
|
+
detail = f"child exited {exit_code} with no report tail"
|
|
410
|
+
|
|
411
|
+
# The pre-execution gate must never swallow its own failure cause:
|
|
412
|
+
# when the child crashed before it could _report (module-import
|
|
413
|
+
# failure, rlimit setup, anything pre-main), the actual exception
|
|
414
|
+
# is on the child's stderr, not in any tail. Fold it into detail so
|
|
415
|
+
# it reaches the caller's recorded gate_result (v1.7 fix 1).
|
|
416
|
+
if outcome == "crashed":
|
|
417
|
+
err_tail = _stderr_tail(stderr)
|
|
418
|
+
if err_tail:
|
|
419
|
+
detail = f"{detail}; child stderr:\n{err_tail}"
|
|
420
|
+
|
|
421
|
+
# The store is the record: re-read the fork run for the numbers.
|
|
422
|
+
events_appended = 0
|
|
423
|
+
behavior_failures = 0
|
|
424
|
+
try:
|
|
425
|
+
fork_view = Runtime.load(store_path, run_id=fork_run_id, behaviors=[])
|
|
426
|
+
events_appended = max(
|
|
427
|
+
0, len(fork_view.graph.events) - initial_events
|
|
428
|
+
)
|
|
429
|
+
behavior_failures = len(fork_view.trace.failures())
|
|
430
|
+
except Exception as e: # noqa: BLE001 — report, never mask the trial
|
|
431
|
+
detail = f"{detail} (store re-read failed: {e})".strip()
|
|
432
|
+
|
|
433
|
+
return TrialReport(
|
|
434
|
+
outcome=outcome,
|
|
435
|
+
fork_run_id=fork_run_id,
|
|
436
|
+
events_appended=events_appended,
|
|
437
|
+
behavior_failures=behavior_failures,
|
|
438
|
+
detail=detail,
|
|
439
|
+
exit_code=exit_code,
|
|
440
|
+
)
|
|
@@ -62,11 +62,15 @@ def _apply_rlimits(limits: dict[str, Any]) -> None:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def _materialize_pack(job: dict[str, Any]) -> Any:
|
|
65
|
-
"""Verify pins, then import
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
"""Verify pins, then import one pack source and return its Pack.
|
|
66
|
+
|
|
67
|
+
``job`` carries ``pack_root`` / ``expected_bundle_hash`` /
|
|
68
|
+
``manifest_required`` — the top-level job spec for the candidate,
|
|
69
|
+
or one ``extra_packs`` entry (v1.7 addendum 1b), which uses the
|
|
70
|
+
identical chain. Order is the design's §3: bundle hash BEFORE any
|
|
71
|
+
import (the pin covers manifest.toml per the v1.4 amendment),
|
|
72
|
+
then manifest schema, then import, then the two-way surface check
|
|
73
|
+
against the live Pack.
|
|
70
74
|
"""
|
|
71
75
|
from activegraph.packs import Pack
|
|
72
76
|
from activegraph.packs.manifest import (
|
|
@@ -133,6 +137,16 @@ def _resolve_scenario(
|
|
|
133
137
|
|
|
134
138
|
def main() -> None:
|
|
135
139
|
job = json.loads(sys.stdin.read())
|
|
140
|
+
|
|
141
|
+
# Preflight (v1.7): a null job that only proves the child could
|
|
142
|
+
# START — i.e. that importing activegraph under the sandbox env
|
|
143
|
+
# succeeded, which happens before this function even runs. Reaching
|
|
144
|
+
# here at all means the heavy import chain worked; echo the marker
|
|
145
|
+
# and exit clean. No fork, no store, no candidate touched.
|
|
146
|
+
if job.get("preflight"):
|
|
147
|
+
print(json.dumps({"preflight": "ok"}), flush=True)
|
|
148
|
+
raise SystemExit(0)
|
|
149
|
+
|
|
136
150
|
fork_run_id = str(job["fork_run_id"])
|
|
137
151
|
initial = int(job.get("initial_events", 0))
|
|
138
152
|
_apply_rlimits(job.get("limits", {}))
|
|
@@ -144,6 +158,9 @@ def main() -> None:
|
|
|
144
158
|
)
|
|
145
159
|
|
|
146
160
|
try:
|
|
161
|
+
# Extra packs first (addendum 1b): each pinned and verified
|
|
162
|
+
# exactly like the candidate, loaded before it below.
|
|
163
|
+
extra = [_materialize_pack(p) for p in job.get("extra_packs", [])]
|
|
147
164
|
pack = _materialize_pack(job)
|
|
148
165
|
except Exception as e: # noqa: BLE001 — outcome, not crash
|
|
149
166
|
_report(
|
|
@@ -174,6 +191,8 @@ def main() -> None:
|
|
|
174
191
|
behaviors=[],
|
|
175
192
|
budget=budget or None,
|
|
176
193
|
)
|
|
194
|
+
for trusted in extra:
|
|
195
|
+
rt.load_pack(trusted)
|
|
177
196
|
rt.load_pack(pack)
|
|
178
197
|
|
|
179
198
|
try:
|