From 147e095df087d7f15a5733b63d3743bf34d77991 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:49:22 -0800 Subject: [PATCH 1/7] Add persistent Fabric controller backend --- src/ravel/fabric_persistent.py | 706 +++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) create mode 100644 src/ravel/fabric_persistent.py diff --git a/src/ravel/fabric_persistent.py b/src/ravel/fabric_persistent.py new file mode 100644 index 0000000..bb26c42 --- /dev/null +++ b/src/ravel/fabric_persistent.py @@ -0,0 +1,706 @@ +"""Persistent-controller RAVEL integration for MNCS Fabric. + +This adapter is intentionally a Fabric *consumer*. It never loads worker +endpoints, TLS keys, TrustStore state, registry files, or bundle-cache paths. +Those remain owned by the persistent Fabric controller. + +The module coexists with :mod:`ravel.fabric`'s historical local/network +reference backends so old evidence remains reproducible while live experiments +can use Fabric's current public consumer boundary. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import json +import os +from pathlib import Path +import shutil +import stat +import tomllib +from typing import Any, Mapping + +from .fabric import ( + DEVELOPMENT_AUTHORITY, + FabricError, + FabricExecutionObservation, + FabricQuestion, + FabricReferenceResult, + FabricUnavailableError, + FabricWorkload, + MAX_OUTPUT_BYTES, + MAX_REPLICAS, + _aggregate, + _identity, + _task_source, + _write_bundle_source_manifest, +) +from .mncs_bundles import BundleResult, build_execution_bundle +from .siblings import ensure_sibling_src + + +PERSISTENT_CONFIG_SCHEMA = "ravel-fabric-persistent-config/0.1" +PERSISTENT_SUBMISSION_SCHEMA = "ravel-fabric-persistent-submission/0.1" + + +@dataclass(frozen=True, slots=True) +class FabricPersistentConfig: + """Consumer-only connection data for the controller-owned service socket.""" + + socket_path: Path + client_identity: str = "ravel" + timeout: float = 5.0 + + def __post_init__(self) -> None: + if not str(self.socket_path): + raise FabricError("persistent Fabric socket_path is required") + if not self.client_identity or len(self.client_identity) > 128 or "\x00" in self.client_identity: + raise FabricError("persistent Fabric client_identity must be bounded text") + if self.timeout <= 0 or self.timeout > 300: + raise FabricError("persistent Fabric timeout must be within (0, 300] seconds") + + @classmethod + def load(cls, path: str | Path) -> "FabricPersistentConfig": + """Load a deliberately narrow config that cannot smuggle worker trust state.""" + + candidate = Path(path) + try: + value = tomllib.loads(candidate.read_text(encoding="utf-8")) + except (OSError, tomllib.TOMLDecodeError) as error: + raise FabricUnavailableError( + f"persistent Fabric config unavailable: {type(error).__name__}" + ) from error + + if set(value) != {"fabric"} or not isinstance(value.get("fabric"), dict): + raise FabricError("persistent Fabric config must contain only a [fabric] table") + fabric = value["fabric"] + allowed = {"mode", "socket_path", "client_identity", "timeout"} + unknown = set(fabric) - allowed + if unknown: + raise FabricError( + "persistent Fabric config contains controller-owned fields: " + + ", ".join(sorted(unknown)) + ) + if fabric.get("mode") != "persistent-controller": + raise FabricError("persistent Fabric mode must be persistent-controller") + socket_path = fabric.get("socket_path") + if not isinstance(socket_path, str) or not socket_path: + raise FabricError("persistent Fabric socket_path must be non-empty text") + client_identity = fabric.get("client_identity", "ravel") + timeout = fabric.get("timeout", 5.0) + if isinstance(timeout, bool) or not isinstance(timeout, (int, float)): + raise FabricError("persistent Fabric timeout must be numeric") + return cls( + socket_path=Path(socket_path).expanduser(), + client_identity=str(client_identity), + timeout=float(timeout), + ) + + def to_dict(self) -> dict[str, Any]: + return { + "schema": PERSISTENT_CONFIG_SCHEMA, + "mode": "persistent-controller", + "socket_path": str(self.socket_path), + "client_identity": self.client_identity, + "timeout": self.timeout, + "authority": "consumer-only", + } + + +@dataclass(frozen=True, slots=True) +class FabricPersistentSubmission: + """Detached Fabric work retained by RAVEL as provenance, not authority.""" + + workload: FabricWorkload + work_id: str + accepted_state: str + provider_identity: str + plan: Mapping[str, Any] + manifest: Mapping[str, Any] + bundle_identity: str + bundle_archive_identity: str | None + archive_path: Path + request_identity: str + accepted: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.work_id or not self.accepted_state: + raise FabricError("persistent submission requires Fabric work identity and state") + if not self.request_identity.startswith("sha256:"): + raise FabricError("persistent submission request identity is invalid") + + def to_dict(self) -> dict[str, Any]: + return { + "schema": PERSISTENT_SUBMISSION_SCHEMA, + "workload": self.workload.to_dict(), + "work_id": self.work_id, + "accepted_state": self.accepted_state, + "provider_identity": self.provider_identity, + "plan": dict(self.plan), + "manifest": dict(self.manifest), + "bundle_identity": self.bundle_identity, + "bundle_archive_identity": self.bundle_archive_identity, + "archive_path": str(self.archive_path), + "request_identity": self.request_identity, + "accepted": dict(self.accepted), + "authority": DEVELOPMENT_AUTHORITY, + "semantics": "detached Fabric work reference; not evaluator authority", + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "FabricPersistentSubmission": + if value.get("schema") != PERSISTENT_SUBMISSION_SCHEMA: + raise FabricError("unsupported persistent submission schema") + workload_value = value.get("workload") + if not isinstance(workload_value, Mapping): + raise FabricError("persistent submission workload is missing") + workload = FabricWorkload( + candidate_identity=str(workload_value["candidate_identity"]), + experiment_identity=str(workload_value["experiment_identity"]), + question_kind=str(workload_value["question_kind"]), + bundle_identity=str(workload_value["bundle_identity"]), + fabric_manifest_identity=str(workload_value["fabric_manifest_identity"]), + required_capabilities=tuple(workload_value.get("required_capabilities", ("python",))), + resource_budget=dict(workload_value.get("resource_budget", {})), + replication_count=int(workload_value.get("replication_count", 1)), + provider_identity=workload_value.get("provider_identity"), + expected_output_kind=str(workload_value.get("expected_output_kind", "diagnostic-observation")), + partition_identity=str(workload_value.get("partition_identity")), + forge_workflow_identity=str(workload_value.get("forge_workflow_identity")), + visibility=str(workload_value.get("visibility")), + authority=str(workload_value.get("authority")), + ) + expected_workload_identity = workload_value.get("workload_identity") + if expected_workload_identity is not None and expected_workload_identity != workload.workload_identity: + raise FabricError("persistent submission workload identity does not verify") + manifest = value.get("manifest") + plan = value.get("plan") + accepted = value.get("accepted") + if not isinstance(manifest, Mapping) or not isinstance(plan, Mapping): + raise FabricError("persistent submission plan or manifest is missing") + return cls( + workload=workload, + work_id=str(value["work_id"]), + accepted_state=str(value["accepted_state"]), + provider_identity=str(value["provider_identity"]), + plan=dict(plan), + manifest=dict(manifest), + bundle_identity=str(value["bundle_identity"]), + bundle_archive_identity=( + str(value["bundle_archive_identity"]) + if value.get("bundle_archive_identity") is not None + else None + ), + archive_path=Path(str(value["archive_path"])), + request_identity=str(value["request_identity"]), + accepted=dict(accepted) if isinstance(accepted, Mapping) else {}, + ) + + +class FabricPersistentBackend: + """Use FabricClient against the persistent controller-owned service boundary.""" + + backend_identity = "ravel-fabric-persistent-consumer/0.1" + + def __init__( + self, + workspace: str | Path, + config: FabricPersistentConfig, + *, + client: Any | None = None, + consumer_context_type: type | None = None, + build_manifest_fn: Any | None = None, + ) -> None: + self.workspace = Path(workspace) + self.workspace.mkdir(parents=True, exist_ok=True) + self.submission_root = self.workspace / "fabric-submissions" + self.submission_root.mkdir(parents=True, exist_ok=True) + self.config = config + self.available = False + self.unavailable_reason: str | None = None + + if client is not None and consumer_context_type is not None and build_manifest_fn is not None: + self._client = client + self._ConsumerContext = consumer_context_type + self._build_manifest = build_manifest_fn + self.available = True + return + + ensure_sibling_src("mncs-fabric", "mncs_validator") + try: + from mncs_fabric import ConsumerContext, FabricClient + from mncs_fabric.artifacts import build_manifest + except ImportError as error: + self.unavailable_reason = f"mncs-fabric unavailable: {type(error).__name__}" + return + try: + self._client = FabricClient.connect( + config.socket_path, + client_identity=config.client_identity, + timeout=config.timeout, + ) + except Exception as error: + self.unavailable_reason = ( + f"persistent Fabric controller unavailable: {type(error).__name__}" + ) + return + + self._ConsumerContext = ConsumerContext + self._build_manifest = build_manifest + self.available = True + + def _require(self) -> None: + if not self.available: + raise FabricUnavailableError( + self.unavailable_reason or "persistent Fabric controller is unavailable" + ) + + def close(self) -> None: + if self.available and hasattr(self._client, "close"): + self._client.close() + + def _submission_path(self, work_id: str) -> Path: + digest = _identity({"fabric_work_id": work_id})[7:] + return self.submission_root / f"{digest}.json" + + def _persist_submission(self, submission: FabricPersistentSubmission) -> None: + path = self._submission_path(submission.work_id) + temporary = path.with_suffix(".tmp") + temporary.write_text( + json.dumps(submission.to_dict(), sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + temporary.replace(path) + + def load_submission(self, work_id: str) -> FabricPersistentSubmission: + path = self._submission_path(work_id) + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise FabricUnavailableError( + f"persistent submission metadata unavailable: {type(error).__name__}" + ) from error + submission = FabricPersistentSubmission.from_dict(value) + if submission.work_id != work_id: + raise FabricError("persistent submission work identity does not match filename") + return submission + + def contract(self) -> Mapping[str, Any]: + self._require() + contract = getattr(self._client, "contract", None) + if contract is None: + return { + "outcome": "UNKNOWN", + "reason": "fabric_public_contract_unavailable", + } + return dict(contract()) + + def workers(self) -> list[dict[str, Any]]: + """Return controller-observed fleet state without reading worker secrets.""" + + self._require() + return [dict(item) for item in self._client.workers()] + + def capabilities(self, worker_label: str) -> Mapping[str, Any]: + self._require() + for worker in self.workers(): + identity = worker.get("worker_id") or worker.get("worker_identity") + if identity == worker_label: + return worker + return { + "worker_identity": worker_label, + "availability": "UNKNOWN", + "capabilities": [], + "reason": "worker_not_visible_through_persistent_controller", + } + + def reconcile(self, records: list[Mapping[str, Any]]) -> Mapping[str, Any]: + """Do not recreate Fabric reconciliation in the consumer process.""" + + self._require() + return { + "outcome": "UNKNOWN", + "scope": "persistent-controller", + "independence": "UNKNOWN", + "records": len(records), + "reason": "fabric_reconciliation_not_exposed_on_public_persistent_boundary", + "semantics": "RAVEL refuses to synthesize Fabric-owned reconciliation", + } + + def _build_provider(self, provider: str, output: Path) -> dict[str, Any]: + from tools.ravel_0_6_build import build + + prior = os.environ.get("RAVEL06_PROVIDER") + try: + os.environ["RAVEL06_PROVIDER"] = provider + return build(output) + finally: + if prior is None: + os.environ.pop("RAVEL06_PROVIDER", None) + else: + os.environ["RAVEL06_PROVIDER"] = prior + + def _make_artifact( + self, provider: str, root: Path + ) -> tuple[Path, dict[str, Any], BundleResult, Path]: + artifact = root / "artifact" + artifact.mkdir(parents=True, exist_ok=True) + build_record = self._build_provider(provider, root / "build") + build_root = root / "build" + for source, target in ( + (build_root / "ravel_0_6_candidate_001", artifact / "candidate-separate"), + (build_root / "ravel_0_6_candidate_001.unity", artifact / "candidate-unity"), + ): + shutil.copyfile(source, target) + target.chmod( + target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + (artifact / "build-record.json").write_text( + json.dumps(build_record, sort_keys=True), encoding="utf-8" + ) + (artifact / "fabric_task.py").write_text( + _task_source(provider), encoding="utf-8" + ) + manifest = self._build_manifest(artifact) + source_manifest = root / "mncs-source-manifest.json" + _write_bundle_source_manifest(artifact, source_manifest) + archive = root / "ravel-execution-bundle.zip" + bundle = build_execution_bundle(source_manifest, artifact, archive) + if ( + bundle.status != "PASS" + or bundle.logical_identity is None + or bundle.archive_identity is None + ): + raise FabricUnavailableError( + "MNCS execution bundle could not be built for persistent Fabric: " + + bundle.reason_code + ) + return artifact, manifest, bundle, archive + + @staticmethod + def _plan(workload: FabricWorkload) -> dict[str, Any]: + return { + "schema_version": "mncs-fabric.job-plan.v0.1", + "job_id": "ravel-fabric-" + workload.workload_identity[7:31], + "candidate_identity": workload.candidate_binding_identity, + "artifact_manifest_identity": workload.fabric_manifest_identity, + "argv": ["@python", "fabric_task.py"], + "working_directory": ".", + "timeout_seconds": float( + workload.resource_budget.get("wall_seconds", 60) + ), + "output_limit_bytes": int( + workload.resource_budget.get("output_bytes", MAX_OUTPUT_BYTES) + ), + "environment": {"PYTHONHASHSEED": "0"}, + "required_capabilities": list(workload.required_capabilities), + "result_paths": ["fabric-result.json"], + "network_policy": "DECLARED_OFFLINE", + } + + def _workload( + self, + provider: str, + *, + candidate_identity: str, + replication_count: int, + manifest: Mapping[str, Any], + bundle: BundleResult, + ) -> FabricWorkload: + experiment_identity = _identity( + { + "candidate_identity": candidate_identity, + "provider": provider, + "question": FabricQuestion.PROVIDER_PARITY, + } + ) + return FabricWorkload( + candidate_identity=candidate_identity, + experiment_identity=experiment_identity, + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity=str(bundle.logical_identity), + fabric_manifest_identity=str(manifest["manifest_identity"]), + required_capabilities=("python",), + replication_count=replication_count, + provider_identity=f"ravel-toy-{provider}-c/1", + ) + + def _consumer_context(self, workload: FabricWorkload) -> Any: + """Translate RAVEL labels into Fabric's opaque sha256 provenance fields.""" + + return self._ConsumerContext( + source_project="RAVEL", + consumer_workload_identity=workload.workload_identity, + experiment_identity=workload.experiment_identity, + forge_workflow_identity=_identity( + {"forge_workflow_identity": workload.forge_workflow_identity} + ), + provider_identity=( + _identity({"provider_identity": workload.provider_identity}) + if workload.provider_identity is not None + else None + ), + partition_identity=_identity( + {"partition_identity": workload.partition_identity} + ), + ) + + def _prepare( + self, + provider: str, + *, + candidate_identity: str, + replication_count: int, + ) -> tuple[FabricWorkload, dict[str, Any], dict[str, Any], BundleResult, Path]: + self._require() + if provider not in {"branching", "ring"}: + raise FabricError("provider must be branching or ring") + if replication_count < 1 or replication_count > MAX_REPLICAS: + raise FabricError("replication_count is outside the bounded range") + root = self.workspace / provider + root.mkdir(parents=True, exist_ok=True) + _artifact, manifest, bundle, archive = self._make_artifact(provider, root) + workload = self._workload( + provider, + candidate_identity=candidate_identity, + replication_count=replication_count, + manifest=manifest, + bundle=bundle, + ) + return workload, self._plan(workload), manifest, bundle, archive + + def submit_provider_parity( + self, + provider: str, + *, + candidate_identity: str = "ravel-0.6-candidate-001", + replication_count: int = 2, + model: str | None = None, + role: str | None = "ravel-development", + ) -> FabricPersistentSubmission: + """Submit detached work; Fabric owns placement, transfer, and execution.""" + + workload, plan, manifest, bundle, archive = self._prepare( + provider, + candidate_identity=candidate_identity, + replication_count=replication_count, + ) + request_identity = _identity( + { + "backend": self.backend_identity, + "workload_identity": workload.workload_identity, + "operation": "execution.submit", + } + ) + accepted = self._client.submit_execution( + plan, + manifest, + replicas=replication_count, + request_id=request_identity, + idempotency_key=workload.workload_identity, + consumer_context=self._consumer_context(workload), + execution_bundle_archive=archive, + model=model, + role=role, + ) + work_id = accepted.get("work_id") + state = accepted.get("state", "UNKNOWN") + if not isinstance(work_id, str) or not work_id: + raise FabricError("persistent Fabric submission returned no work identity") + submission = FabricPersistentSubmission( + workload=workload, + work_id=work_id, + accepted_state=str(state), + provider_identity=provider, + plan=plan, + manifest=manifest, + bundle_identity=str(bundle.logical_identity), + bundle_archive_identity=bundle.archive_identity, + archive_path=archive, + request_identity=request_identity, + accepted=dict(accepted), + ) + self._persist_submission(submission) + return submission + + def execution_status(self, submission: FabricPersistentSubmission | str) -> Mapping[str, Any]: + self._require() + work_id = submission.work_id if isinstance(submission, FabricPersistentSubmission) else submission + return dict(self._client.execution_status(work_id)) + + def collect_work_id(self, work_id: str) -> FabricReferenceResult: + """Recover persisted RAVEL provenance after a client/process restart.""" + + return self.collect_submission(self.load_submission(work_id)) + + def collect_submission( + self, submission: FabricPersistentSubmission + ) -> FabricReferenceResult: + """Collect completed detached work without turning Fabric state into a verdict.""" + + self._require() + payload = dict(self._client.execution_result(submission.work_id)) + container = payload.get("result") if isinstance(payload.get("result"), dict) else payload + results = container.get("results", []) if isinstance(container, dict) else [] + if not isinstance(results, list): + results = [] + return self._report( + submission.workload, + submission.provider_identity, + submission.bundle_identity, + submission.bundle_archive_identity, + [dict(item) for item in results if isinstance(item, Mapping)], + detached_work_id=submission.work_id, + ) + + def execute_provider_parity( + self, + provider: str, + *, + candidate_identity: str = "ravel-0.6-candidate-001", + replication_count: int = 2, + ) -> FabricReferenceResult: + """Execute through FabricClient; long plans may detach inside FabricClient.""" + + workload, plan, manifest, bundle, archive = self._prepare( + provider, + candidate_identity=candidate_identity, + replication_count=replication_count, + ) + request_identity = _identity( + { + "backend": self.backend_identity, + "workload_identity": workload.workload_identity, + "operation": "execution.execute", + } + ) + results = self._client.execute( + plan, + manifest, + replicas=replication_count, + request_id=request_identity, + consumer_context=self._consumer_context(workload), + execution_bundle_archive=archive, + ) + return self._report( + workload, + provider, + str(bundle.logical_identity), + bundle.archive_identity, + [dict(item) for item in results if isinstance(item, Mapping)], + ) + + def _report( + self, + workload: FabricWorkload, + provider: str, + bundle_identity: str, + bundle_archive_identity: str | None, + results: list[Mapping[str, Any]], + *, + detached_work_id: str | None = None, + ) -> FabricReferenceResult: + observations: list[FabricExecutionObservation] = [] + records: list[Mapping[str, Any]] = [] + statuses: list[str] = [] + + for result in results: + record = result.get("record") if isinstance(result.get("record"), Mapping) else {} + receipt = result.get("receipt") if isinstance(result.get("receipt"), Mapping) else {} + records.append(record) + raw_status = record.get("outcome", "UNKNOWN") + status = raw_status if raw_status in {"PASS", "FAIL", "UNKNOWN"} else "UNKNOWN" + statuses.append(status) + + result_identities = tuple( + item["sha256"] + for item in record.get("results", []) + if isinstance(item, Mapping) and isinstance(item.get("sha256"), str) + ) + node = record.get("node") if isinstance(record.get("node"), Mapping) else {} + reason = record.get("termination_reason") or result.get("reason") or result.get("disposition") or "UNKNOWN" + resources: dict[str, Any] = { + "duration_ms": record.get("duration_ms"), + "node_fingerprint": node.get("node_fingerprint"), + "persistent_controller": True, + } + for key in ( + "placement_admission", + "resource_snapshot", + "runtime_observation", + "runtime_binding", + "runtime_capability_observation", + "runtime_capability_binding", + "provenance_binding", + ): + if isinstance(result.get(key), Mapping): + resources[key] = dict(result[key]) + if detached_work_id is not None: + resources["fabric_work_id"] = detached_work_id + + observations.append( + FabricExecutionObservation( + workload_identity=workload.workload_identity, + candidate_identity=workload.candidate_identity, + candidate_binding_identity=workload.candidate_binding_identity, + worker_identity=result.get("worker_identity") or node.get("machine_label"), + request_identity=result.get("request_identity"), + fabric_record_identity=result.get("record_identity") or record.get("record_id"), + fabric_manifest_identity=record.get("artifact_manifest_identity") + or workload.fabric_manifest_identity, + bundle_identity=result.get("bundle_identity") or bundle_identity, + bundle_archive_identity=bundle_archive_identity, + receipt_identity=result.get("receipt_identity") or receipt.get("receipt_identity"), + challenge_identity=result.get("challenge_identity"), + replay_identity=None, + provider_identity=provider, + result_identities=result_identities, + fabric_outcome=status, + reason_codes=(str(reason),), + resource_observations=resources, + ) + ) + + aggregate = _aggregate(statuses) if statuses else "UNKNOWN" + reconciliation = self.reconcile(records) + return FabricReferenceResult( + workload=workload, + observations=tuple(observations), + reconciliation=reconciliation, + bundle={ + "mncs_status": "PASS", + "verified": "PASS", + "logical_identity": bundle_identity, + "archive_identity": bundle_archive_identity, + "pre_staged": "NOT_REQUIRED", + "transport": "fabric-controller-owned-native-bundle-transfer", + "executed": aggregate, + "official_receipt_binding": "UNKNOWN", + }, + replay={ + "status": "UNKNOWN", + "scope": "persistent-controller", + "reason": "challenge_replay_not_requested_by_this_adapter", + }, + negative_cases={ + "status": "UNKNOWN", + "scope": "persistent-controller", + "reason": "legacy_local_negative_matrix_not_replayed_automatically", + }, + fabric_status=aggregate, + limitations=( + "Fabric owns worker placement, endpoint trust, bundle transport, and raw execution evidence.", + "RAVEL does not synthesize persistent-controller reconciliation.", + "Persistent execution evidence is development-only and does not grant evaluator or promotion authority.", + "Legacy local/network backends remain available for historical compatibility and negative-matrix reproduction.", + ), + ) + + +__all__ = [ + "FabricPersistentBackend", + "FabricPersistentConfig", + "FabricPersistentSubmission", + "PERSISTENT_CONFIG_SCHEMA", + "PERSISTENT_SUBMISSION_SCHEMA", +] From 40fc238ca08150e2e42a5ae4c0db3f25ccbb7bcf Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:49:36 -0800 Subject: [PATCH 2/7] Test persistent Fabric consumer boundary --- tests/test_fabric_persistent.py | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/test_fabric_persistent.py diff --git a/tests/test_fabric_persistent.py b/tests/test_fabric_persistent.py new file mode 100644 index 0000000..5e230ca --- /dev/null +++ b/tests/test_fabric_persistent.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import json +import tempfile +import unittest +from pathlib import Path + +from ravel.fabric import FabricError, FabricQuestion, FabricWorkload +from ravel.fabric_persistent import ( + FabricPersistentBackend, + FabricPersistentConfig, + FabricPersistentSubmission, +) + + +class _Context: + def __init__(self, **values): + self.values = values + + +class PersistentConfigTests(unittest.TestCase): + def test_config_exposes_only_controller_consumer_fields(self) -> None: + with tempfile.TemporaryDirectory(prefix="ravel-persistent-config-") as directory: + path = Path(directory) / "fabric.toml" + path.write_text( + """ +[fabric] +mode = "persistent-controller" +socket_path = "/run/mncs-fabric/controller.sock" +client_identity = "ravel" +timeout = 7.5 +""".strip() + + "\n", + encoding="utf-8", + ) + config = FabricPersistentConfig.load(path) + self.assertEqual(config.client_identity, "ravel") + self.assertEqual(config.timeout, 7.5) + self.assertEqual(config.to_dict()["authority"], "consumer-only") + + def test_config_rejects_worker_credentials_and_endpoints(self) -> None: + with tempfile.TemporaryDirectory(prefix="ravel-persistent-config-") as directory: + path = Path(directory) / "fabric.toml" + path.write_text( + """ +[fabric] +mode = "persistent-controller" +socket_path = "/run/mncs-fabric/controller.sock" +ca_file = "/secret/ca.pem" +""".strip() + + "\n", + encoding="utf-8", + ) + with self.assertRaises(FabricError): + FabricPersistentConfig.load(path) + + +class PersistentContractTests(unittest.TestCase): + def _workload(self) -> FabricWorkload: + return FabricWorkload( + candidate_identity="ravel-0.6-candidate-001", + experiment_identity="sha256:" + "1" * 64, + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity="sha256:" + "2" * 64, + fabric_manifest_identity="sha256:" + "3" * 64, + provider_identity="ravel-toy-branching-c/1", + ) + + def test_consumer_context_hashes_ravel_labels_into_fabric_provenance(self) -> None: + backend = FabricPersistentBackend.__new__(FabricPersistentBackend) + backend._ConsumerContext = _Context + context = backend._consumer_context(self._workload()) + self.assertEqual(context.values["source_project"], "RAVEL") + self.assertTrue(context.values["consumer_workload_identity"].startswith("sha256:")) + self.assertTrue(context.values["forge_workflow_identity"].startswith("sha256:")) + self.assertTrue(context.values["provider_identity"].startswith("sha256:")) + self.assertTrue(context.values["partition_identity"].startswith("sha256:")) + + def test_persistent_report_keeps_fabric_status_separate_from_evaluator_authority(self) -> None: + backend = FabricPersistentBackend.__new__(FabricPersistentBackend) + backend.available = True + backend.unavailable_reason = None + workload = self._workload() + result = backend._report( + workload, + "branching", + "sha256:" + "2" * 64, + "sha256:" + "4" * 64, + [ + { + "disposition": "EXECUTED", + "worker_identity": "fabric-worker-01", + "request_identity": "sha256:" + "5" * 64, + "record_identity": "sha256:" + "6" * 64, + "receipt_identity": "sha256:" + "7" * 64, + "bundle_identity": "sha256:" + "2" * 64, + "record": { + "record_id": "sha256:" + "6" * 64, + "artifact_manifest_identity": "sha256:" + "3" * 64, + "outcome": "PASS", + "termination_reason": "completed", + "results": [{"sha256": "8" * 64}], + "node": { + "machine_label": "fabric-worker-01", + "node_fingerprint": "sha256:" + "9" * 64, + }, + }, + "receipt": {"receipt_identity": "sha256:" + "7" * 64}, + "provenance_binding": {"authority": "provenance-only"}, + } + ], + detached_work_id="work-123", + ) + self.assertEqual(result.fabric_status, "PASS") + self.assertEqual(result.reconciliation["outcome"], "UNKNOWN") + self.assertEqual(result.bundle["pre_staged"], "NOT_REQUIRED") + self.assertEqual( + result.bundle["transport"], "fabric-controller-owned-native-bundle-transfer" + ) + self.assertEqual(len(result.observations), 1) + observation = result.observations[0] + self.assertEqual(observation.fabric_outcome, "PASS") + self.assertEqual( + observation.semantics, "development observation; not evaluator authority" + ) + self.assertEqual( + observation.resource_observations["fabric_work_id"], "work-123" + ) + + def test_submission_round_trip_preserves_workload_identity(self) -> None: + workload = self._workload() + submission = FabricPersistentSubmission( + workload=workload, + work_id="work-123", + accepted_state="QUEUED", + provider_identity="branching", + plan={"schema_version": "mncs-fabric.job-plan.v0.1"}, + manifest={"manifest_identity": "sha256:" + "3" * 64}, + bundle_identity="sha256:" + "2" * 64, + bundle_archive_identity="sha256:" + "4" * 64, + archive_path=Path("/tmp/ravel-execution-bundle.zip"), + request_identity="sha256:" + "5" * 64, + accepted={"work_id": "work-123", "state": "QUEUED"}, + ) + restored = FabricPersistentSubmission.from_dict( + json.loads(json.dumps(submission.to_dict())) + ) + self.assertEqual(restored.work_id, submission.work_id) + self.assertEqual( + restored.workload.workload_identity, submission.workload.workload_identity + ) + self.assertEqual(restored.bundle_identity, submission.bundle_identity) + + +if __name__ == "__main__": + unittest.main() From 5cef1a8891dababf977647e8b7607798658903b1 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:49:40 -0800 Subject: [PATCH 3/7] Add persistent Fabric config example --- config/ravel-fabric-persistent.example.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config/ravel-fabric-persistent.example.toml diff --git a/config/ravel-fabric-persistent.example.toml b/config/ravel-fabric-persistent.example.toml new file mode 100644 index 0000000..775b134 --- /dev/null +++ b/config/ravel-fabric-persistent.example.toml @@ -0,0 +1,10 @@ +# RAVEL -> persistent MNCS Fabric consumer configuration. +# +# The persistent controller owns worker endpoints, TLS material, TrustStore +# state, registries, bundle caches, placement, and execution lifecycle. +# RAVEL receives only the controller consumer socket. +[fabric] +mode = "persistent-controller" +socket_path = "/run/mncs-fabric/controller.sock" +client_identity = "ravel" +timeout = 5.0 From a5c37abb6414e866771b5e1c1fd6bc019588875d Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:49:54 -0800 Subject: [PATCH 4/7] Document persistent Fabric integration --- docs/FABRIC_INTEGRATION.md | 132 ++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/docs/FABRIC_INTEGRATION.md b/docs/FABRIC_INTEGRATION.md index 5c339e3..b0e2111 100644 --- a/docs/FABRIC_INTEGRATION.md +++ b/docs/FABRIC_INTEGRATION.md @@ -10,6 +10,96 @@ RAVEL semantic question -> RAVEL scoped advisory experience ``` +## Current live integration: persistent controller + +`FabricPersistentBackend` in `src/ravel/fabric_persistent.py` is the preferred +live-development path. It consumes Fabric through the public +`FabricClient.connect(controller.sock)` boundary and deliberately does **not** +load or own: + +- worker host/port endpoints, +- CA files, client certificates, or client keys, +- TrustStore state, +- worker registry files, +- worker bundle-cache paths, +- placement state, or +- controller execution ledgers. + +Those remain Fabric-owned. RAVEL supplies a development-only semantic workload, +an immutable MNCS execution bundle, required capabilities, and opaque provenance. +Fabric owns admission, controller-side bundle transfer, placement, worker +execution, raw records, receipts, and detached execution state. + +A minimal configuration is: + +```toml +[fabric] +mode = "persistent-controller" +socket_path = "/run/mncs-fabric/controller.sock" +client_identity = "ravel" +timeout = 5.0 +``` + +See +[`config/ravel-fabric-persistent.example.toml`](../config/ravel-fabric-persistent.example.toml). + +The config parser is intentionally narrow. Worker endpoints and trust material +are rejected if they are added to the persistent config. This prevents RAVEL +from silently regaining responsibilities that belong to Fabric. + +### Synchronous and detached execution + +The backend supports both forms: + +```python +from ravel.fabric_persistent import FabricPersistentBackend, FabricPersistentConfig + +backend = FabricPersistentBackend( + "build/fabric-live", + FabricPersistentConfig.load("config/ravel-fabric-persistent.toml"), +) + +report = backend.execute_provider_parity("branching") +``` + +For long-running work RAVEL can submit and disconnect: + +```python +submission = backend.submit_provider_parity("branching") +print(submission.work_id) + +status = backend.execution_status(submission) +report = backend.collect_submission(submission) +``` + +Detached submission metadata is written under +`/fabric-submissions/`. A later RAVEL process can recover it with +`load_submission(work_id)` or collect the final evidence with +`collect_work_id(work_id)`. Fabric remains the source of truth for execution +state; the RAVEL metadata is only the provenance needed to interpret the +returned evidence. + +No model is hard-coded into the adapter. `submit_provider_parity()` accepts an +optional Fabric `model` and `role`, while the default leaves model/worker +selection to the surrounding MNCS policy and Fabric capability inventory. + +### Authority and evidence boundary + +Fabric outcomes remain execution evidence. They are never promoted into a RAVEL +evaluator verdict. Persistent reports therefore keep: + +```text +Fabric execution outcome PASS / FAIL / UNKNOWN +Fabric reconciliation UNKNOWN unless Fabric exposes it publicly +RAVEL evaluator authority separate +promotion / selection authority not asserted +``` + +RAVEL explicitly refuses to reimplement Fabric reconciliation in the consumer +process. If the persistent public API does not expose a Fabric-owned +reconciliation result, the RAVEL report says `UNKNOWN` rather than fabricating +independence. + ## Versioned RAVEL contracts `ravel-fabric-workload/0.1` is RAVEL's semantic request. It binds the candidate, @@ -30,14 +120,15 @@ The compatibility snapshot is [`ravel-0.6-family-compatibility-lock.json`](../ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json); it is evidence about inspected public contracts, not an installation lockfile. -## Local reference backend +## Historical local reference backend -`FabricLocalBackend` uses Fabric's public `FabricService`, `LocalController`, -`LocalWorker`, manifest, receipt, challenge/replay, and reconciliation APIs. It -builds a bounded development-only artifact and runs the branching and ring -provider parity task on two logical workers. These workers share a process and -host, so the report labels the scope `local-in-process-replication` and keeps -independence `UNKNOWN`. +`FabricLocalBackend` remains available for reproducible local development and +negative-matrix testing. It uses Fabric's public `FabricService`, +`LocalController`, `LocalWorker`, manifest, receipt, challenge/replay, and +reconciliation APIs. It builds a bounded development-only artifact and runs the +branching and ring provider parity task on two logical workers. These workers +share a process and host, so the report labels the scope +`local-in-process-replication` and keeps independence `UNKNOWN`. The local command is: @@ -51,32 +142,27 @@ manifest and corrupt record (`FAIL`), idempotent duplicate request, and conflicting replay. A first valid challenge consumption is `PASS`; consuming the same challenge again is a Fabric `FAIL` and is retained rather than hidden. -## Network boundary +## Legacy direct-network boundary + +`FabricNetworkBackend` and `FabricNetworkConfig` are retained for historical +compatibility and targeted network-reference testing. They directly describe +TLS workers and require pre-staged bundles. They are **not** the preferred live +fleet path now that Fabric exposes a persistent controller consumer API. -`FabricNetworkBackend` is optional and TLS-only. `FabricNetworkConfig` requires -operator-supplied CA, client certificate/key, trust store, worker endpoint, -capabilities, and an exact pre-staged Fabric manifest identity. The checked-in -[`ravel-fabric.example.toml`](../config/ravel-fabric.example.toml) contains only -placeholders. RAVEL does not use SSH as a dispatch protocol, does not add a -plaintext fallback, and does not claim native bundle transfer until Fabric -exposes and verifies it. +New live integrations should use `FabricPersistentBackend` instead of teaching +RAVEL worker endpoints, trust material, or bundle staging. -The report therefore keeps these facts separate: +The legacy report keeps these facts separate: ```text bundle verified PASS bundle pre-staged PASS (local artifact root) archive executed UNKNOWN -receipt/archive probe FAIL (Fabric receipt currently binds its artifact manifest) +receipt/archive probe FAIL (legacy Fabric receipt binds its artifact manifest) Fabric reconciliation PASS (Fabric question only) RAVEL evaluator separate; normally UNKNOWN ``` -The receipt/archive probe is retained as a negative compatibility observation; -it is not rewritten as an official execution binding. Native Fabric bundle -transfer and a receipt adapter that binds the MNCS archive remain a sibling -capability boundary. - Fabric execution is not a sandbox, independent evaluation, protected custody, MNCS/MNCDS conformance, or promotion authority. Selection and future-final -material are rejected by the workload contract and are not dispatched. +material remain outside this development adapter. From 7ea784171533db2e662759ac005d25c50f2890bc Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:50:02 -0800 Subject: [PATCH 5/7] Expose persistent Fabric module --- src/ravel/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ravel/__init__.py b/src/ravel/__init__.py index 9b58389..36348ac 100644 --- a/src/ravel/__init__.py +++ b/src/ravel/__init__.py @@ -7,6 +7,7 @@ "development_evaluator", "experience", "fabric", + "fabric_persistent", "knowledge", "lifecycle", "matched_compute", From 75c00b851feecab92bdd7cd3950f4486dbde51fa Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:50:23 -0800 Subject: [PATCH 6/7] Record persistent Fabric implementation status --- .../0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md index 9be5d2f..d04a6b2 100644 --- a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md +++ b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md @@ -82,15 +82,27 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. executions remain `UNKNOWN` until governed disposition exists; rejected and unavailable outcomes remain negative and deterministic retrieval includes them. -- **Fabric development substrate:** `ravel.fabric` now defines the +- **Fabric development substrate:** `ravel.fabric` defines the `ravel-fabric-workload/0.1` and `ravel-fabric-observation/0.1` boundaries, executes a bounded branching/ring provider-parity matrix through Fabric's public local controller/worker service, retains Fabric record/receipt/bundle identities, exercises challenge/replay and conflicting-request handling, and imports observations into advisory negative/`UNKNOWN` memory. Reconciliation is explicitly local in-process replication; it is not independence or final - evaluation. The TLS-only network adapter is implemented but unavailable - without operator trust material and pre-staged bundles. + evaluation. The TLS-only direct-network adapter remains available for + historical compatibility. +- **Persistent Fabric consumer path:** `ravel.fabric_persistent` adds the + preferred live-development adapter for Fabric's persistent controller public + API. It connects through `FabricClient.connect(...)`, leaves worker endpoint + and trust material under controller ownership, delegates immutable bundle + transfer to Fabric, supports controller-owned placement and execution, + supports detached `submit/status/result` lifecycles, and persists only the + RAVEL provenance needed to recover a detached submission after a client + restart. Persistent Fabric outcomes remain development observations and do + not become evaluator, selection, promotion, or conformance authority. The + adapter intentionally reports Fabric reconciliation `UNKNOWN` until a + Fabric-owned reconciliation result is exposed through the persistent public + boundary. ## Not yet implemented or externally unavailable @@ -119,9 +131,14 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. are not declared by the frozen contract. Forge/RAVEL lifecycle mapping is reference-only and does not collapse the two state machines. Observation / reporting remains the next safe C extraction candidate after dependency review. -- The project-local Forge configuration now declares Fabric capability, - reference, negative-matrix, and family-compatibility-lock workflows. The - local Fabric path is optional for package import and ordinary CI. +- The project-local Forge configuration declares Fabric capability, local + reference, negative-matrix, and family-compatibility-lock workflows. A live + persistent-controller E2E workflow still needs to be executed on an enrolled + multi-worker Fabric fleet; CI cannot claim that external controller evidence. +- A Rust-native persistent Fabric client is not yet implemented. Python remains + the compatibility bridge for the current Fabric public API while the boundary + stabilizes; the eventual Rust port must preserve the same authority and + provenance semantics and prove parity before replacement. - R6-05 selection evaluation and promotion logic have not been consumed. The ledger is infrastructure only; no candidate is frozen or selected by it. - R6-06 external final custody/evaluation remains unavailable and `UNKNOWN`. From a28ca1908085e3e9697722bf2aa0c73b7d14da64 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 15 Aug 2026 15:51:24 -0800 Subject: [PATCH 7/7] Describe persistent Fabric live path --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cf4b879..991e9dd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ RAVEL — the **Recursive Adaptive Vector Execution Lattice** — is an experime RAVEL operates beneath the technical authority of the Machine-Native Complexity Standard (MNCS) and the Machine-Native Complexity Development Standard (MNCDS). It is not intended to replace a language model, compiler, static analyzer, test framework, or the MNCS Forge. Its role is to decide what evidence should be gathered, what action should follow, and what experience should be retained for later use without redefining the governing status of that evidence. -> **Project status:** RAVEL is research software. Historical RAVEL 0.4 and 0.5 results remain development `FAIL`; RAVEL 0.6 candidate-001 now has digest-bound policy/evaluator surfaces, separately compiled checkpoint and world/provider contracts, branching/ring unity parity, a Forge-governed development configuration, official MNCS bundle/receipt adapters, a bounded local Fabric development path, and lifecycle/memory integration. It remains unfrozen and has not been selection-evaluated, independently evaluated, or promoted. **Rust is now the canonical future implementation language** (`crates/`, `ravel-rust-foundation/0.1`); C and Python remain the historical and 0.6 compatibility surfaces. Formal MNCS/MNCDS conformance, independent attestation, protected custody, production safety, and general recursive self-improvement remain `UNKNOWN`. +> **Project status:** RAVEL is research software. Historical RAVEL 0.4 and 0.5 results remain development `FAIL`; RAVEL 0.6 candidate-001 now has digest-bound policy/evaluator surfaces, separately compiled checkpoint and world/provider contracts, branching/ring unity parity, a Forge-governed development configuration, official MNCS bundle/receipt adapters, bounded local and persistent-controller Fabric development paths, and lifecycle/memory integration. It remains unfrozen and has not been selection-evaluated, independently evaluated, or promoted. **Rust is now the canonical future implementation language** (`crates/`, `ravel-rust-foundation/0.1`); C and Python remain the historical and 0.6 compatibility surfaces. Formal MNCS/MNCDS conformance, independent attestation, protected custody, production safety, and general recursive self-improvement remain `UNKNOWN`. ## Place in the MNCS ecosystem @@ -114,13 +114,17 @@ algorithmic superiority. The bounded component surfaces in `src/ravel/world.py`, `src/ravel/transition.py`, `src/ravel/planning.py`, `src/ravel/checkpoint.py`, and `src/ravel/mechanism_state.py` provide deterministic provider substitution and checkpoint fixtures; they do not replace the historical 0.5 source. -`src/ravel/fabric.py` adds the optional public Fabric local-controller path; +`src/ravel/fabric.py` retains the local/direct-network compatibility paths and `tools/ravel_fabric_reference.py` runs branching/ring parity, replication, reconciliation, bundle, and replay/negative checks without dispatching selection -or final material. The Rust crates under `crates/` and the `ravel-rs` CLI are -the future implementation home; `src/ravel/rust_bridge.py` and -`tests/test_rust_parity.py` prove discrete C/Python/Rust agreement without -making either side authoritative. See [`docs/RUST_FOUNDATION.md`](docs/RUST_FOUNDATION.md). +or final material. `src/ravel/fabric_persistent.py` is the preferred live Fabric +consumer path: it connects through the persistent controller, delegates bundle +transport and worker placement to Fabric, and supports detached execution with +restart-safe RAVEL provenance metadata. The Rust crates under `crates/` and the +`ravel-rs` CLI are the future implementation home; `src/ravel/rust_bridge.py` +and `tests/test_rust_parity.py` prove discrete C/Python/Rust agreement without +making either side authoritative. See [`docs/RUST_FOUNDATION.md`](docs/RUST_FOUNDATION.md) +and [`docs/FABRIC_INTEGRATION.md`](docs/FABRIC_INTEGRATION.md). ## Non-goals