diff --git a/AGENTS.md b/AGENTS.md index 8b0c5a2bf..9ce145463 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -352,8 +352,6 @@ Each domain has exactly **one** write-owning module (or one tightly-scoped famil | Provider install leases (`health/providers/{local,parakeet}.lease`) | `solstone/think/providers/install_lease.py` | | Provider runtime health and retry-token records (`health/providers/runtime/{local,parakeet}.json`, `health/providers/runtime/{local,parakeet}.retry-token.json`, `health/providers/runtime/{local,parakeet}.operation.lock`) | `solstone/think/providers/runtime_health.py` | | Native speakers-analyze circuit breaker (`health/speakers-analyze/circuit-breaker.json`) | `solstone/think/speakers_analyze_runtime.py` | -| Sandbox profile intent (`health/sandbox-profile/intent.json`) | `solstone/think/sandbox_profile/intent.py` — disposable sandbox lifecycle intent only; marker file remains externally owned and read-only | -| Sandbox profile probes v1 ledger, attempt identity, and proof-private working paths (`health/sandbox-profile/probes-v1.jsonl`, `health/sandbox-profile/probes-v1.lock`, `health/sandbox-profile/probe-attempts-v1/**`, including `scout_provider_probe.SCOUT_PROOF_PRIVATE_*` and `spb_backup_probe` attempt-local `spb/` working paths) | `solstone/think/sandbox_profile/probe_writer.py` + `solstone/think/sandbox_profile/probe_slot.py` + `solstone/think/sandbox_profile/scout_provider_probe.py` + `solstone/think/sandbox_profile/spb_backup_probe.py` — disposable sandbox production-probe coordination only; `probe_writer` owns append-only ledger records, `probe_slot` owns probe lease and attempt identity, `scout_provider_probe` owns creation plus verified removal of Scout proof-private child working paths and containment directories, and `spb_backup_probe` owns attempt-local SPB proof working paths. No retained proof-created secrets, stdout/stderr, request/response bytes, child stdin, or production identifiers are written to ledger or retained attempt state. | | Provider artifact manifests (`cache/providers/**/.solstone-provider-manifest.json`, `cache/providers/local/mlx/**/*.manifest.json`) | `solstone/think/providers/artifact_proof.py` | | nvattest appraiser cache (`cache/providers/nvattest/**`) | `solstone/think/providers/nvattest_install.py` | | Media offload ledger (`health/offload/.jsonl`) | `solstone/think/offload_ledger.py` | diff --git a/Makefile b/Makefile index 37285c614..3a362bd6b 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ export TMPDIR := /var/tmp PYTEST_BASETEMP_INIT := BASETEMP=$$(mktemp -d /var/tmp/solstone-pytest-XXXXXX); trap 'rm -rf "$$BASETEMP"' EXIT INT TERM; PYTEST_BASETEMP_FLAG := --basetemp "$$BASETEMP" -.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-release-advisory-liveness check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors sandbox-probe-contract check-sandbox-probe-contract build-native-sol-grammar-oracle check-native-sol-grammar-oracle build-native-sol-root-contract check-native-sol-root-contract check-core-sdist-compile-inputs build-native-sol-journal-host-commands check-native-sol-journal-host-commands build-journal-access-rejection-inventory check-journal-access-rejection-inventory check-native-sol-python-manifest build-native-sol-inventory check-native-sol-inventory check-native-sol-architecture check-native-sol-contract-routes check-native-sol-conformance check-native-sol-coverage check-native-sol-no-python-spawn check-native-sol-compat check-native-sol-docs-links check-removed-time-parser-ready dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-speakers-analyze-linux wheel-speakers-analyze-linux-x86_64 wheel-speakers-analyze-linux-aarch64 wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-channel-adapter-scrub check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-extras-consistency check-cogitate-prompts smoke-cogitate release release-test publish-release publish-release-test FORCE +.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-release-advisory-liveness check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors build-native-sol-grammar-oracle check-native-sol-grammar-oracle build-native-sol-root-contract check-native-sol-root-contract check-core-sdist-compile-inputs build-native-sol-journal-host-commands check-native-sol-journal-host-commands build-journal-access-rejection-inventory check-journal-access-rejection-inventory check-native-sol-python-manifest build-native-sol-inventory check-native-sol-inventory check-native-sol-architecture check-native-sol-contract-routes check-native-sol-conformance check-native-sol-coverage check-native-sol-no-python-spawn check-native-sol-compat check-native-sol-docs-links check-removed-time-parser-ready dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-speakers-analyze-linux wheel-speakers-analyze-linux-x86_64 wheel-speakers-analyze-linux-aarch64 wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-channel-adapter-scrub check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-extras-consistency check-cogitate-prompts smoke-cogitate release release-test publish-release publish-release-test FORCE # Default target - install package in editable mode all: install @@ -652,9 +652,6 @@ install-checks: .installed @echo "=== Checking journal resolution vectors ===" @$(MAKE) check-journal-resolution-vectors @echo "" - @echo "=== Checking sandbox probe contract ===" - @$(MAKE) check-sandbox-probe-contract - @echo "" @echo "=== Checking nvattest authority ===" @$(MAKE) check-nvattest-authority @echo "" @@ -831,12 +828,6 @@ journal-resolution-vectors: check-journal-resolution-vectors: .installed $(VENV_BIN)/python scripts/build_journal_resolution_vectors.py --check -sandbox-probe-contract: - $(VENV_BIN)/python scripts/build_sandbox_probe_contract.py - -check-sandbox-probe-contract: .installed - $(VENV_BIN)/python scripts/build_sandbox_probe_contract.py --check - nvattest-authority: $(VENV_BIN)/python scripts/build_nvattest_authority.py diff --git a/core/crates/solstone-core-sol/src/generated/journal_host_commands.rs b/core/crates/solstone-core-sol/src/generated/journal_host_commands.rs index d7fa0a7c5..82d3550ce 100644 --- a/core/crates/solstone-core-sol/src/generated/journal_host_commands.rs +++ b/core/crates/solstone-core-sol/src/generated/journal_host_commands.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright (c) 2026 sol pbc -pub const JOURNAL_HOST_COMMAND_COUNT: usize = 45; +pub const JOURNAL_HOST_COMMAND_COUNT: usize = 44; pub const JOURNAL_HOST_COMMANDS: &[&str] = &[ "backfill-processing-records", "backup", @@ -30,7 +30,6 @@ pub const JOURNAL_HOST_COMMANDS: &[&str] = &[ "observer", "reprocess", "restart-convey", - "sandbox-profile", "schedule", "segment", "sense", diff --git a/docs/SOLCLI.md b/docs/SOLCLI.md index 391177e72..833bfdccc 100644 --- a/docs/SOLCLI.md +++ b/docs/SOLCLI.md @@ -72,13 +72,6 @@ For host-only commands, use the `journal` dispatcher instead: create a Python module with `main()` and register it in `solstone/think/sol_cli.py` with the appropriate service or universal surface. -`journal sandbox-profile` is a host-only service command for disposable sandbox -journals. It is a deterministic, redacted lifecycle wrapper around existing -service enable/disable owners and is gated by the externally owned -`.solstone-sandbox.json` marker. Its closed capability vocabulary is defined in -`solstone/think/sandbox_profile/manifest.py`; do not duplicate that list in -generated skill references. - ### Files to maintain | File | What to do | diff --git a/scripts/build_native_sol_journal_host_commands.py b/scripts/build_native_sol_journal_host_commands.py index 7390b4c15..ed9519fc6 100644 --- a/scripts/build_native_sol_journal_host_commands.py +++ b/scripts/build_native_sol_journal_host_commands.py @@ -14,7 +14,7 @@ SOURCE = REPO_ROOT / "solstone/think/sol_cli.py" OUTPUT = ( REPO_ROOT / "core/crates/solstone-core-sol/src/generated/journal_host_commands.rs" ) -EXPECTED_SERVICE_COMMANDS_COUNT = 43 +EXPECTED_SERVICE_COMMANDS_COUNT = 42 EXPECTED_UNIVERSAL_COMMANDS = frozenset({"doctor", "check", "contract", "link"}) EXPECTED_SERVICE_ALIASES = frozenset({"up", "down"}) EXPECTED_UNIVERSAL_ALIASES = frozenset() diff --git a/scripts/build_sandbox_probe_contract.py b/scripts/build_sandbox_probe_contract.py deleted file mode 100644 index cbbaf44c9..000000000 --- a/scripts/build_sandbox_probe_contract.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Build the committed sandbox probe v1 machine contract.""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -from solstone.think.sandbox_profile import probe_contract - -ROOT = Path(__file__).resolve().parent.parent -ARTIFACT_PATH = ( - ROOT / "solstone" / "think" / "sandbox_profile" / "probe_contract_v1.json" -) - - -def render_probe_contract_json() -> str: - return ( - json.dumps(probe_contract.contract_payload(), indent=2, sort_keys=True) + "\n" - ) - - -def write_outputs() -> None: - ARTIFACT_PATH.parent.mkdir(parents=True, exist_ok=True) - ARTIFACT_PATH.write_text(render_probe_contract_json(), encoding="utf-8") - print(f"wrote {ARTIFACT_PATH.relative_to(ROOT)}") - - -def check_outputs() -> int: - expected = render_probe_contract_json() - try: - actual = ARTIFACT_PATH.read_text(encoding="utf-8") - except FileNotFoundError: - actual = None - if actual != expected: - print( - "Sandbox probe contract is stale: " - f"{ARTIFACT_PATH.relative_to(ROOT)}. Run: make sandbox-probe-contract", - file=sys.stderr, - ) - return 1 - return 0 - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--check", action="store_true") - args = parser.parse_args() - if args.check: - return check_outputs() - write_outputs() - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_journal_io_access.py b/scripts/check_journal_io_access.py index d82b8e619..5c8a4b7ce 100644 --- a/scripts/check_journal_io_access.py +++ b/scripts/check_journal_io_access.py @@ -185,7 +185,6 @@ OWNER_FILES: frozenset[str] = frozenset( "solstone/think/importers/plaud.py", # streamed imported-audio install. "solstone/think/importers/sync.py", "solstone/think/importers/utils.py", - "solstone/think/sandbox_profile/intent.py", } ) diff --git a/solstone/think/sandbox_profile/__init__.py b/solstone/think/sandbox_profile/__init__.py deleted file mode 100644 index 934af9075..000000000 --- a/solstone/think/sandbox_profile/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Disposable sandbox journal profile lifecycle command.""" diff --git a/solstone/think/sandbox_profile/capabilities.py b/solstone/think/sandbox_profile/capabilities.py deleted file mode 100644 index 533e73448..000000000 --- a/solstone/think/sandbox_profile/capabilities.py +++ /dev/null @@ -1,729 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Capability composition and read-only observed-state reconciliation.""" - -from __future__ import annotations - -import hashlib -import json -import logging -from pathlib import Path -from typing import Any - -from solstone.think.backup import state as backup_state -from solstone.think.backup.hosted import delete_hosted_binding, save_hosted_binding -from solstone.think.journal_config import ( - JournalConfigMutation, - JournalConfigPostCommitError, - mutate_journal_config, -) -from solstone.think.link.ca import load_or_generate_ca -from solstone.think.link.paths import LinkState, ca_dir -from solstone.think.providers.local_endpoint import ( - confidential_fingerprint_provenance_block, -) -from solstone.think.sandbox_profile import envelope, intent, manifest -from solstone.think.services import scout, spl, spp -from solstone.think.services.spb_handoff import _BINDING_FIELDS, _binding_from_payload -from solstone.think.services.spl_handoff import _classify_spl_payload - -_SCOUT_FIELDS = tuple(scout._HANDOFF_FIELDS) -_SPL_FIELDS = ("service", "state", "approved_at") -_SPP_FIELDS = tuple(spp._HANDOFF_FIELDS) -logger = logging.getLogger(__name__) - -_DISABLE_CARRIED_RESIDUALS: dict[str, frozenset[str]] = { - manifest.CAPABILITY_SCOUT: frozenset({"scout_block_missing"}), - manifest.CAPABILITY_SPB: frozenset({"spb_binding_missing"}), - manifest.CAPABILITY_SPP: frozenset({"spp_block_missing"}), -} - - -class PayloadValidationError(ValueError): - def __init__(self, message: str, *, error_code: str = "payload_invalid") -> None: - super().__init__(message) - self.message = message - self.error_code = error_code - - -def _read_json_file(path: Path) -> Any: - try: - return json.loads(path.read_text("utf-8")) - except (FileNotFoundError, json.JSONDecodeError, OSError): - return None - - -def _read_config(journal_path: Path) -> dict[str, Any]: - payload = _read_json_file(journal_path / "config" / "journal.json") - return payload if isinstance(payload, dict) else {} - - -def _read_link_state(journal_path: Path) -> dict[str, Any] | None: - payload = _read_json_file(journal_path / "link" / "state.json") - return payload if isinstance(payload, dict) else None - - -def _read_service_token(journal_path: Path) -> str | None: - payload = _read_json_file(journal_path / "link" / "tokens" / "account.json") - if not isinstance(payload, dict): - return None - token = payload.get("service_token") or payload.get("account_token") - return token if isinstance(token, str) and token else None - - -def _read_hosted_binding(journal_path: Path) -> dict[str, Any] | None: - payload = _read_json_file(journal_path / "backup" / "hosted" / "binding.json") - return payload if isinstance(payload, dict) else None - - -def _ca_present(journal_path: Path) -> bool: - ca_root = journal_path / "link" / "ca" - return (ca_root / "cert.pem").exists() and (ca_root / "private.pem").exists() - - -def _posture(config: dict[str, Any]) -> str: - link = config.get("link") - if isinstance(link, dict) and link.get("posture") == "spl": - return "spl" - return "direct" - - -def _backup_config(config: dict[str, Any]) -> dict[str, Any]: - backup = config.get("backup") - return backup if isinstance(backup, dict) else {} - - -def _intent_capability( - intent_payload: dict[str, Any] | None, - name: str, -) -> dict[str, Any] | None: - if not isinstance(intent_payload, dict): - return None - caps = intent_payload.get("capabilities") - if not isinstance(caps, list): - return None - for item in caps: - if isinstance(item, dict) and item.get("name") == name: - return item - return None - - -def _cap_intent_state(intent_payload: dict[str, Any] | None, name: str) -> str | None: - item = _intent_capability(intent_payload, name) - state = item.get("intent_state") if isinstance(item, dict) else None - return state if isinstance(state, str) else None - - -def _observed_at_apply( - intent_payload: dict[str, Any] | None, name: str -) -> dict[str, Any]: - if not isinstance(intent_payload, dict): - return {} - observed = intent_payload.get("observed_at_apply") - if not isinstance(observed, dict): - return {} - block = observed.get(name) - return block if isinstance(block, dict) else {} - - -def _observed_string( - intent_payload: dict[str, Any] | None, name: str, field: str -) -> str | None: - value = _observed_at_apply(intent_payload, name).get(field) - return value if isinstance(value, str) and value else None - - -def _is_applied_intent(state: str | None) -> bool: - return state in {intent.INTENT_APPLIED, intent.INTENT_APPLY_STARTED} - - -def _is_disabled_intent(state: str | None) -> bool: - return state in {intent.INTENT_DISABLED, intent.INTENT_DISABLE_STARTED} - - -def _cap(name: str, state: str, *residuals: str) -> envelope.CapabilityEnvelope: - return envelope.CapabilityEnvelope(name, state, tuple(residuals)) - - -def _secret_sha256(value: str) -> str: - # scout.KEY_FINGERPRINT_FIELD is written from this same UTF-8 SHA-256 input. - return hashlib.sha256(value.encode("utf-8")).hexdigest() - - -def _validate_exact_fields( - payload: dict[str, Any], - fields: tuple[str, ...], - *, - capability: str, -) -> None: - expected = set(fields) - actual = set(payload) - if actual != expected: - raise PayloadValidationError(f"{capability} payload fields are unsupported") - - -def _non_blank_payload( - payload: dict[str, Any], fields: tuple[str, ...], capability: str -) -> None: - for field in fields: - value = payload.get(field) - if not isinstance(value, str) or not value: - raise PayloadValidationError( - f"{capability} payload field {field} is invalid" - ) - - -def _validate_scout_payload(payload: dict[str, Any]) -> None: - _validate_exact_fields(payload, _SCOUT_FIELDS, capability=manifest.CAPABILITY_SCOUT) - try: - scout._validate_handoff_payload(payload) - except ValueError: - raise PayloadValidationError("scout payload is invalid") from None - - -def _validate_spl_payload(payload: dict[str, Any]) -> None: - _validate_exact_fields(payload, _SPL_FIELDS, capability=manifest.CAPABILITY_SPL) - try: - state = _classify_spl_payload(payload) - except ValueError: - raise PayloadValidationError("spl payload is invalid") from None - if state != "approved": - raise PayloadValidationError("spl payload must be approved") - - -def _validate_spb_payload(payload: dict[str, Any], journal_path: Path) -> None: - _validate_exact_fields( - payload, tuple(_BINDING_FIELDS), capability=manifest.CAPABILITY_SPB - ) - _non_blank_payload(payload, tuple(_BINDING_FIELDS), manifest.CAPABILITY_SPB) - state = _read_link_state(journal_path) - instance_id = state.get("instance_id") if isinstance(state, dict) else None - if not isinstance(instance_id, str) or not instance_id: - raise PayloadValidationError("spb requires prepared runtime identity") - if payload.get("instance_id") != instance_id: - raise PayloadValidationError( - "spb payload field instance_id does not match runtime", - error_code="spb_instance_mismatch", - ) - try: - _binding_from_payload(payload) - except ValueError: - raise PayloadValidationError("spb payload is invalid") from None - - -def _validate_spp_payload(payload: dict[str, Any]) -> None: - _validate_exact_fields(payload, _SPP_FIELDS, capability=manifest.CAPABILITY_SPP) - try: - spp._validate_handoff_payload(payload) - except ValueError: - raise PayloadValidationError("spp payload is invalid") from None - - -def validate_payload( - capability: str, payload: dict[str, Any], journal_path: Path -) -> None: - if capability == manifest.CAPABILITY_SCOUT: - _validate_scout_payload(payload) - elif capability == manifest.CAPABILITY_SPL: - _validate_spl_payload(payload) - elif capability == manifest.CAPABILITY_SPB: - _validate_spb_payload(payload, journal_path) - elif capability == manifest.CAPABILITY_SPP: - _validate_spp_payload(payload) - else: - raise PayloadValidationError("unsupported capability") - - -def _runtime_status( - journal_path: Path, intent_payload: dict[str, Any] | None -) -> envelope.CapabilityEnvelope: - config = _read_config(journal_path) - setup = config.get("setup") - completed = setup.get("completed_at") if isinstance(setup, dict) else None - active = ( - isinstance(completed, (int, float)) - and not isinstance(completed, bool) - and completed > 0 - ) - link_state = _read_link_state(journal_path) - ready = active and isinstance(link_state, dict) and _ca_present(journal_path) - cap_state = _cap_intent_state(intent_payload, manifest.CAPABILITY_RUNTIME) - if ready: - return _cap(manifest.CAPABILITY_RUNTIME, envelope.CAP_READY) - if cap_state in {intent.INTENT_PREPARED, intent.INTENT_APPLY_STARTED}: - return _cap( - manifest.CAPABILITY_RUNTIME, envelope.CAP_DEGRADED, "apply_interrupted" - ) - return _cap(manifest.CAPABILITY_RUNTIME, envelope.CAP_NOT_APPLIED) - - -def _scout_status( - config: dict[str, Any], intent_payload: dict[str, Any] | None -) -> envelope.CapabilityEnvelope: - state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SCOUT) - block = config.get("services", {}).get("scout") - applied = isinstance(block, dict) and block.get("state") != "pending" - key = config.get("env", {}).get("GOOGLE_API_KEY") - has_key = isinstance(key, str) and bool(key) - complete = applied and has_key - if complete and state is None: - return _cap( - manifest.CAPABILITY_SCOUT, envelope.CAP_DEGRADED, "unmanaged_existing_state" - ) - if complete and state == intent.INTENT_APPLY_STARTED: - return _cap( - manifest.CAPABILITY_SCOUT, envelope.CAP_DEGRADED, "intent_finalize_missing" - ) - if complete: - live_fingerprint = _secret_sha256(key) - stored_fingerprint = block.get(scout.KEY_FINGERPRINT_FIELD) - recorded_fingerprint = _observed_string( - intent_payload, manifest.CAPABILITY_SCOUT, scout.KEY_FINGERPRINT_FIELD - ) - expected = recorded_fingerprint or ( - stored_fingerprint if isinstance(stored_fingerprint, str) else None - ) - if expected is not None and ( - live_fingerprint != expected or stored_fingerprint != expected - ): - return _cap( - manifest.CAPABILITY_SCOUT, - envelope.CAP_DEGRADED, - "scout_key_fingerprint_mismatch", - ) - live_account_id = block.get("account_id") - recorded_account_id = _observed_string( - intent_payload, manifest.CAPABILITY_SCOUT, "account_id" - ) - if ( - not isinstance(live_account_id, str) - or not live_account_id - or recorded_account_id is None - or live_account_id != recorded_account_id - ): - return _cap( - manifest.CAPABILITY_SCOUT, - envelope.CAP_DEGRADED, - "scout_account_id_mismatch", - ) - return _cap(manifest.CAPABILITY_SCOUT, envelope.CAP_READY) - if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: - return _cap(manifest.CAPABILITY_SCOUT, envelope.CAP_NOT_APPLIED) - if _is_applied_intent(state): - return _cap( - manifest.CAPABILITY_SCOUT, envelope.CAP_DEGRADED, "scout_block_missing" - ) - return _cap(manifest.CAPABILITY_SCOUT, envelope.CAP_NOT_APPLIED) - - -def _spl_status( - journal_path: Path, - config: dict[str, Any], - intent_payload: dict[str, Any] | None, -) -> envelope.CapabilityEnvelope: - state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SPL) - posture = _posture(config) - link_state = _read_link_state(journal_path) - token = _read_service_token(journal_path) - complete = posture == "spl" and token is not None - if complete and state is None: - return _cap( - manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "unmanaged_existing_state" - ) - if complete and state == intent.INTENT_APPLY_STARTED: - return _cap( - manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "intent_finalize_missing" - ) - if complete: - recorded_instance_id = _observed_string( - intent_payload, manifest.CAPABILITY_SPL, "instance_id" - ) - live_instance_id = ( - link_state.get("instance_id") if isinstance(link_state, dict) else None - ) - if ( - recorded_instance_id is not None - and live_instance_id != recorded_instance_id - ): - return _cap( - manifest.CAPABILITY_SPL, - envelope.CAP_DEGRADED, - "spl_identity_missing", - ) - return _cap(manifest.CAPABILITY_SPL, envelope.CAP_READY) - if posture == "spl" and token is None: - return _cap(manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "spl_token_missing") - if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: - return _cap(manifest.CAPABILITY_SPL, envelope.CAP_NOT_APPLIED) - if _is_applied_intent(state): - return _cap( - manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "spl_posture_not_spl" - ) - return _cap(manifest.CAPABILITY_SPL, envelope.CAP_NOT_APPLIED) - - -def _spb_status( - journal_path: Path, - config: dict[str, Any], - intent_payload: dict[str, Any] | None, -) -> envelope.CapabilityEnvelope: - state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SPB) - binding = _read_hosted_binding(journal_path) - backup = _backup_config(config) - complete = ( - isinstance(binding, dict) - and backup.get("mode") == "operated" - and backup.get("enabled") is True - ) - if complete and state is None: - return _cap( - manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "unmanaged_existing_state" - ) - if complete and state == intent.INTENT_APPLY_STARTED: - return _cap( - manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "intent_finalize_missing" - ) - recorded_instance_id = _observed_string( - intent_payload, manifest.CAPABILITY_SPB, "instance_id" - ) - live_instance_id = binding.get("instance_id") if isinstance(binding, dict) else None - if ( - binding is not None - and recorded_instance_id is not None - and live_instance_id != recorded_instance_id - ): - return _cap( - manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "spb_instance_mismatch" - ) - if complete: - return _cap(manifest.CAPABILITY_SPB, envelope.CAP_READY) - if binding is None and _is_applied_intent(state): - return _cap( - manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "spb_binding_missing" - ) - if ( - binding is not None - or backup.get("mode") == "operated" - or backup.get("enabled") is True - ): - return _cap( - manifest.CAPABILITY_SPB, - envelope.CAP_DEGRADED, - "spb_backup_config_incomplete", - ) - if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: - return _cap(manifest.CAPABILITY_SPB, envelope.CAP_NOT_APPLIED) - return _cap(manifest.CAPABILITY_SPB, envelope.CAP_NOT_APPLIED) - - -def _spp_status( - config: dict[str, Any], intent_payload: dict[str, Any] | None -) -> envelope.CapabilityEnvelope: - state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SPP) - block = confidential_fingerprint_provenance_block(config) - local = config.get("providers", {}).get("local", {}) - credential = local.get("credential") if isinstance(local, dict) else None - complete = isinstance(block, dict) and bool(credential) - if complete and state is None: - return _cap( - manifest.CAPABILITY_SPP, envelope.CAP_DEGRADED, "unmanaged_existing_state" - ) - if complete and state == intent.INTENT_APPLY_STARTED: - return _cap( - manifest.CAPABILITY_SPP, envelope.CAP_DEGRADED, "intent_finalize_missing" - ) - if complete: - recorded_fingerprint = _observed_string( - intent_payload, - manifest.CAPABILITY_SPP, - spp.CREDENTIAL_FINGERPRINT_FIELD, - ) - live_fingerprint = block.get(spp.CREDENTIAL_FINGERPRINT_FIELD) - if ( - recorded_fingerprint is not None - and live_fingerprint != recorded_fingerprint - ): - return _cap( - manifest.CAPABILITY_SPP, - envelope.CAP_DEGRADED, - "spp_credential_fingerprint_mismatch", - ) - return _cap(manifest.CAPABILITY_SPP, envelope.CAP_READY) - if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: - return _cap(manifest.CAPABILITY_SPP, envelope.CAP_NOT_APPLIED) - if _is_applied_intent(state): - return _cap(manifest.CAPABILITY_SPP, envelope.CAP_DEGRADED, "spp_block_missing") - return _cap(manifest.CAPABILITY_SPP, envelope.CAP_NOT_APPLIED) - - -def observe_capabilities( - journal_path: str | Path, - intent_payload: dict[str, Any] | None = None, -) -> tuple[envelope.CapabilityEnvelope, ...]: - journal = Path(journal_path) - config = _read_config(journal) - by_name = { - manifest.CAPABILITY_SCOUT: _scout_status(config, intent_payload), - manifest.CAPABILITY_SPL: _spl_status(journal, config, intent_payload), - manifest.CAPABILITY_SPB: _spb_status(journal, config, intent_payload), - manifest.CAPABILITY_SPP: _spp_status(config, intent_payload), - manifest.CAPABILITY_RUNTIME: _runtime_status(journal, intent_payload), - } - return tuple(by_name[name] for name in manifest.CAPABILITY_ORDER) - - -def top_state(capabilities: tuple[envelope.CapabilityEnvelope, ...]) -> str: - if any(cap.state == envelope.CAP_CLEANUP_FAILED for cap in capabilities): - return envelope.TOP_CLEANUP_FAILED - if any(cap.state == envelope.CAP_DEGRADED for cap in capabilities): - return envelope.TOP_DEGRADED - return envelope.TOP_OK - - -def prepare_runtime( - journal_path: Path, run_id: str -) -> tuple[envelope.CapabilityEnvelope, ...]: - intent.ensure_prepared(journal_path, run_id) - owner = manifest.synthetic_owner_metadata(run_id) - - def apply(config: dict[str, Any]) -> JournalConfigMutation[None]: - changed = False - setup = config.setdefault("setup", {}) - if setup.get("completed_at") != owner.setup_completed_at: - setup["completed_at"] = owner.setup_completed_at - changed = True - identity = config.setdefault("identity", {}) - for key, value in { - "name": owner.identity_name, - "preferred": owner.identity_preferred, - "timezone": owner.identity_timezone, - }.items(): - if identity.get(key) != value: - identity[key] = value - changed = True - journal = config.setdefault("journal", {}) - if journal.get("name") != owner.journal_name: - journal["name"] = owner.journal_name - changed = True - return JournalConfigMutation(changed=changed, value=None) - - mutate_journal_config(apply, journal_path=journal_path) - LinkState.load_or_create(default_label=owner.home_label) - load_or_generate_ca(ca_dir()) - return observe_capabilities( - journal_path, intent.require_intent(journal_path, run_id) - ) - - -def apply_capability( - journal_path: Path, - run_id: str, - capability: str, - payload: dict[str, Any], -) -> tuple[envelope.CapabilityEnvelope, ...]: - validate_payload(capability, payload, journal_path) - intent.require_intent(journal_path, run_id) - intent.update_capability( - journal_path, - run_id, - capability, - state=intent.INTENT_APPLY_STARTED, - ) - if capability == manifest.CAPABILITY_SCOUT: - scout.provision_scout_handoff(payload) - observed = _read_config(journal_path).get("services", {}).get("scout", {}) - intent.update_capability( - journal_path, - run_id, - capability, - state=intent.INTENT_APPLIED, - observed={ - "key_fingerprint_sha256": observed.get("key_fingerprint_sha256") - if isinstance(observed, dict) - else None, - "account_id": observed.get("account_id") - if isinstance(observed, dict) - else None, - }, - ) - elif capability == manifest.CAPABILITY_SPL: - spl.enable_spl() - state = _read_link_state(journal_path) or {} - intent.update_capability( - journal_path, - run_id, - capability, - state=intent.INTENT_APPLIED, - observed={"instance_id": state.get("instance_id")}, - ) - elif capability == manifest.CAPABILITY_SPB: - binding = _binding_from_payload(payload) - backup_state.generate_and_store_keys() - backup_state.set_recovery_key_confirmed(True) - save_hosted_binding(binding) - backup_state.set_mode("operated") - backup_state.set_enabled(True) - intent.update_capability( - journal_path, - run_id, - capability, - state=intent.INTENT_APPLIED, - observed={ - "instance_id": binding.instance_id, - "bucket": binding.bucket, - "prefix": binding.prefix, - }, - ) - elif capability == manifest.CAPABILITY_SPP: - spp.provision_confidential_handoff(payload) - observed = ( - _read_config(journal_path).get("services", {}).get("confidential", {}) - ) - intent.update_capability( - journal_path, - run_id, - capability, - state=intent.INTENT_APPLIED, - observed={ - "credential_fingerprint_sha256": observed.get( - "credential_fingerprint_sha256" - ) - if isinstance(observed, dict) - else None - }, - ) - else: - raise PayloadValidationError("unsupported capability") - return observe_capabilities( - journal_path, intent.require_intent(journal_path, run_id) - ) - - -def _disable_spb() -> None: - backup_state.clear_backup_config() - delete_hosted_binding() - - -def _disable_preexisting_residuals( - name: str, - prior: envelope.CapabilityEnvelope | None, -) -> list[str]: - if prior is None or prior.state != envelope.CAP_DEGRADED: - return [] - carried = _DISABLE_CARRIED_RESIDUALS.get(name, frozenset()) - return [residual for residual in prior.residuals if residual in carried] - - -def _record_disable_exception( - name: str, - exc: Exception, - residuals: list[str], -) -> None: - if isinstance(exc, JournalConfigPostCommitError): - residual = "post_commit_failed" - elif isinstance(exc, OSError): - residual = "local_artifact_io_failed" - else: - residual = "missing_expected_artifact" - logger.exception( - "sandbox profile disable failed for capability=%s exception_type=%s", - name, - type(exc).__name__, - ) - residuals.append(residual) - - -def disable_capabilities( - journal_path: Path, - run_id: str, - only: str | None = None, -) -> tuple[envelope.CapabilityEnvelope, ...]: - current_intent = intent.require_intent(journal_path, run_id) - before = { - cap.name: cap for cap in observe_capabilities(journal_path, current_intent) - } - names = ( - (only,) - if only is not None - else ( - manifest.CAPABILITY_SPP, - manifest.CAPABILITY_SPB, - manifest.CAPABILITY_SPL, - manifest.CAPABILITY_SCOUT, - ) - ) - residuals_by_cap: dict[str, list[str]] = {name: [] for name in names} - for name in names: - residuals_by_cap[name].extend( - _disable_preexisting_residuals(name, before.get(name)) - ) - intent.update_capability( - journal_path, - run_id, - name, - state=intent.INTENT_DISABLE_STARTED, - ) - try: - if name == manifest.CAPABILITY_SPP: - outcome = spp.disable_confidential() - if outcome.credential_preserved: - residuals_by_cap[name].append("spp_credential_ownership_conflict") - elif name == manifest.CAPABILITY_SPB: - _disable_spb() - elif name == manifest.CAPABILITY_SPL: - spl.disable_spl() - elif name == manifest.CAPABILITY_SCOUT: - outcome = scout.disable_scout() - if outcome.env_key_preserved: - residuals_by_cap[name].append("unrelated_manual_key_preserved") - elif name == manifest.CAPABILITY_RUNTIME: - continue - except Exception as exc: - _record_disable_exception(name, exc, residuals_by_cap[name]) - state = ( - intent.INTENT_FAILED - if any( - residual != "unrelated_manual_key_preserved" - for residual in residuals_by_cap[name] - ) - else intent.INTENT_DISABLED - ) - intent.update_capability( - journal_path, - run_id, - name, - state=state, - residuals=tuple(residuals_by_cap[name]), - ) - - observed = observe_capabilities( - journal_path, intent.require_intent(journal_path, run_id) - ) - adjusted = [] - for cap in observed: - residuals = list(cap.residuals) - residuals.extend(residuals_by_cap.get(cap.name, [])) - residuals = list(dict.fromkeys(residuals)) - if cap.name in residuals_by_cap: - if cap.state == envelope.CAP_READY and not residuals: - residuals.append("cleanup_still_applied") - if residuals: - state = cap.state - if residuals != ["unrelated_manual_key_preserved"]: - state = envelope.CAP_CLEANUP_FAILED - adjusted.append( - envelope.CapabilityEnvelope(cap.name, state, tuple(residuals)) - ) - continue - adjusted.append(cap) - else: - adjusted.append(cap) - result = tuple(adjusted) - if all( - cap.state == envelope.CAP_NOT_APPLIED - for cap in result - if cap.name != manifest.CAPABILITY_RUNTIME - ): - intent.mark_disabled_if_complete(journal_path, run_id) - return result diff --git a/solstone/think/sandbox_profile/cli.py b/solstone/think/sandbox_profile/cli.py deleted file mode 100644 index 1a77dc239..000000000 --- a/solstone/think/sandbox_profile/cli.py +++ /dev/null @@ -1,589 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""CLI for the disposable sandbox profile lifecycle. - -``describe`` and ``status`` are read-only verbs and must not reach any helper -that can materialize journal state. ``prepare``, ``apply``, and ``disable`` are -polarity ``other`` rather than write-verb commands; the disposable sandbox -marker is their safety gate, so L5's mechanical ``--commit`` default does not -bind this host-only lifecycle wrapper. -""" - -from __future__ import annotations - -import json -import logging -import sys -from pathlib import Path -from typing import Any, NoReturn - -import typer - -from solstone.think import json_codec -from solstone.think.sandbox_profile import ( - capabilities, - envelope, - intent, - manifest, - marker, -) -from solstone.think.utils import get_journal_info - -app = typer.Typer( - help="Manage disposable sandbox service profile.", no_args_is_help=True -) -log = logging.getLogger(__name__) - -MAX_STDIN_BYTES = 64 * 1024 -PRODUCTION_RECONCILER_ACTION = ( - "Relay instance retirement, portal token and binding revocation, and " - "production storage purge are not performed here; the production-side " - "reconciler owns them." -) - - -class PayloadReadError(ValueError): - def __init__(self, message: str) -> None: - super().__init__(message) - self.message = message - - -def _configure_logging(verbose: bool, debug: bool) -> None: - if debug: - level = logging.DEBUG - elif verbose: - level = logging.INFO - else: - level = logging.WARNING - logging.basicConfig(level=level) - - -def _supported_contract_action() -> tuple[str, ...]: - return ( - "Supported sandbox profile contract: " - f"profile={manifest.PROFILE} " - f"contract_version={manifest.CONTRACT_VERSION} " - f"capabilities={','.join(manifest.CAPABILITY_ORDER)}.", - ) - - -def _supported_apply_action() -> tuple[str, ...]: - return (f"Supported apply capabilities: {', '.join(manifest.APPLY_CAPABILITIES)}.",) - - -def _supported_disable_action() -> tuple[str, ...]: - return ( - f"Supported disable capabilities: all, {', '.join(manifest.APPLY_CAPABILITIES)}.", - ) - - -def _emit(result: envelope.Envelope, *, json_output: bool) -> NoReturn: - body = ( - envelope.render_json(result) - if json_output - else envelope.summarize_human(result) - ) - typer.echo(body, nl=False) - raise typer.Exit(result.exit_code) - - -def _error( - *, - action: str, - code: str, - message: str, - run_id: str | None = None, - next_actions: tuple[str, ...] = (), -) -> envelope.Envelope: - return envelope.error_envelope( - action=action, - code=code, - message=message, - run_id=run_id, - next_actions=next_actions, - ) - - -def _resolved_journal_path() -> Path: - path, _source = get_journal_info() - return marker.canonical_path(path) - - -def _marker_context(action: str) -> marker.MarkerContext | envelope.Envelope: - journal_path = _resolved_journal_path() - try: - return marker.validate_marker(journal_path) - except marker.MarkerError as exc: - return _error( - action=action, - code=exc.code, - message=exc.message, - run_id=None, - next_actions=_supported_contract_action() - if exc.code - in { - "sandbox_marker_wrong_contract_version", - "sandbox_marker_wrong_profile", - } - else (), - ) - - -def _load_intent_for_context( - ctx: marker.MarkerContext, - *, - require: bool, -) -> dict[str, Any] | None: - if require: - return intent.require_intent(ctx.journal_path, ctx.run_id) - payload = intent.load_intent(ctx.journal_path) - if payload is None: - return None - return intent.require_intent(ctx.journal_path, ctx.run_id) - - -def _capability_status( - action: str, - ctx: marker.MarkerContext, - *, - require_intent: bool, -) -> envelope.Envelope: - try: - intent_payload = _load_intent_for_context(ctx, require=require_intent) - except FileNotFoundError: - return _error( - action=action, code="intent_missing", message="prepare is required first" - ) - except intent.IntentRunMismatch: - return _error( - action=action, - code="intent_run_mismatch", - message="sandbox profile intent belongs to a different run", - run_id=ctx.run_id, - ) - except intent.IntentError: - return _error( - action=action, - code="intent_malformed", - message="sandbox profile intent is malformed", - run_id=ctx.run_id, - ) - caps = capabilities.observe_capabilities(ctx.journal_path, intent_payload) - return envelope.Envelope( - action=action, - profile=ctx.profile, - run_id=ctx.run_id, - state=capabilities.top_state(caps), - capabilities=caps, - ) - - -def _read_payload() -> dict[str, Any]: - stream = getattr(sys.stdin, "buffer", None) - if stream is None: - raw = sys.stdin.read(MAX_STDIN_BYTES + 1).encode("utf-8", "surrogateescape") - else: - raw = stream.read(MAX_STDIN_BYTES + 1) - if len(raw) > MAX_STDIN_BYTES: - raise PayloadReadError("payload exceeds 64 KiB") - try: - text = raw.decode("utf-8") - stripped = text.lstrip() - decoder = json.JSONDecoder(object_pairs_hook=json_codec.reject_duplicate_keys) - payload, end = decoder.raw_decode(stripped) - except (UnicodeDecodeError, ValueError, json.JSONDecodeError): - raise PayloadReadError("payload must be one valid JSON object") from None - if text[len(text) - len(stripped) + end :].strip(): - raise PayloadReadError("payload must not contain trailing content") - if not isinstance(payload, dict): - raise PayloadReadError("payload must be a JSON object") - return payload - - -def _finalize_action( - *, - action: str, - ctx: marker.MarkerContext, - caps: tuple[envelope.CapabilityEnvelope, ...], - next_actions: tuple[str, ...] = (), -) -> envelope.Envelope: - return envelope.Envelope( - action=action, - profile=ctx.profile, - run_id=ctx.run_id, - state=capabilities.top_state(caps), - capabilities=caps, - next_actions=next_actions, - ) - - -def _cleanup_next_actions( - caps: tuple[envelope.CapabilityEnvelope, ...], -) -> tuple[str, ...]: - actions = [PRODUCTION_RECONCILER_ACTION] - residuals = {residual for cap in caps for residual in cap.residuals} - if "spp_credential_ownership_conflict" in residuals: - actions.append( - "Repair SPP credential ownership before treating the sandbox as clean." - ) - if "spb_binding_missing" in residuals: - actions.append( - "Restore the hosted backup binding or create a fresh sandbox before retrying cleanup." - ) - if "missing_expected_artifact" in residuals: - actions.append( - "Inspect the named local artifact and retry disable after restoring or removing it." - ) - return tuple(actions) - - -@app.command("describe") -def describe( - json_output: bool = typer.Option( - True, - "--json/--human", - help="Emit machine JSON or redacted human text.", - ), - profile: str = typer.Option(manifest.PROFILE, "--profile", help="Profile name."), - contract_version: int = typer.Option( - manifest.CONTRACT_VERSION, - "--contract-version", - help="Sandbox profile contract version.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Enable verbose logging." - ), - debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging."), -) -> None: - _configure_logging(verbose, debug) - action = "describe" - if profile != manifest.PROFILE: - _emit( - _error( - action=action, - code="sandbox_marker_wrong_profile", - message="profile is unsupported", - next_actions=_supported_contract_action(), - ), - json_output=json_output, - ) - if contract_version != manifest.CONTRACT_VERSION: - _emit( - _error( - action=action, - code="sandbox_marker_wrong_contract_version", - message="contract_version is unsupported", - next_actions=_supported_contract_action(), - ), - json_output=json_output, - ) - result = envelope.Envelope( - action=action, - profile=manifest.PROFILE, - run_id=None, - state=envelope.TOP_OK, - capabilities=envelope.empty_capabilities(), - next_actions=_supported_contract_action(), - ) - _emit(result, json_output=json_output) - - -@app.command("prepare") -def prepare( - json_output: bool = typer.Option( - True, - "--json/--human", - help="Emit machine JSON or redacted human text.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Enable verbose logging." - ), - debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging."), -) -> None: - _configure_logging(verbose, debug) - action = "prepare" - ctx_or_error = _marker_context(action) - if isinstance(ctx_or_error, envelope.Envelope): - _emit(ctx_or_error, json_output=json_output) - ctx = ctx_or_error - try: - caps = capabilities.prepare_runtime(ctx.journal_path, ctx.run_id) - except intent.IntentRunMismatch: - _emit( - _error( - action=action, - code="intent_run_mismatch", - message="sandbox profile intent belongs to a different run", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except intent.IntentError: - _emit( - _error( - action=action, - code="intent_malformed", - message="sandbox profile intent is malformed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except Exception: - log.debug("sandbox profile prepare failed without payload details") - _emit( - _error( - action=action, - code="internal_error", - message="prepare failed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - _emit(_finalize_action(action=action, ctx=ctx, caps=caps), json_output=json_output) - - -@app.command("apply") -def apply( - capability: str = typer.Argument(..., help="Capability to apply."), - json_output: bool = typer.Option( - True, - "--json/--human", - help="Emit machine JSON or redacted human text.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Enable verbose logging." - ), - debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging."), -) -> None: - _configure_logging(verbose, debug) - action = "apply" - ctx_or_error = _marker_context(action) - if isinstance(ctx_or_error, envelope.Envelope): - _emit(ctx_or_error, json_output=json_output) - ctx = ctx_or_error - if capability not in manifest.CAPABILITY_ORDER: - _emit( - _error( - action=action, - code="unknown_capability", - message="capability is unknown", - run_id=ctx.run_id, - next_actions=_supported_apply_action(), - ), - json_output=json_output, - ) - if capability == manifest.CAPABILITY_RUNTIME: - _emit( - _error( - action=action, - code="unsupported_capability_action", - message="runtime is prepare-only", - run_id=ctx.run_id, - next_actions=_supported_apply_action(), - ), - json_output=json_output, - ) - try: - intent.require_intent(ctx.journal_path, ctx.run_id) - except FileNotFoundError: - _emit( - _error( - action=action, - code="intent_missing", - message="prepare is required first", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except intent.IntentRunMismatch: - _emit( - _error( - action=action, - code="intent_run_mismatch", - message="sandbox profile intent belongs to a different run", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except intent.IntentError: - _emit( - _error( - action=action, - code="intent_malformed", - message="sandbox profile intent is malformed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - try: - payload = _read_payload() - caps = capabilities.apply_capability( - ctx.journal_path, - ctx.run_id, - capability, - payload, - ) - except PayloadReadError: - _emit( - _error( - action=action, - code="payload_invalid", - message="payload is invalid", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except capabilities.PayloadValidationError as exc: - _emit( - _error( - action=action, - code=exc.error_code, - message=exc.message, - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except Exception: - log.debug("sandbox profile apply failed without payload details") - _emit( - _error( - action=action, - code="internal_error", - message="apply failed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - _emit(_finalize_action(action=action, ctx=ctx, caps=caps), json_output=json_output) - - -@app.command("status") -def status( - json_output: bool = typer.Option( - True, - "--json/--human", - help="Emit machine JSON or redacted human text.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Enable verbose logging." - ), - debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging."), -) -> None: - _configure_logging(verbose, debug) - action = "status" - ctx_or_error = _marker_context(action) - if isinstance(ctx_or_error, envelope.Envelope): - _emit(ctx_or_error, json_output=json_output) - _emit( - _capability_status(action, ctx_or_error, require_intent=False), - json_output=json_output, - ) - - -@app.command("disable") -def disable( - capability: str | None = typer.Argument( - None, help="Optional capability to disable." - ), - json_output: bool = typer.Option( - True, - "--json/--human", - help="Emit machine JSON or redacted human text.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Enable verbose logging." - ), - debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging."), -) -> None: - _configure_logging(verbose, debug) - action = "disable" - ctx_or_error = _marker_context(action) - if isinstance(ctx_or_error, envelope.Envelope): - _emit(ctx_or_error, json_output=json_output) - ctx = ctx_or_error - if capability is not None: - if capability not in manifest.CAPABILITY_ORDER: - _emit( - _error( - action=action, - code="unknown_capability", - message="capability is unknown", - run_id=ctx.run_id, - next_actions=_supported_disable_action(), - ), - json_output=json_output, - ) - if capability == manifest.CAPABILITY_RUNTIME: - _emit( - _error( - action=action, - code="unsupported_capability_action", - message="runtime state is owned by the sandbox harness", - run_id=ctx.run_id, - next_actions=_supported_disable_action(), - ), - json_output=json_output, - ) - try: - caps = capabilities.disable_capabilities( - ctx.journal_path, ctx.run_id, capability - ) - except FileNotFoundError: - _emit( - _error( - action=action, - code="intent_missing", - message="prepare is required first", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except intent.IntentRunMismatch: - _emit( - _error( - action=action, - code="intent_run_mismatch", - message="sandbox profile intent belongs to a different run", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except intent.IntentError: - _emit( - _error( - action=action, - code="intent_malformed", - message="sandbox profile intent is malformed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - except Exception: - log.debug("sandbox profile disable failed without payload details") - _emit( - _error( - action=action, - code="internal_error", - message="disable failed", - run_id=ctx.run_id, - ), - json_output=json_output, - ) - _emit( - _finalize_action( - action=action, - ctx=ctx, - caps=caps, - next_actions=_cleanup_next_actions(caps), - ), - json_output=json_output, - ) - - -def main() -> None: - app() - - -if __name__ == "__main__": - main() diff --git a/solstone/think/sandbox_profile/envelope.py b/solstone/think/sandbox_profile/envelope.py deleted file mode 100644 index c8d13e3e3..000000000 --- a/solstone/think/sandbox_profile/envelope.py +++ /dev/null @@ -1,225 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Closed response envelope for ``journal sandbox-profile``.""" - -from __future__ import annotations - -import json -from dataclasses import dataclass, field - -from solstone.think.sandbox_profile import manifest - -TOP_OK = "ok" -TOP_DEGRADED = "degraded" -TOP_ERROR = "error" -TOP_CLEANUP_FAILED = "cleanup_failed" - -TOP_STATES = frozenset({TOP_OK, TOP_DEGRADED, TOP_ERROR, TOP_CLEANUP_FAILED}) -EXIT_COMPLETE = 0 -EXIT_INTERNAL_FAILURE = 1 -EXIT_REFUSED = 2 -EXIT_RESIDUAL = 3 - -CAP_NOT_APPLIED = "not_applied" -CAP_READY = "ready" -CAP_DEGRADED = "degraded" -CAP_CLEANUP_FAILED = "cleanup_failed" - -CAPABILITY_STATES = frozenset( - {CAP_NOT_APPLIED, CAP_READY, CAP_DEGRADED, CAP_CLEANUP_FAILED} -) - -RESIDUAL_CODES = frozenset( - { - "apply_interrupted", - "intent_finalize_missing", - "unmanaged_existing_state", - "scout_block_missing", - "scout_account_id_mismatch", - "scout_key_fingerprint_mismatch", - "unrelated_manual_key_preserved", - "spl_identity_missing", - "spl_token_missing", - "spl_posture_not_spl", - "spb_binding_missing", - "spb_instance_mismatch", - "spb_backup_config_incomplete", - "spp_block_missing", - "spp_credential_fingerprint_mismatch", - "spp_credential_ownership_conflict", - "cleanup_still_applied", - "local_artifact_io_failed", - "post_commit_failed", - "missing_expected_artifact", - } -) - -ERROR_CODES = frozenset( - { - "sandbox_marker_missing", - "sandbox_marker_symlink", - "sandbox_marker_not_regular", - "sandbox_marker_unparseable", - "sandbox_marker_non_object", - "sandbox_marker_wrong_kind", - "sandbox_marker_wrong_contract_version", - "sandbox_marker_wrong_profile", - "sandbox_marker_bad_run_id", - "sandbox_marker_path_mismatch", - "intent_missing", - "intent_malformed", - "intent_run_mismatch", - "payload_invalid", - "spb_instance_mismatch", - "unknown_capability", - "unsupported_capability_action", - "internal_error", - } -) - -GUIDANCE: dict[str, str | None] = { - "sandbox_marker_missing": "Create a fresh disposable sandbox marker, then retry.", - "sandbox_marker_symlink": "Replace the marker with a regular JSON file.", - "sandbox_marker_not_regular": "Replace the marker with a regular JSON file.", - "sandbox_marker_unparseable": "Rewrite the marker as valid JSON.", - "sandbox_marker_non_object": "Rewrite the marker as a JSON object.", - "sandbox_marker_wrong_kind": "Use kind 'solstone-disposable-journal'.", - "sandbox_marker_wrong_contract_version": "Use the supported sandbox profile contract.", - "sandbox_marker_wrong_profile": "Use the supported sandbox profile contract.", - "sandbox_marker_bad_run_id": "Use a canonical UUID run_id.", - "sandbox_marker_path_mismatch": "Point SOLSTONE_JOURNAL at the marker's canonical journal path.", - "intent_missing": "Run prepare first.", - "intent_malformed": "Use the owning run or create a fresh sandbox.", - "intent_run_mismatch": "Use the owning run or create a fresh sandbox.", - "payload_invalid": "Send one valid JSON object on stdin for the selected capability.", - "spb_instance_mismatch": "Send a hosted backup binding for the prepared runtime instance_id.", - "unknown_capability": "Use one of the supported capabilities.", - "unsupported_capability_action": "Use one of the supported capabilities for this action.", - "internal_error": "Inspect logs and retry in a fresh sandbox.", -} - - -@dataclass(frozen=True, slots=True) -class CapabilityEnvelope: - name: str - state: str - residuals: tuple[str, ...] = () - - def to_json(self) -> dict[str, object]: - if self.name not in manifest.CAPABILITY_ORDER: - raise ValueError(f"unsupported capability name: {self.name!r}") - if self.state not in CAPABILITY_STATES: - raise ValueError(f"unsupported capability state: {self.state!r}") - for residual in self.residuals: - if residual not in RESIDUAL_CODES: - raise ValueError(f"unsupported residual code: {residual!r}") - return { - "name": self.name, - "state": self.state, - "residuals": list(self.residuals), - } - - -@dataclass(frozen=True, slots=True) -class ErrorEnvelope: - code: str - message: str - - def to_json(self) -> dict[str, str]: - if self.code not in ERROR_CODES: - raise ValueError(f"unsupported error code: {self.code!r}") - return {"code": self.code, "message": self.message} - - -@dataclass(frozen=True, slots=True) -class Envelope: - action: str - profile: str - run_id: str | None - state: str - capabilities: tuple[CapabilityEnvelope, ...] - next_actions: tuple[str, ...] = () - error: ErrorEnvelope | None = None - contract_version: int = field(default=manifest.CONTRACT_VERSION) - - def to_json(self) -> dict[str, object]: - if self.state not in TOP_STATES: - raise ValueError(f"unsupported top-level state: {self.state!r}") - return { - "contract_version": self.contract_version, - "action": self.action, - "profile": self.profile, - "run_id": self.run_id, - "state": self.state, - "capabilities": [cap.to_json() for cap in self.capabilities], - "next_actions": list(self.next_actions), - "error": None if self.error is None else self.error.to_json(), - } - - @property - def exit_code(self) -> int: - if self.state == TOP_OK: - return EXIT_COMPLETE - if self.state == TOP_ERROR: - if self.error is not None and self.error.code == "internal_error": - return EXIT_INTERNAL_FAILURE - return EXIT_REFUSED - return EXIT_RESIDUAL - - -def empty_capabilities() -> tuple[CapabilityEnvelope, ...]: - return tuple( - CapabilityEnvelope(name, CAP_NOT_APPLIED) for name in manifest.CAPABILITY_ORDER - ) - - -def error_envelope( - *, - action: str, - code: str, - message: str, - run_id: str | None, - next_actions: tuple[str, ...] = (), -) -> Envelope: - if not next_actions: - guidance = GUIDANCE.get(code) - next_actions = (guidance,) if guidance else () - return Envelope( - action=action, - profile=manifest.PROFILE, - run_id=run_id, - state=TOP_ERROR, - capabilities=empty_capabilities(), - next_actions=next_actions, - error=ErrorEnvelope(code, message), - ) - - -def render_json(envelope: Envelope) -> str: - return json.dumps(envelope.to_json(), indent=2) + "\n" - - -def summarize_human(envelope: Envelope) -> str: - payload = envelope.to_json() - lines = [ - f"action: {payload['action']}", - f"profile: {payload['profile']}", - f"run_id: {payload['run_id'] or ''}", - f"state: {payload['state']}", - ] - for capability in payload["capabilities"]: - if not isinstance(capability, dict): - continue - residuals = capability.get("residuals") or [] - suffix = f" residuals={','.join(residuals)}" if residuals else "" - lines.append(f"- {capability.get('name')}: {capability.get('state')}{suffix}") - if payload["error"]: - error = payload["error"] - if isinstance(error, dict): - lines.append(f"error: {error.get('code')}: {error.get('message')}") - next_actions = payload["next_actions"] - if next_actions: - lines.append("next_actions:") - lines.extend(f"- {item}" for item in next_actions) - return "\n".join(lines) + "\n" diff --git a/solstone/think/sandbox_profile/intent.py b/solstone/think/sandbox_profile/intent.py deleted file mode 100644 index 1818cba21..000000000 --- a/solstone/think/sandbox_profile/intent.py +++ /dev/null @@ -1,186 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Sole writer for sandbox profile lifecycle intent.""" - -from __future__ import annotations - -import copy -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from solstone.think.journal_io import write_json -from solstone.think.sandbox_profile import manifest - -INTENT_REL = Path("health") / "sandbox-profile" / "intent.json" - -INTENT_PREPARED = "prepared" -INTENT_APPLY_STARTED = "apply_started" -INTENT_APPLIED = "applied" -INTENT_DISABLE_STARTED = "disable_started" -INTENT_DISABLED = "disabled" -INTENT_FAILED = "failed" - - -class IntentError(RuntimeError): - pass - - -class IntentRunMismatch(IntentError): - def __init__(self, existing_run_id: str) -> None: - super().__init__("sandbox profile intent belongs to a different run") - self.existing_run_id = existing_run_id - - -def intent_path(journal_path: str | Path) -> Path: - return Path(journal_path) / INTENT_REL - - -def _now_iso() -> str: - return datetime.now(timezone.utc).isoformat() - - -def _base_capability(name: str) -> dict[str, Any]: - return { - "name": name, - "intent_state": INTENT_PREPARED, - "prepared_at": None, - "apply_started_at": None, - "applied_at": None, - "disable_started_at": None, - "disabled_at": None, - "residuals": [], - } - - -def _default_intent(run_id: str) -> dict[str, Any]: - now = _now_iso() - capabilities = [] - for name in manifest.CAPABILITY_ORDER: - item = _base_capability(name) - item["prepared_at"] = now - capabilities.append(item) - return { - "kind": manifest.INTENT_KIND, - "contract_version": manifest.CONTRACT_VERSION, - "run_id": run_id, - "profile": manifest.PROFILE, - "status": INTENT_PREPARED, - "created_at": now, - "updated_at": now, - "capabilities": capabilities, - "observed_at_apply": {}, - } - - -def _write(path: Path, payload: dict[str, Any]) -> None: - path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) - write_json(path, payload, indent=2, mode=0o600) - - -def load_intent(journal_path: str | Path) -> dict[str, Any] | None: - path = intent_path(journal_path) - if not path.exists(): - return None - try: - payload = json.loads(path.read_text("utf-8")) - except (json.JSONDecodeError, OSError) as exc: - raise IntentError("sandbox profile intent is unreadable") from exc - if not isinstance(payload, dict): - raise IntentError("sandbox profile intent is malformed") - return payload - - -def require_intent(journal_path: str | Path, run_id: str) -> dict[str, Any]: - payload = load_intent(journal_path) - if payload is None: - raise FileNotFoundError("sandbox profile intent is missing") - return _validate_intent_payload(payload, run_id) - - -def _validate_intent_payload(payload: dict[str, Any], run_id: str) -> dict[str, Any]: - existing = payload.get("run_id") - if existing != run_id: - raise IntentRunMismatch(str(existing) if isinstance(existing, str) else "") - if payload.get("kind") != manifest.INTENT_KIND: - raise IntentError("sandbox profile intent kind is unsupported") - if payload.get("contract_version") != manifest.CONTRACT_VERSION: - raise IntentError("sandbox profile intent contract is unsupported") - if payload.get("profile") != manifest.PROFILE: - raise IntentError("sandbox profile intent profile is unsupported") - return payload - - -def ensure_prepared(journal_path: str | Path, run_id: str) -> dict[str, Any]: - existing = load_intent(journal_path) - if existing is not None: - return _validate_intent_payload(existing, run_id) - payload = _default_intent(run_id) - _write(intent_path(journal_path), payload) - return payload - - -def _capability(payload: dict[str, Any], name: str) -> dict[str, Any]: - caps = payload.get("capabilities") - if not isinstance(caps, list): - raise IntentError("sandbox profile intent capabilities are malformed") - for item in caps: - if isinstance(item, dict) and item.get("name") == name: - return item - item = _base_capability(name) - caps.append(item) - return item - - -def update_capability( - journal_path: str | Path, - run_id: str, - name: str, - *, - state: str, - residuals: tuple[str, ...] = (), - observed: dict[str, Any] | None = None, -) -> dict[str, Any]: - payload = copy.deepcopy(require_intent(journal_path, run_id)) - now = _now_iso() - cap = _capability(payload, name) - cap["intent_state"] = state - cap["residuals"] = list(residuals) - if state == INTENT_APPLY_STARTED: - cap["apply_started_at"] = cap.get("apply_started_at") or now - payload["status"] = "applying" - elif state == INTENT_APPLIED: - cap["applied_at"] = cap.get("applied_at") or now - elif state == INTENT_DISABLE_STARTED: - cap["disable_started_at"] = cap.get("disable_started_at") or now - payload["status"] = "disabling" - elif state == INTENT_DISABLED: - cap["disabled_at"] = cap.get("disabled_at") or now - elif state == INTENT_FAILED: - payload["status"] = INTENT_FAILED - if observed is not None: - observed_block = payload.get("observed_at_apply") - if not isinstance(observed_block, dict): - observed_block = {} - payload["observed_at_apply"] = observed_block - observed_block[name] = observed - payload["updated_at"] = now - _write(intent_path(journal_path), payload) - return payload - - -def mark_disabled_if_complete(journal_path: str | Path, run_id: str) -> dict[str, Any]: - payload = copy.deepcopy(require_intent(journal_path, run_id)) - now = _now_iso() - payload["status"] = INTENT_DISABLED - payload["updated_at"] = now - for name in manifest.CAPABILITY_ORDER: - cap = _capability(payload, name) - if name == manifest.CAPABILITY_RUNTIME: - continue - cap["intent_state"] = INTENT_DISABLED - cap["disabled_at"] = cap.get("disabled_at") or now - _write(intent_path(journal_path), payload) - return payload diff --git a/solstone/think/sandbox_profile/manifest.py b/solstone/think/sandbox_profile/manifest.py deleted file mode 100644 index ac053564d..000000000 --- a/solstone/think/sandbox_profile/manifest.py +++ /dev/null @@ -1,69 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Manifest constants for the disposable sandbox profile. - -The command owns only lifecycle intent inside an already-marked disposable -sandbox journal. Runtime disable intentionally leaves setup, identity, CA, and -directory deletion to the external sandbox harness. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from uuid import UUID - -CONTRACT_VERSION = 1 -PROFILE = "full" -MARKER_KIND = "solstone-disposable-journal" -INTENT_KIND = "solstone.sandbox_profile.intent" - -CAPABILITY_SCOUT = "scout" -CAPABILITY_SPL = "spl" -CAPABILITY_SPB = "spb" -CAPABILITY_SPP = "spp" -CAPABILITY_RUNTIME = "runtime" - -CAPABILITY_ORDER: tuple[str, ...] = ( - CAPABILITY_SCOUT, - CAPABILITY_SPL, - CAPABILITY_SPB, - CAPABILITY_SPP, - CAPABILITY_RUNTIME, -) -APPLY_CAPABILITIES: tuple[str, ...] = ( - CAPABILITY_SCOUT, - CAPABILITY_SPL, - CAPABILITY_SPB, - CAPABILITY_SPP, -) - - -@dataclass(frozen=True, slots=True) -class SyntheticOwner: - setup_completed_at: int - identity_name: str - identity_preferred: str - identity_timezone: str - journal_name: str - home_label: str - - -def synthetic_owner_metadata(run_id: str) -> SyntheticOwner: - """Return clearly synthetic owner metadata derived only from ``run_id``. - - This is intentionally non-real and deterministic: a repeated prepare for the - same canonical UUID converges on the same setup and identity fields. - """ - - parsed = UUID(run_id) - slug = parsed.hex[:12] - suffix = parsed.hex[-9:] - return SyntheticOwner( - setup_completed_at=1_700_000_000_000 + int(suffix, 16) % 1_000_000_000, - identity_name=f"Synthetic Sandbox Owner {slug}", - identity_preferred=f"sandbox-run-{slug}", - identity_timezone="UTC", - journal_name=f"Synthetic sandbox {slug}", - home_label=f"sandbox-{slug}", - ) diff --git a/solstone/think/sandbox_profile/marker.py b/solstone/think/sandbox_profile/marker.py deleted file mode 100644 index d556276a8..000000000 --- a/solstone/think/sandbox_profile/marker.py +++ /dev/null @@ -1,147 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Read-only disposable sandbox marker validation.""" - -from __future__ import annotations - -import json -import stat -from dataclasses import dataclass -from pathlib import Path -from uuid import UUID - -from solstone.think import json_codec -from solstone.think.sandbox_profile import manifest - -MAX_MARKER_BYTES = 64 * 1024 -MARKER_NAME = ".solstone-sandbox.json" - - -@dataclass(frozen=True, slots=True) -class MarkerContext: - journal_path: Path - run_id: str - profile: str - contract_version: int - - -class MarkerError(RuntimeError): - def __init__(self, code: str, message: str) -> None: - super().__init__(message) - self.code = code - self.message = message - - -def canonical_path(path: str | Path) -> Path: - return Path(path).expanduser().resolve(strict=False) - - -def _load_marker(path: Path) -> object: - try: - raw = path.read_bytes() - except FileNotFoundError as exc: - raise MarkerError( - "sandbox_marker_missing", "sandbox marker is missing" - ) from exc - except OSError as exc: - raise MarkerError( - "sandbox_marker_missing", "sandbox marker is unreadable" - ) from exc - if len(raw) > MAX_MARKER_BYTES: - raise MarkerError("sandbox_marker_unparseable", "sandbox marker is too large") - try: - text = raw.decode("utf-8") - decoder = json.JSONDecoder(object_pairs_hook=json_codec.reject_duplicate_keys) - payload, end = decoder.raw_decode(text.lstrip()) - prefix_len = len(text) - len(text.lstrip()) - if text[prefix_len + end :].strip(): - raise ValueError("trailing content") - return payload - except (UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc: - raise MarkerError( - "sandbox_marker_unparseable", - "sandbox marker is not valid JSON", - ) from exc - - -def validate_marker(journal_path: str | Path) -> MarkerContext: - journal = canonical_path(journal_path) - marker = journal / MARKER_NAME - try: - marker_stat = marker.lstat() - except FileNotFoundError as exc: - raise MarkerError( - "sandbox_marker_missing", "sandbox marker is missing" - ) from exc - except OSError as exc: - raise MarkerError( - "sandbox_marker_missing", "sandbox marker is unreadable" - ) from exc - if stat.S_ISLNK(marker_stat.st_mode): - raise MarkerError( - "sandbox_marker_symlink", "sandbox marker must not be a symlink" - ) - if not stat.S_ISREG(marker_stat.st_mode): - raise MarkerError( - "sandbox_marker_not_regular", - "sandbox marker must be a regular file", - ) - - payload = _load_marker(marker) - if not isinstance(payload, dict): - raise MarkerError( - "sandbox_marker_non_object", - "sandbox marker must be a JSON object", - ) - if payload.get("kind") != manifest.MARKER_KIND: - raise MarkerError( - "sandbox_marker_wrong_kind", - "sandbox marker kind is unsupported", - ) - if payload.get("contract_version") != manifest.CONTRACT_VERSION: - raise MarkerError( - "sandbox_marker_wrong_contract_version", - "sandbox marker contract_version is unsupported", - ) - if payload.get("profile") != manifest.PROFILE: - raise MarkerError( - "sandbox_marker_wrong_profile", - "sandbox marker profile is unsupported", - ) - - run_id_raw = payload.get("run_id") - if not isinstance(run_id_raw, str): - raise MarkerError( - "sandbox_marker_bad_run_id", "sandbox marker run_id is invalid" - ) - try: - parsed_uuid = UUID(run_id_raw) - except ValueError as exc: - raise MarkerError( - "sandbox_marker_bad_run_id", "sandbox marker run_id is invalid" - ) from exc - if str(parsed_uuid) != run_id_raw: - raise MarkerError( - "sandbox_marker_bad_run_id", - "sandbox marker run_id must be canonical", - ) - - marker_journal = payload.get("journal_path") - if not isinstance(marker_journal, str): - raise MarkerError( - "sandbox_marker_path_mismatch", - "sandbox marker journal_path is invalid", - ) - if canonical_path(marker_journal) != journal: - raise MarkerError( - "sandbox_marker_path_mismatch", - "sandbox marker journal_path does not match resolved journal", - ) - - return MarkerContext( - journal_path=journal, - run_id=run_id_raw, - profile=manifest.PROFILE, - contract_version=manifest.CONTRACT_VERSION, - ) diff --git a/solstone/think/sandbox_profile/probe_contract.py b/solstone/think/sandbox_profile/probe_contract.py deleted file mode 100644 index 43c8becac..000000000 --- a/solstone/think/sandbox_profile/probe_contract.py +++ /dev/null @@ -1,557 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Closed v1 machine vocabulary for sandbox production probes. - -The reason strings deliberately are not imported from ``envelope.py``. That -module owns CLI-envelope capability residuals; this module owns the separate -append-only production-probe contract, and extending the envelope vocabulary is -out of scope for this lode. -""" - -from __future__ import annotations - -from pathlib import Path - -CONTRACT_VERSION = 1 - -CAPABILITY_SCOUT = "scout" -CAPABILITY_SPL = "spl" -CAPABILITY_SPB = "spb" -CAPABILITY_SPP = "spp" -CAPABILITY_RUNTIME = "runtime" - -CAPABILITY_ORDER: tuple[str, ...] = ( - CAPABILITY_SCOUT, - CAPABILITY_SPL, - CAPABILITY_SPB, - CAPABILITY_SPP, - CAPABILITY_RUNTIME, -) - -HEALTH_DIR_NAME = "health" -SANDBOX_PROFILE_DIR_NAME = "sandbox-profile" -LEDGER_FILE_NAME = "probes-v1.jsonl" -LOCK_FILE_NAME = "probes-v1.lock" -ATTEMPTS_DIR_NAME = "probe-attempts-v1" -ATTEMPT_DIR_MODE = 0o700 - -MAX_ATTEMPTS = 64 -MAX_LEDGER_BYTES = 1_048_576 - -RECORD_TYPE_ATTEMPT_STARTED = "attempt_started" -RECORD_TYPE_PROOF_TERMINAL = "proof_terminal" -RECORD_TYPE_ATTEMPT_TERMINAL = "attempt_terminal" -RECORD_TYPES: tuple[str, ...] = ( - RECORD_TYPE_ATTEMPT_STARTED, - RECORD_TYPE_PROOF_TERMINAL, - RECORD_TYPE_ATTEMPT_TERMINAL, -) - -PROOF_STATE_PASSED = "passed" -PROOF_STATE_FAILED = "failed" -PROOF_STATE_NOT_RUN = "not_run" -PROOF_TERMINAL_STATES: tuple[str, ...] = ( - PROOF_STATE_PASSED, - PROOF_STATE_FAILED, - PROOF_STATE_NOT_RUN, -) - -CLEANUP_STATE_VERIFIED = "verified" -CLEANUP_STATE_RETAINED_SYNTHETIC = "retained_synthetic" -CLEANUP_STATE_UNVERIFIED = "unverified" -CLEANUP_STATES: tuple[str, ...] = ( - CLEANUP_STATE_VERIFIED, - CLEANUP_STATE_RETAINED_SYNTHETIC, - CLEANUP_STATE_UNVERIFIED, -) - -ATTEMPT_STATE_OK = "ok" -ATTEMPT_STATE_DEGRADED = "degraded" -ATTEMPT_STATE_CANCELLED = "cancelled" -ATTEMPT_STATE_ERROR = "error" -ATTEMPT_TERMINAL_STATES: tuple[str, ...] = ( - ATTEMPT_STATE_OK, - ATTEMPT_STATE_DEGRADED, - ATTEMPT_STATE_CANCELLED, - ATTEMPT_STATE_ERROR, -) - -REASON_CAPABILITY_NOT_READY = "capability_not_ready" -REASON_DEADLINE_EXCEEDED = "deadline_exceeded" -REASON_REMOTE_REJECTED = "remote_rejected" -REASON_RESPONSE_INVALID = "response_invalid" -REASON_CONTENT_MISMATCH = "content_mismatch" -REASON_USAGE_INVALID = "usage_invalid" -REASON_ATTESTATION_UNVERIFIED = "attestation_unverified" -REASON_RUNTIME_UNAVAILABLE = "runtime_unavailable" -REASON_CADENCE_CONTRACT_MISMATCH = "cadence_contract_mismatch" -PROOF_REASON_POOL: tuple[str, ...] = ( - REASON_CAPABILITY_NOT_READY, - REASON_DEADLINE_EXCEEDED, - REASON_REMOTE_REJECTED, - REASON_RESPONSE_INVALID, - REASON_CONTENT_MISMATCH, - REASON_USAGE_INVALID, - REASON_ATTESTATION_UNVERIFIED, - REASON_RUNTIME_UNAVAILABLE, - REASON_CADENCE_CONTRACT_MISMATCH, -) - -REASON_DEPENDENCY_FAILED = "dependency_failed" -REASON_CANCELLED = "cancelled" -REASON_CLEANUP_UNVERIFIED = "cleanup_unverified" -REASON_INTERNAL_ERROR = "internal_error" -COMMON_REASONS: tuple[str, ...] = ( - REASON_DEPENDENCY_FAILED, - REASON_CANCELLED, - REASON_CLEANUP_UNVERIFIED, - REASON_INTERNAL_ERROR, -) -FAILED_COMMON_REASONS: tuple[str, ...] = ( - REASON_CANCELLED, - REASON_CLEANUP_UNVERIFIED, - REASON_INTERNAL_ERROR, -) -NOT_RUN_REASONS: tuple[str, ...] = ( - REASON_DEPENDENCY_FAILED, - REASON_CANCELLED, -) - -ATTEMPT_TERMINAL_REASON_PROOF_FAILED = "proof_failed" -ATTEMPT_TERMINAL_REASONS: tuple[str, ...] = ( - REASON_CLEANUP_UNVERIFIED, - REASON_CANCELLED, - REASON_INTERNAL_ERROR, - ATTEMPT_TERMINAL_REASON_PROOF_FAILED, -) - -STABLE_ERROR_PROBE_ACTIVE = "probe_active" -STABLE_ERROR_ATTEMPT_LIMIT_REACHED = "attempt_limit_reached" -STABLE_ERROR_STALE_ATTEMPT = "stale_attempt" -STABLE_ERROR_RECORD_WRITE_FAILED = "record_write_failed" -STABLE_ERROR_INTERNAL_ERROR = "internal_error" -STABLE_ERRORS: tuple[str, ...] = ( - STABLE_ERROR_PROBE_ACTIVE, - STABLE_ERROR_ATTEMPT_LIMIT_REACHED, - STABLE_ERROR_STALE_ATTEMPT, - STABLE_ERROR_RECORD_WRITE_FAILED, - STABLE_ERROR_INTERNAL_ERROR, -) - -PROOF_CHECKS: dict[str, tuple[str, ...]] = { - CAPABILITY_SCOUT: ( - f"{CAPABILITY_SCOUT}.response_schema", - f"{CAPABILITY_SCOUT}.nonce_match", - f"{CAPABILITY_SCOUT}.finish", - f"{CAPABILITY_SCOUT}.usage", - ), - CAPABILITY_SPL: ( - f"{CAPABILITY_SPL}.enrollment", - f"{CAPABILITY_SPL}.relay_dial", - f"{CAPABILITY_SPL}.inner_tls", - f"{CAPABILITY_SPL}.observer_registered", - f"{CAPABILITY_SPL}.segment_transferred", - f"{CAPABILITY_SPL}.segment_landed", - f"{CAPABILITY_SPL}.authorization_removed", - ), - CAPABILITY_SPB: ( - f"{CAPABILITY_SPB}.repository_initialized", - f"{CAPABILITY_SPB}.snapshot_created", - f"{CAPABILITY_SPB}.snapshot_confirmed", - f"{CAPABILITY_SPB}.restore_match", - f"{CAPABILITY_SPB}.local_cleanup", - ), - CAPABILITY_SPP: ( - f"{CAPABILITY_SPP}.attestation_session", - f"{CAPABILITY_SPP}.text_nonce", - f"{CAPABILITY_SPP}.text_usage", - f"{CAPABILITY_SPP}.transcript_expected", - ), - CAPABILITY_RUNTIME: ( - f"{CAPABILITY_RUNTIME}.supervisor", - f"{CAPABILITY_RUNTIME}.callosum", - f"{CAPABILITY_RUNTIME}.listener", - f"{CAPABILITY_RUNTIME}.sense", - f"{CAPABILITY_RUNTIME}.task_queue", - f"{CAPABILITY_RUNTIME}.cortex", - f"{CAPABILITY_RUNTIME}.talent_output", - f"{CAPABILITY_RUNTIME}.talent_usage", - f"{CAPABILITY_RUNTIME}.cadence_contract", - f"{CAPABILITY_RUNTIME}.cadence_dry_run", - ), -} - -_PROOF_REASON_EXCLUSIONS: dict[str, frozenset[str]] = { - CAPABILITY_SCOUT: frozenset( - { - REASON_ATTESTATION_UNVERIFIED, - REASON_RUNTIME_UNAVAILABLE, - REASON_CADENCE_CONTRACT_MISMATCH, - } - ), - CAPABILITY_SPL: frozenset( - { - REASON_USAGE_INVALID, - REASON_ATTESTATION_UNVERIFIED, - REASON_RUNTIME_UNAVAILABLE, - REASON_CADENCE_CONTRACT_MISMATCH, - } - ), - CAPABILITY_SPB: frozenset( - { - REASON_USAGE_INVALID, - REASON_ATTESTATION_UNVERIFIED, - REASON_RUNTIME_UNAVAILABLE, - REASON_CADENCE_CONTRACT_MISMATCH, - } - ), - CAPABILITY_SPP: frozenset( - { - REASON_RUNTIME_UNAVAILABLE, - REASON_CADENCE_CONTRACT_MISMATCH, - } - ), - CAPABILITY_RUNTIME: frozenset( - { - REASON_REMOTE_REJECTED, - REASON_ATTESTATION_UNVERIFIED, - } - ), -} - -PROOF_SPECIFIC_REASONS: dict[str, tuple[str, ...]] = { - proof: tuple( - sorted( - reason - for reason in PROOF_REASON_POOL - if reason not in _PROOF_REASON_EXCLUSIONS[proof] - ) - ) - for proof in CAPABILITY_ORDER -} - -# Cleanup class is declared per proof; it is not derived from whether a proof -# has a terminal cleanup check. -DECLARED_CLEANUP_STATES: dict[str, str] = { - # Request/response probe; no durable remote or local artifact is created. - CAPABILITY_SCOUT: CLEANUP_STATE_VERIFIED, - # Local authorization is removed, but a synthetic segment remains landed. - CAPABILITY_SPL: CLEANUP_STATE_RETAINED_SYNTHETIC, - # Local cleanup precedes terminalization; retained state is the remote snapshot. - CAPABILITY_SPB: CLEANUP_STATE_RETAINED_SYNTHETIC, - # Attested text round trip; no durable artifact is retained. - CAPABILITY_SPP: CLEANUP_STATE_VERIFIED, - # Runtime is the sandbox substrate and cannot self-clean mid-probe. - CAPABILITY_RUNTIME: CLEANUP_STATE_RETAINED_SYNTHETIC, -} - -FIELD_ATTEMPT_ID = "attempt_id" -FIELD_CHECKS = "checks" -FIELD_CLEANUP_STATE = "cleanup_state" -FIELD_CONTRACT_VERSION = "contract_version" -FIELD_DURATION_MS = "duration_ms" -FIELD_EXECUTION_ORDER = "execution_order" -FIELD_FINISHED_AT = "finished_at" -FIELD_PROOF = "proof" -FIELD_REASON = "reason" -FIELD_RUN_ID = "run_id" -FIELD_SELECTED = "selected" -FIELD_STARTED_AT = "started_at" -FIELD_STATE = "state" -FIELD_TERMINAL_REASON = "terminal_reason" -FIELD_TYPE = "type" - -RECORD_FIELDS: dict[str, tuple[str, ...]] = { - RECORD_TYPE_ATTEMPT_STARTED: ( - FIELD_ATTEMPT_ID, - FIELD_CONTRACT_VERSION, - FIELD_EXECUTION_ORDER, - FIELD_RUN_ID, - FIELD_SELECTED, - FIELD_STARTED_AT, - FIELD_TYPE, - ), - RECORD_TYPE_PROOF_TERMINAL: ( - FIELD_ATTEMPT_ID, - FIELD_CHECKS, - FIELD_CLEANUP_STATE, - FIELD_CONTRACT_VERSION, - FIELD_DURATION_MS, - FIELD_FINISHED_AT, - FIELD_PROOF, - FIELD_REASON, - FIELD_RUN_ID, - FIELD_STATE, - FIELD_TYPE, - ), - RECORD_TYPE_ATTEMPT_TERMINAL: ( - FIELD_ATTEMPT_ID, - FIELD_CONTRACT_VERSION, - FIELD_FINISHED_AT, - FIELD_RUN_ID, - FIELD_STATE, - FIELD_TERMINAL_REASON, - FIELD_TYPE, - ), -} - -PREDICATE_CHECKS_COMPLETE = "checks.complete" -PREDICATE_CHECKS_ORDERED_PREFIX = "checks.ordered_prefix" -PREDICATE_CHECKS_EMPTY = "checks.empty" -PREDICATE_REASON_NULL = "reason.null" -PREDICATE_REASON_FAILED_SPECIFIC_OR_COMMON = "reason.failed_specific_or_common" -PREDICATE_REASON_NOT_RUN = "reason.not_run" -PREDICATE_DURATION_NON_NEGATIVE_INT = "duration.non_negative_int" -PREDICATE_DURATION_NULL = "duration.null" -PREDICATE_CLEANUP_EXPECTED = "cleanup.expected_for_proof_state_reason" -PREDICATE_TERMINAL_ANY_CLEANUP_UNVERIFIED = "terminal.any_cleanup_unverified" -PREDICATE_TERMINAL_ANY_REASON_CANCELLED = "terminal.any_reason_cancelled" -PREDICATE_TERMINAL_ANY_REASON_INTERNAL_ERROR = "terminal.any_reason_internal_error" -PREDICATE_TERMINAL_ANY_FAILED_PROOF = "terminal.any_failed_proof" -PREDICATE_TERMINAL_ALL_PASSED = "terminal.all_passed" -PREDICATE_RETRY_ALL_CLEANUP_CLOSED = "retry.all_cleanup_closed" -PREDICATE_CANCELLATION_FIRST_FAILED_CANCELLED_AFTER_CONTACT = ( - "cancellation.first_failed_cancelled_after_contact" -) -PREDICATE_CANCELLATION_FIRST_NOT_RUN_CANCELLED_WITHOUT_CONTACT = ( - "cancellation.first_not_run_cancelled_without_contact" -) -PREDICATE_CANCELLATION_CONTIGUOUS_NOT_RUN_CANCELLED_SUFFIX = ( - "cancellation.contiguous_not_run_cancelled_suffix" -) -PREDICATE_KEYS: tuple[str, ...] = ( - PREDICATE_CHECKS_COMPLETE, - PREDICATE_CHECKS_ORDERED_PREFIX, - PREDICATE_CHECKS_EMPTY, - PREDICATE_REASON_NULL, - PREDICATE_REASON_FAILED_SPECIFIC_OR_COMMON, - PREDICATE_REASON_NOT_RUN, - PREDICATE_DURATION_NON_NEGATIVE_INT, - PREDICATE_DURATION_NULL, - PREDICATE_CLEANUP_EXPECTED, - PREDICATE_TERMINAL_ANY_CLEANUP_UNVERIFIED, - PREDICATE_TERMINAL_ANY_REASON_CANCELLED, - PREDICATE_TERMINAL_ANY_REASON_INTERNAL_ERROR, - PREDICATE_TERMINAL_ANY_FAILED_PROOF, - PREDICATE_TERMINAL_ALL_PASSED, - PREDICATE_RETRY_ALL_CLEANUP_CLOSED, - PREDICATE_CANCELLATION_FIRST_FAILED_CANCELLED_AFTER_CONTACT, - PREDICATE_CANCELLATION_FIRST_NOT_RUN_CANCELLED_WITHOUT_CONTACT, - PREDICATE_CANCELLATION_CONTIGUOUS_NOT_RUN_CANCELLED_SUFFIX, -) - -PROOF_TERMINAL_RULES: dict[str, dict[str, object]] = { - PROOF_STATE_PASSED: { - FIELD_CHECKS: PREDICATE_CHECKS_COMPLETE, - FIELD_REASON: PREDICATE_REASON_NULL, - FIELD_DURATION_MS: PREDICATE_DURATION_NON_NEGATIVE_INT, - FIELD_CLEANUP_STATE: PREDICATE_CLEANUP_EXPECTED, - }, - PROOF_STATE_FAILED: { - FIELD_CHECKS: PREDICATE_CHECKS_ORDERED_PREFIX, - FIELD_REASON: PREDICATE_REASON_FAILED_SPECIFIC_OR_COMMON, - FIELD_DURATION_MS: PREDICATE_DURATION_NON_NEGATIVE_INT, - FIELD_CLEANUP_STATE: PREDICATE_CLEANUP_EXPECTED, - "failed_common_reasons": FAILED_COMMON_REASONS, - }, - PROOF_STATE_NOT_RUN: { - FIELD_CHECKS: PREDICATE_CHECKS_EMPTY, - FIELD_REASON: PREDICATE_REASON_NOT_RUN, - FIELD_DURATION_MS: PREDICATE_DURATION_NULL, - FIELD_CLEANUP_STATE: PREDICATE_CLEANUP_EXPECTED, - }, -} - -CLEANUP_RESOLUTION: dict[str, object] = { - "declared_defaults": DECLARED_CLEANUP_STATES, - "state_overrides": { - PROOF_STATE_NOT_RUN: CLEANUP_STATE_VERIFIED, - }, - "reason_overrides": { - REASON_CLEANUP_UNVERIFIED: CLEANUP_STATE_UNVERIFIED, - }, -} - -RECORD_CARDINALITY: dict[str, object] = { - "attempt_sequence": ( - { - "type": RECORD_TYPE_ATTEMPT_STARTED, - "count": 1, - "position": "first", - }, - { - "type": RECORD_TYPE_PROOF_TERMINAL, - "count": "len(execution_order)", - "order": "execution_order", - }, - { - "type": RECORD_TYPE_ATTEMPT_TERMINAL, - "count": 1, - "position": "last", - }, - ), - "attempt_count_type": RECORD_TYPE_ATTEMPT_STARTED, -} - -CANCELLATION: dict[str, object] = { - "first_started_predicate": PREDICATE_CANCELLATION_FIRST_FAILED_CANCELLED_AFTER_CONTACT, - "first_unstarted_predicate": ( - PREDICATE_CANCELLATION_FIRST_NOT_RUN_CANCELLED_WITHOUT_CONTACT - ), - "suffix_predicate": PREDICATE_CANCELLATION_CONTIGUOUS_NOT_RUN_CANCELLED_SUFFIX, - "later_proof": { - FIELD_STATE: PROOF_STATE_NOT_RUN, - FIELD_CHECKS: (), - FIELD_REASON: REASON_CANCELLED, - FIELD_DURATION_MS: None, - FIELD_CLEANUP_STATE: CLEANUP_STATE_VERIFIED, - }, -} - -TERMINAL_DERIVATION: tuple[dict[str, str | None], ...] = ( - { - "predicate": PREDICATE_TERMINAL_ANY_CLEANUP_UNVERIFIED, - "state": ATTEMPT_STATE_DEGRADED, - FIELD_TERMINAL_REASON: REASON_CLEANUP_UNVERIFIED, - }, - { - "predicate": PREDICATE_TERMINAL_ANY_REASON_CANCELLED, - "state": ATTEMPT_STATE_CANCELLED, - FIELD_TERMINAL_REASON: REASON_CANCELLED, - }, - { - "predicate": PREDICATE_TERMINAL_ANY_REASON_INTERNAL_ERROR, - "state": ATTEMPT_STATE_ERROR, - FIELD_TERMINAL_REASON: REASON_INTERNAL_ERROR, - }, - { - "predicate": PREDICATE_TERMINAL_ANY_FAILED_PROOF, - "state": ATTEMPT_STATE_DEGRADED, - FIELD_TERMINAL_REASON: ATTEMPT_TERMINAL_REASON_PROOF_FAILED, - }, - { - "predicate": PREDICATE_TERMINAL_ALL_PASSED, - "state": ATTEMPT_STATE_OK, - FIELD_TERMINAL_REASON: None, - }, -) - -RETRY_ELIGIBLE_TERMINALS: tuple[dict[str, str | None], ...] = ( - { - "state": ATTEMPT_STATE_OK, - FIELD_TERMINAL_REASON: None, - "proofs": None, - }, - { - "state": ATTEMPT_STATE_DEGRADED, - FIELD_TERMINAL_REASON: ATTEMPT_TERMINAL_REASON_PROOF_FAILED, - "proofs": PREDICATE_RETRY_ALL_CLEANUP_CLOSED, - }, -) - - -def _json_dict_tuple(value: dict[str, tuple[str, ...]]) -> dict[str, list[str]]: - return {key: list(items) for key, items in value.items()} - - -def _json_record_sequence( - records: tuple[dict[str, object], ...], -) -> list[dict[str, object]]: - return [dict(record) for record in records] - - -def _json_terminal_rules() -> dict[str, dict[str, object]]: - return { - state: { - key: list(value) if isinstance(value, tuple) else value - for key, value in rule.items() - } - for state, rule in PROOF_TERMINAL_RULES.items() - } - - -def _json_cleanup_resolution() -> dict[str, object]: - return { - "declared_defaults": dict(DECLARED_CLEANUP_STATES), - "reason_overrides": dict(CLEANUP_RESOLUTION["reason_overrides"]), - "state_overrides": dict(CLEANUP_RESOLUTION["state_overrides"]), - } - - -def _json_cancellation() -> dict[str, object]: - later = CANCELLATION["later_proof"] - if not isinstance(later, dict): - raise RuntimeError("invalid cancellation contract") - return { - "first_started_predicate": CANCELLATION["first_started_predicate"], - "first_unstarted_predicate": CANCELLATION["first_unstarted_predicate"], - "later_proof": { - key: list(value) if isinstance(value, tuple) else value - for key, value in later.items() - }, - "suffix_predicate": CANCELLATION["suffix_predicate"], - } - - -def sandbox_profile_health_path(journal_path: str | Path) -> Path: - return Path(journal_path) / HEALTH_DIR_NAME / SANDBOX_PROFILE_DIR_NAME - - -def probe_ledger_path(journal_path: str | Path) -> Path: - return sandbox_profile_health_path(journal_path) / LEDGER_FILE_NAME - - -def probe_lock_path(journal_path: str | Path) -> Path: - return sandbox_profile_health_path(journal_path) / LOCK_FILE_NAME - - -def probe_attempts_parent_path(journal_path: str | Path) -> Path: - return sandbox_profile_health_path(journal_path) / ATTEMPTS_DIR_NAME - - -def contract_payload() -> dict[str, object]: - return { - "attempt_terminal_reasons": list(ATTEMPT_TERMINAL_REASONS), - "attempt_terminal_states": list(ATTEMPT_TERMINAL_STATES), - "cancellation": _json_cancellation(), - "capability_order": list(CAPABILITY_ORDER), - "cleanup_resolution": _json_cleanup_resolution(), - "cleanup_states": list(CLEANUP_STATES), - "common_reasons": list(COMMON_REASONS), - "contract_version": CONTRACT_VERSION, - "limits": { - "max_attempts": MAX_ATTEMPTS, - "max_ledger_bytes": MAX_LEDGER_BYTES, - }, - "not_run_reasons": list(NOT_RUN_REASONS), - "predicate_keys": list(PREDICATE_KEYS), - "proof_reason_pool": list(PROOF_REASON_POOL), - "proof_terminal_rules": _json_terminal_rules(), - "proof_terminal_states": list(PROOF_TERMINAL_STATES), - "proofs": { - proof: { - "checks": list(PROOF_CHECKS[proof]), - "proof_specific_reasons": list(PROOF_SPECIFIC_REASONS[proof]), - } - for proof in CAPABILITY_ORDER - }, - "record_cardinality": { - "attempt_count_type": RECORD_CARDINALITY["attempt_count_type"], - "attempt_sequence": _json_record_sequence(_record_cardinality_sequence()), - }, - "record_fields": _json_dict_tuple(RECORD_FIELDS), - "record_types": list(RECORD_TYPES), - "retry_eligible_terminals": [ - dict(terminal) for terminal in RETRY_ELIGIBLE_TERMINALS - ], - "stable_errors": list(STABLE_ERRORS), - "terminal_derivation": [dict(rule) for rule in TERMINAL_DERIVATION], - } - - -def _record_cardinality_sequence() -> tuple[dict[str, object], ...]: - sequence = RECORD_CARDINALITY["attempt_sequence"] - if not isinstance(sequence, tuple): - raise RuntimeError("invalid record cardinality contract") - return sequence diff --git a/solstone/think/sandbox_profile/probe_contract_v1.json b/solstone/think/sandbox_profile/probe_contract_v1.json deleted file mode 100644 index 7a7ffaf73..000000000 --- a/solstone/think/sandbox_profile/probe_contract_v1.json +++ /dev/null @@ -1,325 +0,0 @@ -{ - "attempt_terminal_reasons": [ - "cleanup_unverified", - "cancelled", - "internal_error", - "proof_failed" - ], - "attempt_terminal_states": [ - "ok", - "degraded", - "cancelled", - "error" - ], - "cancellation": { - "first_started_predicate": "cancellation.first_failed_cancelled_after_contact", - "first_unstarted_predicate": "cancellation.first_not_run_cancelled_without_contact", - "later_proof": { - "checks": [], - "cleanup_state": "verified", - "duration_ms": null, - "reason": "cancelled", - "state": "not_run" - }, - "suffix_predicate": "cancellation.contiguous_not_run_cancelled_suffix" - }, - "capability_order": [ - "scout", - "spl", - "spb", - "spp", - "runtime" - ], - "cleanup_resolution": { - "declared_defaults": { - "runtime": "retained_synthetic", - "scout": "verified", - "spb": "retained_synthetic", - "spl": "retained_synthetic", - "spp": "verified" - }, - "reason_overrides": { - "cleanup_unverified": "unverified" - }, - "state_overrides": { - "not_run": "verified" - } - }, - "cleanup_states": [ - "verified", - "retained_synthetic", - "unverified" - ], - "common_reasons": [ - "dependency_failed", - "cancelled", - "cleanup_unverified", - "internal_error" - ], - "contract_version": 1, - "limits": { - "max_attempts": 64, - "max_ledger_bytes": 1048576 - }, - "not_run_reasons": [ - "dependency_failed", - "cancelled" - ], - "predicate_keys": [ - "checks.complete", - "checks.ordered_prefix", - "checks.empty", - "reason.null", - "reason.failed_specific_or_common", - "reason.not_run", - "duration.non_negative_int", - "duration.null", - "cleanup.expected_for_proof_state_reason", - "terminal.any_cleanup_unverified", - "terminal.any_reason_cancelled", - "terminal.any_reason_internal_error", - "terminal.any_failed_proof", - "terminal.all_passed", - "retry.all_cleanup_closed", - "cancellation.first_failed_cancelled_after_contact", - "cancellation.first_not_run_cancelled_without_contact", - "cancellation.contiguous_not_run_cancelled_suffix" - ], - "proof_reason_pool": [ - "capability_not_ready", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - "content_mismatch", - "usage_invalid", - "attestation_unverified", - "runtime_unavailable", - "cadence_contract_mismatch" - ], - "proof_terminal_rules": { - "failed": { - "checks": "checks.ordered_prefix", - "cleanup_state": "cleanup.expected_for_proof_state_reason", - "duration_ms": "duration.non_negative_int", - "failed_common_reasons": [ - "cancelled", - "cleanup_unverified", - "internal_error" - ], - "reason": "reason.failed_specific_or_common" - }, - "not_run": { - "checks": "checks.empty", - "cleanup_state": "cleanup.expected_for_proof_state_reason", - "duration_ms": "duration.null", - "reason": "reason.not_run" - }, - "passed": { - "checks": "checks.complete", - "cleanup_state": "cleanup.expected_for_proof_state_reason", - "duration_ms": "duration.non_negative_int", - "reason": "reason.null" - } - }, - "proof_terminal_states": [ - "passed", - "failed", - "not_run" - ], - "proofs": { - "runtime": { - "checks": [ - "runtime.supervisor", - "runtime.callosum", - "runtime.listener", - "runtime.sense", - "runtime.task_queue", - "runtime.cortex", - "runtime.talent_output", - "runtime.talent_usage", - "runtime.cadence_contract", - "runtime.cadence_dry_run" - ], - "proof_specific_reasons": [ - "cadence_contract_mismatch", - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "response_invalid", - "runtime_unavailable", - "usage_invalid" - ] - }, - "scout": { - "checks": [ - "scout.response_schema", - "scout.nonce_match", - "scout.finish", - "scout.usage" - ], - "proof_specific_reasons": [ - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - "usage_invalid" - ] - }, - "spb": { - "checks": [ - "spb.repository_initialized", - "spb.snapshot_created", - "spb.snapshot_confirmed", - "spb.restore_match", - "spb.local_cleanup" - ], - "proof_specific_reasons": [ - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid" - ] - }, - "spl": { - "checks": [ - "spl.enrollment", - "spl.relay_dial", - "spl.inner_tls", - "spl.observer_registered", - "spl.segment_transferred", - "spl.segment_landed", - "spl.authorization_removed" - ], - "proof_specific_reasons": [ - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid" - ] - }, - "spp": { - "checks": [ - "spp.attestation_session", - "spp.text_nonce", - "spp.text_usage", - "spp.transcript_expected" - ], - "proof_specific_reasons": [ - "attestation_unverified", - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - "usage_invalid" - ] - } - }, - "record_cardinality": { - "attempt_count_type": "attempt_started", - "attempt_sequence": [ - { - "count": 1, - "position": "first", - "type": "attempt_started" - }, - { - "count": "len(execution_order)", - "order": "execution_order", - "type": "proof_terminal" - }, - { - "count": 1, - "position": "last", - "type": "attempt_terminal" - } - ] - }, - "record_fields": { - "attempt_started": [ - "attempt_id", - "contract_version", - "execution_order", - "run_id", - "selected", - "started_at", - "type" - ], - "attempt_terminal": [ - "attempt_id", - "contract_version", - "finished_at", - "run_id", - "state", - "terminal_reason", - "type" - ], - "proof_terminal": [ - "attempt_id", - "checks", - "cleanup_state", - "contract_version", - "duration_ms", - "finished_at", - "proof", - "reason", - "run_id", - "state", - "type" - ] - }, - "record_types": [ - "attempt_started", - "proof_terminal", - "attempt_terminal" - ], - "retry_eligible_terminals": [ - { - "proofs": null, - "state": "ok", - "terminal_reason": null - }, - { - "proofs": "retry.all_cleanup_closed", - "state": "degraded", - "terminal_reason": "proof_failed" - } - ], - "stable_errors": [ - "probe_active", - "attempt_limit_reached", - "stale_attempt", - "record_write_failed", - "internal_error" - ], - "terminal_derivation": [ - { - "predicate": "terminal.any_cleanup_unverified", - "state": "degraded", - "terminal_reason": "cleanup_unverified" - }, - { - "predicate": "terminal.any_reason_cancelled", - "state": "cancelled", - "terminal_reason": "cancelled" - }, - { - "predicate": "terminal.any_reason_internal_error", - "state": "error", - "terminal_reason": "internal_error" - }, - { - "predicate": "terminal.any_failed_proof", - "state": "degraded", - "terminal_reason": "proof_failed" - }, - { - "predicate": "terminal.all_passed", - "state": "ok", - "terminal_reason": null - } - ] -} diff --git a/solstone/think/sandbox_profile/probe_durability.py b/solstone/think/sandbox_profile/probe_durability.py deleted file mode 100644 index a2279cf83..000000000 --- a/solstone/think/sandbox_profile/probe_durability.py +++ /dev/null @@ -1,77 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Strict fd durability primitives for sandbox probe coordination. - -Probe rows are encoded once by the caller, prospectively size-checked against -the retained ledger descriptor, then appended through that same descriptor. The -caller owns open-time no-follow checks, descriptor identity, and tracked byte -accounting; this module preserves the append durability order: write once, -fsync the file, then fsync the parent directory. -""" - -from __future__ import annotations - -import json -import os -from pathlib import Path - -from solstone.think.sandbox_profile import probe_contract as contract -from solstone.think.sandbox_profile import probe_records - - -def encode_jsonl_record(record: dict[str, object]) -> bytes: - return ( - json.dumps(record, ensure_ascii=False, separators=(",", ":"), sort_keys=True) - + "\n" - ).encode("utf-8") - - -def append_jsonl_strict(fd: int, parent_dir: Path, data: bytes) -> None: - try: - written = _write_once(fd, data) - if written != len(data): - probe_records.raise_probe_error(contract.STABLE_ERROR_RECORD_WRITE_FAILED) - _fsync_file(fd) - _fsync_directory(parent_dir) - except probe_records.ProbeOperationError: - raise - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_RECORD_WRITE_FAILED) - - -def _mkdir(path: Path, *, mode: int, parents: bool = False) -> None: - path.mkdir(mode=mode, parents=parents, exist_ok=parents) - - -def _open_append(path: Path) -> int: - fd = os.open( - path, - os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW, - 0o600, - ) - try: - os.fchmod(fd, 0o600) - except OSError: - os.close(fd) - raise - return fd - - -def _write_once(fd: int, data: bytes) -> int: - return os.write(fd, data) - - -def _fsync_file(fd: int) -> None: - os.fsync(fd) - - -def _fsync_directory(path: Path) -> None: - flags = os.O_RDONLY - if hasattr(os, "O_DIRECTORY"): - flags |= os.O_DIRECTORY - fd = os.open(path, flags) - try: - os.fsync(fd) - finally: - os.close(fd) diff --git a/solstone/think/sandbox_profile/probe_records.py b/solstone/think/sandbox_profile/probe_records.py deleted file mode 100644 index f5413754c..000000000 --- a/solstone/think/sandbox_profile/probe_records.py +++ /dev/null @@ -1,831 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Record grammar and truth-table validation for sandbox probe ledgers.""" - -from __future__ import annotations - -import re -from collections.abc import Callable, Mapping, Sequence -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path -from typing import Any, NoReturn -from uuid import UUID, uuid4 - -from solstone.think.sandbox_profile import probe_contract as contract - -_TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$") - - -class ProbeRecordValidationError(ValueError): - """Internal validation failure converted to stable public errors by callers.""" - - -class ProbeOperationError(RuntimeError): - def __init__( - self, - code: str, - *, - attempt_id: str | None = None, - record_type: str | None = None, - proof: str | None = None, - ) -> None: - if code not in contract.STABLE_ERRORS: - raise ValueError("invalid stable error code") - if attempt_id is not None: - validate_canonical_uuid(attempt_id) - if record_type is not None and record_type not in contract.RECORD_TYPES: - raise ValueError("invalid record type") - if proof is not None and proof not in contract.CAPABILITY_ORDER: - raise ValueError("invalid proof") - super().__init__(code) - self.code = code - self.attempt_id = attempt_id - self.record_type = record_type - self.proof = proof - - -def raise_probe_error( - code: str, - *, - attempt_id: str | None = None, - record_type: str | None = None, - proof: str | None = None, -) -> NoReturn: - raise ProbeOperationError( - code, - attempt_id=attempt_id, - record_type=record_type, - proof=proof, - ) from None - - -@dataclass(frozen=True, slots=True) -class AttemptStartedRecord: - contract_version: int - run_id: str - attempt_id: str - started_at: str - selected: tuple[str, ...] - execution_order: tuple[str, ...] - - def to_json_obj(self) -> dict[str, object]: - return { - "attempt_id": self.attempt_id, - "contract_version": self.contract_version, - "execution_order": list(self.execution_order), - "run_id": self.run_id, - "selected": list(self.selected), - "started_at": self.started_at, - "type": contract.RECORD_TYPE_ATTEMPT_STARTED, - } - - -@dataclass(frozen=True, slots=True) -class ProofTerminalRecord: - contract_version: int - run_id: str - attempt_id: str - proof: str - state: str - checks: tuple[str, ...] - reason: str | None - cleanup_state: str - duration_ms: int | None - finished_at: str - - def to_json_obj(self) -> dict[str, object]: - return { - "attempt_id": self.attempt_id, - "checks": list(self.checks), - "cleanup_state": self.cleanup_state, - "contract_version": self.contract_version, - "duration_ms": self.duration_ms, - "finished_at": self.finished_at, - "proof": self.proof, - "reason": self.reason, - "run_id": self.run_id, - "state": self.state, - "type": contract.RECORD_TYPE_PROOF_TERMINAL, - } - - -@dataclass(frozen=True, slots=True) -class AttemptTerminalRecord: - contract_version: int - run_id: str - attempt_id: str - state: str - terminal_reason: str | None - finished_at: str - - def to_json_obj(self) -> dict[str, object]: - return { - "attempt_id": self.attempt_id, - "contract_version": self.contract_version, - "finished_at": self.finished_at, - "run_id": self.run_id, - "state": self.state, - "terminal_reason": self.terminal_reason, - "type": contract.RECORD_TYPE_ATTEMPT_TERMINAL, - } - - -@dataclass(frozen=True, slots=True) -class ProbeAttemptReplay: - start: AttemptStartedRecord - proofs: tuple[ProofTerminalRecord, ...] - terminal: AttemptTerminalRecord - - -@dataclass(frozen=True, slots=True) -class ProbeReplay: - journal_path: Path - ledger_path: Path - ledger_size_bytes: int - ledger_identity: tuple[int, int] | None - run_id: str | None - attempts: tuple[ProbeAttemptReplay, ...] - retry_permitted: bool - - @property - def attempt_count(self) -> int: - return len(self.attempts) - - -@dataclass(frozen=True, slots=True) -class _ProofRuleContext: - record: ProofTerminalRecord - ordered_checks: tuple[str, ...] - failed_reasons: frozenset[str] - - -@dataclass(frozen=True, slots=True) -class _TerminalDerivationContext: - proofs: tuple[ProofTerminalRecord, ...] - - -@dataclass(frozen=True, slots=True) -class _RetryEligibilityContext: - terminal: AttemptTerminalRecord - proofs: tuple[ProofTerminalRecord, ...] - - -@dataclass(frozen=True, slots=True) -class _CancellationSuffixContext: - proofs: tuple[ProofTerminalRecord, ...] - start_index: int - - -def utc_timestamp_ms() -> str: - return ( - datetime.now(timezone.utc) - .isoformat(timespec="milliseconds") - .replace("+00:00", "Z") - ) - - -def new_attempt_id() -> str: - return str(uuid4()) - - -def validate_canonical_uuid(value: object) -> str: - if not isinstance(value, str): - raise ProbeRecordValidationError("uuid must be a string") - try: - parsed_uuid = UUID(value) - except ValueError as exc: - raise ProbeRecordValidationError("uuid is invalid") from exc - if str(parsed_uuid) != value: - raise ProbeRecordValidationError("uuid must be canonical") - return value - - -def validate_timestamp(value: object) -> str: - if not isinstance(value, str) or _TIMESTAMP_RE.fullmatch(value) is None: - raise ProbeRecordValidationError("timestamp must be RFC3339 UTC milliseconds") - try: - datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) - except ValueError as exc: - raise ProbeRecordValidationError("timestamp is invalid") from exc - return value - - -def validate_selected(value: object) -> tuple[str, ...]: - items = _string_tuple(value, "selected") - if not items: - raise ProbeRecordValidationError("selected must not be empty") - if len(set(items)) != len(items): - raise ProbeRecordValidationError("selected must not contain duplicates") - expected = tuple( - capability - for capability in contract.CAPABILITY_ORDER - if capability in set(items) - ) - if items != expected: - raise ProbeRecordValidationError("selected must be a canonical subsequence") - return items - - -def validate_execution_order(value: object, selected: Sequence[str]) -> tuple[str, ...]: - items = _string_tuple(value, "execution_order") - if len(set(items)) != len(items): - raise ProbeRecordValidationError("execution_order must not contain duplicates") - if set(items) != set(selected): - raise ProbeRecordValidationError("execution_order must be selected permutation") - return items - - -def build_attempt_started_record( - *, - run_id: str, - attempt_id: str, - selected: Sequence[str], - execution_order: Sequence[str], - started_at: str | None = None, -) -> AttemptStartedRecord: - record = AttemptStartedRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(run_id), - attempt_id=validate_canonical_uuid(attempt_id), - started_at=validate_timestamp(started_at or utc_timestamp_ms()), - selected=validate_selected(list(selected)), - execution_order=(), - ) - return AttemptStartedRecord( - contract_version=record.contract_version, - run_id=record.run_id, - attempt_id=record.attempt_id, - started_at=record.started_at, - selected=record.selected, - execution_order=validate_execution_order( - list(execution_order), record.selected - ), - ) - - -def build_proof_terminal_record( - *, - run_id: str, - attempt_id: str, - proof: str, - state: str, - checks: Sequence[str], - reason: str | None, - duration_ms: int | None, - finished_at: str | None = None, -) -> ProofTerminalRecord: - proof = validate_proof_name(proof) - cleanup_state = cleanup_state_for(proof=proof, state=state, reason=reason) - record = ProofTerminalRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(run_id), - attempt_id=validate_canonical_uuid(attempt_id), - proof=proof, - state=state, - checks=tuple(checks), - reason=reason, - cleanup_state=cleanup_state, - duration_ms=duration_ms, - finished_at=validate_timestamp(finished_at or utc_timestamp_ms()), - ) - validate_proof_terminal_semantics(record) - return record - - -def build_attempt_terminal_record( - *, - run_id: str, - attempt_id: str, - proofs: Sequence[ProofTerminalRecord], - finished_at: str | None = None, -) -> AttemptTerminalRecord: - state, terminal_reason = derive_attempt_terminal(proofs) - return AttemptTerminalRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(run_id), - attempt_id=validate_canonical_uuid(attempt_id), - state=state, - terminal_reason=terminal_reason, - finished_at=validate_timestamp(finished_at or utc_timestamp_ms()), - ) - - -def validate_attempt_started_payload( - payload: Mapping[str, Any], -) -> AttemptStartedRecord: - _reject_unknown_fields( - payload, - contract.RECORD_TYPE_ATTEMPT_STARTED, - ) - _validate_contract_version(payload.get("contract_version")) - if payload.get("type") != contract.RECORD_TYPE_ATTEMPT_STARTED: - raise ProbeRecordValidationError("wrong record type") - selected = validate_selected(payload.get("selected")) - return AttemptStartedRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(payload.get("run_id")), - attempt_id=validate_canonical_uuid(payload.get("attempt_id")), - started_at=validate_timestamp(payload.get("started_at")), - selected=selected, - execution_order=validate_execution_order( - payload.get("execution_order"), selected - ), - ) - - -def validate_proof_terminal_payload( - payload: Mapping[str, Any], -) -> ProofTerminalRecord: - _reject_unknown_fields( - payload, - contract.RECORD_TYPE_PROOF_TERMINAL, - ) - _validate_contract_version(payload.get("contract_version")) - if payload.get("type") != contract.RECORD_TYPE_PROOF_TERMINAL: - raise ProbeRecordValidationError("wrong record type") - record = ProofTerminalRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(payload.get("run_id")), - attempt_id=validate_canonical_uuid(payload.get("attempt_id")), - proof=validate_proof_name(payload.get("proof")), - state=_validate_str(payload.get("state"), "state"), - checks=_string_tuple(payload.get("checks"), "checks"), - reason=_validate_reason_or_none(payload.get("reason")), - cleanup_state=_validate_str(payload.get("cleanup_state"), "cleanup_state"), - duration_ms=payload.get("duration_ms"), - finished_at=validate_timestamp(payload.get("finished_at")), - ) - validate_proof_terminal_semantics(record) - return record - - -def validate_attempt_terminal_payload( - payload: Mapping[str, Any], -) -> AttemptTerminalRecord: - _reject_unknown_fields( - payload, - contract.RECORD_TYPE_ATTEMPT_TERMINAL, - ) - _validate_contract_version(payload.get("contract_version")) - if payload.get("type") != contract.RECORD_TYPE_ATTEMPT_TERMINAL: - raise ProbeRecordValidationError("wrong record type") - terminal_reason = payload.get("terminal_reason") - if ( - terminal_reason is not None - and terminal_reason not in contract.ATTEMPT_TERMINAL_REASONS - ): - raise ProbeRecordValidationError("invalid attempt terminal reason") - state = _validate_str(payload.get("state"), "state") - if state not in contract.ATTEMPT_TERMINAL_STATES: - raise ProbeRecordValidationError("invalid attempt terminal state") - return AttemptTerminalRecord( - contract_version=contract.CONTRACT_VERSION, - run_id=validate_canonical_uuid(payload.get("run_id")), - attempt_id=validate_canonical_uuid(payload.get("attempt_id")), - state=state, - terminal_reason=terminal_reason, - finished_at=validate_timestamp(payload.get("finished_at")), - ) - - -def validate_proof_terminal_semantics(record: ProofTerminalRecord) -> None: - rule = contract.PROOF_TERMINAL_RULES.get(record.state) - if rule is None: - raise ProbeRecordValidationError("invalid proof terminal state") - failed_common = rule.get("failed_common_reasons", ()) - if not isinstance(failed_common, tuple): - raise ProbeRecordValidationError("invalid proof terminal rule") - context = _ProofRuleContext( - record=record, - ordered_checks=contract.PROOF_CHECKS[record.proof], - failed_reasons=frozenset( - set(contract.PROOF_SPECIFIC_REASONS[record.proof]) | set(failed_common) - ), - ) - for field in ( - contract.FIELD_CHECKS, - contract.FIELD_REASON, - contract.FIELD_DURATION_MS, - contract.FIELD_CLEANUP_STATE, - ): - predicate_key = rule.get(field) - if not isinstance(predicate_key, str): - raise ProbeRecordValidationError("invalid proof terminal rule") - if not _predicate_matches(predicate_key, context): - raise ProbeRecordValidationError(_proof_terminal_error(record.state, field)) - - -def cleanup_state_for(*, proof: str, state: str, reason: str | None) -> str: - proof = validate_proof_name(proof) - state_overrides = contract.CLEANUP_RESOLUTION["state_overrides"] - reason_overrides = contract.CLEANUP_RESOLUTION["reason_overrides"] - declared_defaults = contract.CLEANUP_RESOLUTION["declared_defaults"] - if not ( - isinstance(state_overrides, dict) - and isinstance(reason_overrides, dict) - and isinstance(declared_defaults, dict) - ): - raise ProbeRecordValidationError("invalid cleanup resolution") - if state in state_overrides: - return _validate_str(state_overrides[state], "cleanup_state") - if reason in reason_overrides: - return _validate_str(reason_overrides[reason], "cleanup_state") - return _validate_str(declared_defaults[proof], "cleanup_state") - - -def derive_attempt_terminal( - proofs: Sequence[ProofTerminalRecord], -) -> tuple[str, str | None]: - if not proofs: - raise ProbeRecordValidationError("attempt needs proof rows") - validate_cancellation_suffix(proofs) - context = _TerminalDerivationContext(proofs=tuple(proofs)) - for rule in contract.TERMINAL_DERIVATION: - predicate_key = rule.get("predicate") - state = rule.get(contract.FIELD_STATE) - terminal_reason = rule.get(contract.FIELD_TERMINAL_REASON) - if not isinstance(predicate_key, str) or not isinstance(state, str): - raise ProbeRecordValidationError("invalid terminal derivation rule") - if _predicate_matches(predicate_key, context): - if terminal_reason is not None and not isinstance(terminal_reason, str): - raise ProbeRecordValidationError("invalid terminal derivation rule") - return state, terminal_reason - raise ProbeRecordValidationError("invalid proof truth table") - - -def attempt_terminal_retry_permitted( - terminal: AttemptTerminalRecord, - proofs: Sequence[ProofTerminalRecord], -) -> bool: - context = _RetryEligibilityContext(terminal=terminal, proofs=tuple(proofs)) - for eligible in contract.RETRY_ELIGIBLE_TERMINALS: - if terminal.state == eligible.get( - contract.FIELD_STATE - ) and terminal.terminal_reason == eligible.get(contract.FIELD_TERMINAL_REASON): - proofs_predicate = eligible.get("proofs") - if proofs_predicate is None: - return True - if not isinstance(proofs_predicate, str): - raise ProbeRecordValidationError("invalid retry eligibility rule") - return _predicate_matches(proofs_predicate, context) - return False - - -def validate_attempt_terminal_matches( - terminal: AttemptTerminalRecord, - proofs: Sequence[ProofTerminalRecord], -) -> None: - expected_state, expected_reason = derive_attempt_terminal(proofs) - if terminal.state != expected_state or terminal.terminal_reason != expected_reason: - raise ProbeRecordValidationError("attempt terminal does not match proof rows") - - -def validate_cancellation_suffix(proofs: Sequence[ProofTerminalRecord]) -> None: - proof_rows = tuple(proofs) - first_cancelled = next( - ( - index - for index, proof in enumerate(proof_rows) - if proof.reason == contract.REASON_CANCELLED - ), - None, - ) - if first_cancelled is None: - return - - first = proof_rows[first_cancelled] - first_context = _ProofRuleContext( - record=first, - ordered_checks=contract.PROOF_CHECKS[first.proof], - failed_reasons=frozenset( - set(contract.PROOF_SPECIFIC_REASONS[first.proof]) - | set(contract.FAILED_COMMON_REASONS) - ), - ) - first_predicates = ( - contract.CANCELLATION["first_started_predicate"], - contract.CANCELLATION["first_unstarted_predicate"], - ) - if not all(isinstance(predicate, str) for predicate in first_predicates): - raise ProbeRecordValidationError("invalid cancellation rule") - if not any( - _predicate_matches(predicate, first_context) - for predicate in first_predicates - if isinstance(predicate, str) - ): - raise ProbeRecordValidationError("invalid cancellation first row") - - suffix_predicate = contract.CANCELLATION["suffix_predicate"] - if not isinstance(suffix_predicate, str): - raise ProbeRecordValidationError("invalid cancellation rule") - suffix_context = _CancellationSuffixContext( - proofs=proof_rows, - start_index=first_cancelled + 1, - ) - if not _predicate_matches(suffix_predicate, suffix_context): - raise ProbeRecordValidationError("invalid cancellation suffix") - - -def _predicate_checks_complete(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return context.record.checks == context.ordered_checks - - -def _predicate_checks_ordered_prefix(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return _is_ordered_prefix(context.record.checks, context.ordered_checks) - - -def _predicate_checks_empty(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return not context.record.checks - - -def _predicate_reason_null(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return context.record.reason is None - - -def _predicate_reason_failed_specific_or_common(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return context.record.reason in context.failed_reasons - - -def _predicate_reason_not_run(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return context.record.reason in contract.NOT_RUN_REASONS - - -def _predicate_duration_non_negative_int(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - try: - validate_non_negative_int(context.record.duration_ms) - except ProbeRecordValidationError: - return False - return True - - -def _predicate_duration_null(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return context.record.duration_ms is None - - -def _predicate_cleanup_expected(context: object) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - expected = cleanup_state_for( - proof=context.record.proof, - state=context.record.state, - reason=context.record.reason, - ) - return context.record.cleanup_state == expected - - -def _predicate_terminal_any_cleanup_unverified(context: object) -> bool: - if not isinstance(context, _TerminalDerivationContext): - return False - return any( - proof.cleanup_state == contract.CLEANUP_STATE_UNVERIFIED - or proof.reason == contract.REASON_CLEANUP_UNVERIFIED - for proof in context.proofs - ) - - -def _predicate_terminal_any_reason_cancelled(context: object) -> bool: - if not isinstance(context, _TerminalDerivationContext): - return False - return any(proof.reason == contract.REASON_CANCELLED for proof in context.proofs) - - -def _predicate_terminal_any_reason_internal_error(context: object) -> bool: - if not isinstance(context, _TerminalDerivationContext): - return False - return any( - proof.reason == contract.REASON_INTERNAL_ERROR for proof in context.proofs - ) - - -def _predicate_terminal_any_failed_proof(context: object) -> bool: - if not isinstance(context, _TerminalDerivationContext): - return False - return any(proof.state == contract.PROOF_STATE_FAILED for proof in context.proofs) - - -def _predicate_terminal_all_passed(context: object) -> bool: - if not isinstance(context, _TerminalDerivationContext): - return False - return all(proof.state == contract.PROOF_STATE_PASSED for proof in context.proofs) - - -def _predicate_retry_all_cleanup_closed(context: object) -> bool: - if not isinstance(context, _RetryEligibilityContext): - return False - return all( - proof.cleanup_state - in { - contract.CLEANUP_STATE_VERIFIED, - contract.CLEANUP_STATE_RETAINED_SYNTHETIC, - } - for proof in context.proofs - ) - - -def _predicate_cancellation_first_failed_cancelled_after_contact( - context: object, -) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return ( - context.record.state == contract.PROOF_STATE_FAILED - and context.record.reason == contract.REASON_CANCELLED - and _is_ordered_prefix(context.record.checks, context.ordered_checks) - and _predicate_duration_non_negative_int(context) - ) - - -def _predicate_cancellation_first_not_run_cancelled_without_contact( - context: object, -) -> bool: - if not isinstance(context, _ProofRuleContext): - return False - return ( - context.record.state == contract.PROOF_STATE_NOT_RUN - and context.record.reason == contract.REASON_CANCELLED - and not context.record.checks - and context.record.duration_ms is None - and context.record.cleanup_state == contract.CLEANUP_STATE_VERIFIED - ) - - -def _predicate_cancellation_contiguous_not_run_cancelled_suffix( - context: object, -) -> bool: - if not isinstance(context, _CancellationSuffixContext): - return False - suffix_shape = contract.CANCELLATION["later_proof"] - if not isinstance(suffix_shape, dict): - return False - for proof in context.proofs[context.start_index :]: - if ( - proof.state != suffix_shape[contract.FIELD_STATE] - or proof.checks != tuple(suffix_shape[contract.FIELD_CHECKS]) - or proof.reason != suffix_shape[contract.FIELD_REASON] - or proof.duration_ms != suffix_shape[contract.FIELD_DURATION_MS] - or proof.cleanup_state != suffix_shape[contract.FIELD_CLEANUP_STATE] - ): - return False - return True - - -PREDICATE_REGISTRY: dict[str, Callable[[object], bool]] = { - contract.PREDICATE_CHECKS_COMPLETE: _predicate_checks_complete, - contract.PREDICATE_CHECKS_ORDERED_PREFIX: _predicate_checks_ordered_prefix, - contract.PREDICATE_CHECKS_EMPTY: _predicate_checks_empty, - contract.PREDICATE_REASON_NULL: _predicate_reason_null, - contract.PREDICATE_REASON_FAILED_SPECIFIC_OR_COMMON: ( - _predicate_reason_failed_specific_or_common - ), - contract.PREDICATE_REASON_NOT_RUN: _predicate_reason_not_run, - contract.PREDICATE_DURATION_NON_NEGATIVE_INT: ( - _predicate_duration_non_negative_int - ), - contract.PREDICATE_DURATION_NULL: _predicate_duration_null, - contract.PREDICATE_CLEANUP_EXPECTED: _predicate_cleanup_expected, - contract.PREDICATE_TERMINAL_ANY_CLEANUP_UNVERIFIED: ( - _predicate_terminal_any_cleanup_unverified - ), - contract.PREDICATE_TERMINAL_ANY_REASON_CANCELLED: ( - _predicate_terminal_any_reason_cancelled - ), - contract.PREDICATE_TERMINAL_ANY_REASON_INTERNAL_ERROR: ( - _predicate_terminal_any_reason_internal_error - ), - contract.PREDICATE_TERMINAL_ANY_FAILED_PROOF: ( - _predicate_terminal_any_failed_proof - ), - contract.PREDICATE_TERMINAL_ALL_PASSED: _predicate_terminal_all_passed, - contract.PREDICATE_RETRY_ALL_CLEANUP_CLOSED: (_predicate_retry_all_cleanup_closed), - contract.PREDICATE_CANCELLATION_FIRST_FAILED_CANCELLED_AFTER_CONTACT: ( - _predicate_cancellation_first_failed_cancelled_after_contact - ), - contract.PREDICATE_CANCELLATION_FIRST_NOT_RUN_CANCELLED_WITHOUT_CONTACT: ( - _predicate_cancellation_first_not_run_cancelled_without_contact - ), - contract.PREDICATE_CANCELLATION_CONTIGUOUS_NOT_RUN_CANCELLED_SUFFIX: ( - _predicate_cancellation_contiguous_not_run_cancelled_suffix - ), -} - - -def _predicate_matches(predicate_key: str, context: object) -> bool: - try: - predicate = PREDICATE_REGISTRY[predicate_key] - except KeyError as exc: - raise ProbeRecordValidationError("unknown contract predicate") from exc - return predicate(context) - - -def _proof_terminal_error(state: str, field: str) -> str: - messages = { - (contract.PROOF_STATE_PASSED, contract.FIELD_CHECKS): ( - "passed checks must be complete" - ), - (contract.PROOF_STATE_PASSED, contract.FIELD_REASON): ( - "passed reason must be null" - ), - (contract.PROOF_STATE_PASSED, contract.FIELD_DURATION_MS): ( - "duration must be a nonnegative integer" - ), - (contract.PROOF_STATE_PASSED, contract.FIELD_CLEANUP_STATE): ( - "invalid cleanup state" - ), - (contract.PROOF_STATE_FAILED, contract.FIELD_CHECKS): ( - "failed checks must be an ordered prefix" - ), - (contract.PROOF_STATE_FAILED, contract.FIELD_REASON): ("invalid failed reason"), - (contract.PROOF_STATE_FAILED, contract.FIELD_DURATION_MS): ( - "duration must be a nonnegative integer" - ), - (contract.PROOF_STATE_FAILED, contract.FIELD_CLEANUP_STATE): ( - "invalid cleanup state" - ), - (contract.PROOF_STATE_NOT_RUN, contract.FIELD_CHECKS): ( - "not_run checks must be empty" - ), - (contract.PROOF_STATE_NOT_RUN, contract.FIELD_REASON): ( - "invalid not_run reason" - ), - (contract.PROOF_STATE_NOT_RUN, contract.FIELD_DURATION_MS): ( - "not_run duration must be null" - ), - (contract.PROOF_STATE_NOT_RUN, contract.FIELD_CLEANUP_STATE): ( - "invalid cleanup state" - ), - } - return messages[(state, field)] - - -def validate_proof_name(value: object) -> str: - proof = _validate_str(value, "proof") - if proof not in contract.CAPABILITY_ORDER: - raise ProbeRecordValidationError("invalid proof") - return proof - - -def validate_non_negative_int(value: object) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise ProbeRecordValidationError("duration must be a nonnegative integer") - return value - - -def _validate_contract_version(value: object) -> None: - if ( - isinstance(value, bool) - or not isinstance(value, int) - or value != contract.CONTRACT_VERSION - ): - raise ProbeRecordValidationError("invalid contract version") - - -def _reject_unknown_fields(payload: Mapping[str, Any], record_type: str) -> None: - fields = contract.RECORD_FIELDS.get(record_type) - if fields is None or set(payload) != set(fields): - raise ProbeRecordValidationError("record fields do not match grammar") - - -def _string_tuple(value: object, field: str) -> tuple[str, ...]: - if not isinstance(value, list): - raise ProbeRecordValidationError(f"{field} must be a list") - if not all(isinstance(item, str) for item in value): - raise ProbeRecordValidationError(f"{field} entries must be strings") - return tuple(value) - - -def _validate_reason_or_none(value: object) -> str | None: - if value is None: - return None - if not isinstance(value, str): - raise ProbeRecordValidationError("reason must be a string or null") - if value not in set(contract.PROOF_REASON_POOL) | set(contract.COMMON_REASONS): - raise ProbeRecordValidationError("unknown reason") - return value - - -def _validate_str(value: object, field: str) -> str: - if not isinstance(value, str): - raise ProbeRecordValidationError(f"{field} must be a string") - return value - - -def _is_ordered_prefix(value: Sequence[str], expected: Sequence[str]) -> bool: - return tuple(value) == tuple(expected[: len(value)]) diff --git a/solstone/think/sandbox_profile/probe_replay.py b/solstone/think/sandbox_profile/probe_replay.py deleted file mode 100644 index e91bebc67..000000000 --- a/solstone/think/sandbox_profile/probe_replay.py +++ /dev/null @@ -1,282 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Fail-closed replay for the sandbox production-probe ledger. - -Replay validates any preexisting ledger as one regular non-symlink file and -reads it through a no-follow descriptor before partial JSONL validation. Torn -records, bounded decode failures including recursion, cardinality mismatches, -attempt-directory mismatches, and unsafe terminal classes become stale attempts -that require external whole-profile stop. The replay result retains ledger -byte-size and identity metadata for the writer-side descriptor checks. -""" - -from __future__ import annotations - -import json -import os -import stat -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -from solstone.think import json_codec -from solstone.think.sandbox_profile import probe_contract as contract -from solstone.think.sandbox_profile import probe_records, probe_slot - -_ReplayRecord = ( - probe_records.AttemptStartedRecord - | probe_records.ProofTerminalRecord - | probe_records.AttemptTerminalRecord -) -_RECORD_VALIDATORS: dict[str, Callable[[Mapping[str, Any]], _ReplayRecord]] = { - contract.RECORD_TYPE_ATTEMPT_STARTED: probe_records.validate_attempt_started_payload, - contract.RECORD_TYPE_PROOF_TERMINAL: probe_records.validate_proof_terminal_payload, - contract.RECORD_TYPE_ATTEMPT_TERMINAL: probe_records.validate_attempt_terminal_payload, -} - - -@dataclass(frozen=True, slots=True) -class _LedgerRead: - payloads: tuple[dict[str, Any], ...] - size: int - identity: tuple[int, int] | None - - -def replay_probe_ledger(journal_path: Path) -> probe_records.ProbeReplay: - journal = Path(journal_path) - ledger_path = contract.probe_ledger_path(journal) - ledger = _read_framed_payloads(ledger_path) - raw_payloads = ledger.payloads - attempt_count_type = contract.RECORD_CARDINALITY["attempt_count_type"] - attempt_count = sum( - 1 for payload in raw_payloads if payload.get("type") == attempt_count_type - ) - if attempt_count >= contract.MAX_ATTEMPTS: - probe_records.raise_probe_error(contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - attempts, run_id, attempt_ids = _fold_records(raw_payloads) - probe_slot.validate_attempt_directory_set(journal, attempt_ids) - retry_permitted = _retry_permitted(attempts) - if not retry_permitted: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - return probe_records.ProbeReplay( - journal_path=journal, - ledger_path=ledger_path, - ledger_size_bytes=ledger.size, - ledger_identity=ledger.identity, - run_id=run_id, - attempts=attempts, - retry_permitted=retry_permitted, - ) - - -def _read_framed_payloads(ledger_path: Path) -> _LedgerRead: - try: - ledger_stat = ledger_path.lstat() - except FileNotFoundError: - return _LedgerRead(payloads=(), size=0, identity=None) - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if stat.S_ISLNK(ledger_stat.st_mode) or not stat.S_ISREG(ledger_stat.st_mode): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - size = ledger_stat.st_size - if size > contract.MAX_LEDGER_BYTES: - probe_records.raise_probe_error(contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - identity = (ledger_stat.st_dev, ledger_stat.st_ino) - fd: int | None = None - try: - fd = os.open(ledger_path, os.O_RDONLY | os.O_NOFOLLOW) - fd_stat = os.fstat(fd) - if ( - not stat.S_ISREG(fd_stat.st_mode) - or (fd_stat.st_dev, fd_stat.st_ino) != identity - or fd_stat.st_size != size - ): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - raw = os.read(fd, size + 1) - if len(raw) != size: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - except probe_records.ProbeOperationError: - raise - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - finally: - if fd is not None: - os.close(fd) - if not raw: - return _LedgerRead(payloads=(), size=size, identity=identity) - if not raw.endswith(b"\n"): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - payloads: list[dict[str, Any]] = [] - for line in raw.splitlines(keepends=True): - if line == b"\n" or not line.endswith(b"\n"): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - body = line[:-1] - try: - text = body.decode("utf-8") - decoder = json.JSONDecoder( - object_pairs_hook=json_codec.reject_duplicate_keys - ) - payload, end = decoder.raw_decode(text) - except (UnicodeDecodeError, ValueError, json.JSONDecodeError, RecursionError): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if end != len(text) or not isinstance(payload, dict): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - payloads.append(payload) - return _LedgerRead(payloads=tuple(payloads), size=size, identity=identity) - - -def _fold_records( - payloads: tuple[dict[str, Any], ...], -) -> tuple[tuple[probe_records.ProbeAttemptReplay, ...], str | None, set[str]]: - if not payloads: - return (), None, set() - - run_id: str | None = None - seen_attempt_ids: set[str] = set() - attempts: list[probe_records.ProbeAttemptReplay] = [] - index = 0 - prior_retry_permitted = True - attempt_sequence = _attempt_sequence() - while index < len(payloads): - if not prior_retry_permitted: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - start: probe_records.AttemptStartedRecord | None = None - proofs: list[probe_records.ProofTerminalRecord] = [] - terminal: probe_records.AttemptTerminalRecord | None = None - for step in attempt_sequence: - record_type = _step_record_type(step) - if record_type == contract.RECORD_TYPE_ATTEMPT_STARTED: - _require_step_count(step, 1) - if index >= len(payloads): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - record = _validate_step_record(record_type, payloads[index]) - if not isinstance(record, probe_records.AttemptStartedRecord): - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - start = record - run_id = _merge_run_id(run_id, start.run_id) - if start.attempt_id in seen_attempt_ids: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - seen_attempt_ids.add(start.attempt_id) - index += 1 - elif record_type == contract.RECORD_TYPE_PROOF_TERMINAL: - if start is None: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - if step.get("count") != "len(execution_order)": - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - if step.get("order") != "execution_order": - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - for expected_proof in start.execution_order: - if index >= len(payloads): - probe_records.raise_probe_error( - contract.STABLE_ERROR_STALE_ATTEMPT - ) - record = _validate_step_record(record_type, payloads[index]) - if not isinstance(record, probe_records.ProofTerminalRecord): - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - run_id = _merge_run_id(run_id, record.run_id) - if ( - record.attempt_id != start.attempt_id - or record.proof != expected_proof - ): - probe_records.raise_probe_error( - contract.STABLE_ERROR_STALE_ATTEMPT - ) - proofs.append(record) - index += 1 - elif record_type == contract.RECORD_TYPE_ATTEMPT_TERMINAL: - _require_step_count(step, 1) - if start is None: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - if index >= len(payloads): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - record = _validate_step_record(record_type, payloads[index]) - if not isinstance(record, probe_records.AttemptTerminalRecord): - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR - ) - terminal = record - run_id = _merge_run_id(run_id, terminal.run_id) - if terminal.attempt_id != start.attempt_id: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - try: - probe_records.validate_attempt_terminal_matches(terminal, proofs) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - index += 1 - else: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - if start is None or terminal is None: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - attempt = probe_records.ProbeAttemptReplay( - start=start, - proofs=tuple(proofs), - terminal=terminal, - ) - attempts.append(attempt) - prior_retry_permitted = probe_records.attempt_terminal_retry_permitted( - terminal, proofs - ) - return tuple(attempts), run_id, seen_attempt_ids - - -def _attempt_sequence() -> tuple[dict[str, object], ...]: - sequence = contract.RECORD_CARDINALITY.get("attempt_sequence") - if not isinstance(sequence, tuple) or not all( - isinstance(step, dict) for step in sequence - ): - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - return sequence - - -def _step_record_type(step: dict[str, object]) -> str: - record_type = step.get("type") - if not isinstance(record_type, str) or record_type not in contract.RECORD_TYPES: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - return record_type - - -def _require_step_count(step: dict[str, object], expected: int) -> None: - if step.get("count") != expected: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - - -def _validate_step_record( - record_type: str, - payload: Mapping[str, Any], -) -> _ReplayRecord: - validator = _RECORD_VALIDATORS.get(record_type) - if validator is None: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - try: - return validator(payload) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - -def _merge_run_id(existing: str | None, value: str) -> str: - if existing is not None and existing != value: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - return value - - -def _retry_permitted(attempts: tuple[probe_records.ProbeAttemptReplay, ...]) -> bool: - if not attempts: - return True - latest = attempts[-1] - return probe_records.attempt_terminal_retry_permitted( - latest.terminal, latest.proofs - ) diff --git a/solstone/think/sandbox_profile/probe_slot.py b/solstone/think/sandbox_profile/probe_slot.py deleted file mode 100644 index a3a0e7983..000000000 --- a/solstone/think/sandbox_profile/probe_slot.py +++ /dev/null @@ -1,410 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""OS-level probe slot ownership and attempt-directory identity.""" - -from __future__ import annotations - -import errno -import fcntl -import os -import stat -import threading -from collections.abc import Iterator -from contextlib import contextmanager -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -from solstone.think.sandbox_profile import probe_contract as contract -from solstone.think.sandbox_profile import probe_durability, probe_records - -SLOT_STATE_UNUSED = "unused" -SLOT_STATE_ACTIVE = "active" -SLOT_STATE_SPENT = "spent" -SLOT_STATE_POISONED = "poisoned" - - -@dataclass(slots=True) -class ProbeSlot: - journal_path: Path - run_id: str - ledger_path: Path - lock_path: Path - attempts_parent_path: Path - _lock_fd: int | None - _lock_identity: tuple[int, int] - _ledger_fd: int | None - _ledger_identity: tuple[int, int] - _ledger_tracked_size: int - state: str = SLOT_STATE_UNUSED - _poisoned_code: str | None = None - _operation_lock: threading.RLock = field(default_factory=threading.RLock) - _operation_active: bool = False - - @property - def owned(self) -> bool: - if self._lock_fd is None: - return False - try: - os.fstat(self._lock_fd) - except OSError: - return False - return True - - @property - def ledger_size_bytes(self) -> int: - return self._ledger_tracked_size - - @contextmanager - def operation_guard(self) -> Iterator[None]: - self._operation_lock.acquire() - if self._operation_active: - self._operation_lock.release() - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - self._operation_active = True - try: - yield - finally: - self._operation_active = False - self._operation_lock.release() - - def release(self) -> None: - with self.operation_guard(): - self._release_unlocked() - - def __enter__(self) -> ProbeSlot: - return self - - def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: - self.release() - - def assert_owned_unlocked(self) -> None: - if not self.owned: - probe_records.raise_probe_error(contract.STABLE_ERROR_PROBE_ACTIVE) - - def assert_active_writer_unlocked(self, attempt_id: str) -> None: - self._raise_if_poisoned_unlocked(attempt_id=attempt_id) - if self.state != SLOT_STATE_ACTIVE: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, attempt_id=attempt_id - ) - - def revalidate_identities_unlocked(self) -> None: - self.assert_owned_unlocked() - self._revalidate_lock_identity_unlocked() - self._revalidate_ledger_identity_unlocked() - - def check_ledger_capacity_unlocked( - self, - data: bytes, - *, - poison_on_failure: bool, - ) -> None: - if self._ledger_tracked_size + len(data) <= contract.MAX_LEDGER_BYTES: - return - if poison_on_failure: - self._poison_unlocked(contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - probe_records.raise_probe_error(contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - - def append_encoded_record_unlocked( - self, - data: bytes, - *, - record_type: str, - attempt_id: str, - capacity_checked: bool = False, - poison_on_overflow: bool = True, - ) -> None: - self.revalidate_identities_unlocked() - if not capacity_checked: - self.check_ledger_capacity_unlocked( - data, poison_on_failure=poison_on_overflow - ) - fd = self._ledger_fd - if fd is None: - probe_records.raise_probe_error( - contract.STABLE_ERROR_PROBE_ACTIVE, - attempt_id=attempt_id, - record_type=record_type, - ) - try: - probe_durability.append_jsonl_strict( - fd, - self.ledger_path.parent, - data, - ) - except probe_records.ProbeOperationError as exc: - self._poison_unlocked(exc.code) - probe_records.raise_probe_error( - exc.code, - attempt_id=attempt_id, - record_type=record_type, - ) - self._ledger_tracked_size += len(data) - - def mark_spent_unlocked(self) -> None: - if self.state != SLOT_STATE_POISONED: - self.state = SLOT_STATE_SPENT - - def _poison_unlocked(self, code: str) -> None: - if code not in contract.STABLE_ERRORS: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - if self._poisoned_code is None: - self._poisoned_code = code - self.state = SLOT_STATE_POISONED - - def _raise_if_poisoned_unlocked(self, *, attempt_id: str | None = None) -> None: - if self._poisoned_code is not None: - probe_records.raise_probe_error(self._poisoned_code, attempt_id=attempt_id) - - def _release_unlocked(self) -> None: - self.mark_spent_unlocked() - ledger_fd = self._ledger_fd - lock_fd = self._lock_fd - self._ledger_fd = None - self._lock_fd = None - for fd in (ledger_fd, lock_fd): - if fd is None: - continue - try: - os.close(fd) - except OSError: - pass - - def _revalidate_lock_identity_unlocked(self) -> None: - try: - current = self.lock_path.lstat() - except OSError: - self._poison_unlocked(contract.STABLE_ERROR_STALE_ATTEMPT) - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if ( - stat.S_ISLNK(current.st_mode) - or not stat.S_ISREG(current.st_mode) - or (current.st_dev, current.st_ino) != self._lock_identity - ): - self._poison_unlocked(contract.STABLE_ERROR_STALE_ATTEMPT) - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - def _revalidate_ledger_identity_unlocked(self) -> None: - try: - current = self.ledger_path.lstat() - except OSError: - self._poison_unlocked(contract.STABLE_ERROR_STALE_ATTEMPT) - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if ( - stat.S_ISLNK(current.st_mode) - or not stat.S_ISREG(current.st_mode) - or (current.st_dev, current.st_ino) != self._ledger_identity - or current.st_size != self._ledger_tracked_size - ): - self._poison_unlocked(contract.STABLE_ERROR_STALE_ATTEMPT) - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - -def acquire_probe_slot(journal_path: Path, *, run_id: str) -> ProbeSlot: - journal = Path(journal_path) - try: - run_id = probe_records.validate_canonical_uuid(run_id) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - - lock_fd: int | None = None - ledger_fd: int | None = None - try: - lock_path = contract.probe_lock_path(journal) - lock_fd, lock_identity = _open_lock_fd(lock_path) - - from solstone.think.sandbox_profile.probe_replay import replay_probe_ledger - - replay = replay_probe_ledger(journal) - if replay.run_id is not None and replay.run_id != run_id: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - ledger_path = contract.probe_ledger_path(journal) - ledger_fd, ledger_identity, tracked_size = _open_ledger_fd( - ledger_path, - replay_identity=replay.ledger_identity, - replay_size=replay.ledger_size_bytes, - ) - return ProbeSlot( - journal_path=journal, - run_id=run_id, - ledger_path=ledger_path, - lock_path=lock_path, - attempts_parent_path=contract.probe_attempts_parent_path(journal), - _lock_fd=lock_fd, - _lock_identity=lock_identity, - _ledger_fd=ledger_fd, - _ledger_identity=ledger_identity, - _ledger_tracked_size=tracked_size, - ) - except probe_records.ProbeOperationError: - _close_fd_quietly(ledger_fd) - _close_fd_quietly(lock_fd) - raise - except OSError: - _close_fd_quietly(ledger_fd) - _close_fd_quietly(lock_fd) - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - except BaseException: - _close_fd_quietly(ledger_fd) - _close_fd_quietly(lock_fd) - raise - - -def create_attempt_directory_unlocked(slot: ProbeSlot, attempt_id: str) -> Path: - attempt_id = _validate_attempt_id_for_error(attempt_id) - slot.revalidate_identities_unlocked() - path = slot.attempts_parent_path / attempt_id - try: - probe_durability._mkdir( - slot.attempts_parent_path, - mode=contract.ATTEMPT_DIR_MODE, - parents=True, - ) - probe_durability._mkdir(path, mode=contract.ATTEMPT_DIR_MODE) - probe_durability._fsync_directory(slot.attempts_parent_path) - except FileExistsError: - slot._poison_unlocked(contract.STABLE_ERROR_STALE_ATTEMPT) - probe_records.raise_probe_error( - contract.STABLE_ERROR_STALE_ATTEMPT, attempt_id=attempt_id - ) - except OSError: - slot._poison_unlocked(contract.STABLE_ERROR_RECORD_WRITE_FAILED) - probe_records.raise_probe_error( - contract.STABLE_ERROR_RECORD_WRITE_FAILED, attempt_id=attempt_id - ) - return path - - -def validate_attempt_directory_set(journal_path: Path, attempt_ids: set[str]) -> None: - parent = contract.probe_attempts_parent_path(journal_path) - if not parent.exists(): - if attempt_ids: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - return - try: - parent_stat = parent.lstat() - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - seen: set[str] = set() - try: - children = sorted(parent.iterdir(), key=lambda item: item.name) - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - for child in children: - try: - attempt_id = probe_records.validate_canonical_uuid(child.name) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - try: - child_stat = child.lstat() - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if stat.S_ISLNK(child_stat.st_mode) or not stat.S_ISDIR(child_stat.st_mode): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if stat.S_IMODE(child_stat.st_mode) != contract.ATTEMPT_DIR_MODE: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if attempt_id not in attempt_ids or attempt_id in seen: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - seen.add(attempt_id) - if seen != attempt_ids: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - -def _open_lock_fd(path: Path) -> tuple[int, tuple[int, int]]: - probe_durability._mkdir(path.parent, mode=contract.ATTEMPT_DIR_MODE, parents=True) - try: - path_stat = path.lstat() - except FileNotFoundError: - path_stat = None - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if path_stat is not None and ( - stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) - ): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - fd: int | None = None - try: - fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600) - os.fchmod(fd, 0o600) - fd_stat = os.fstat(fd) - if not stat.S_ISREG(fd_stat.st_mode): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - try: - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError as exc: - if exc.errno in {errno.EACCES, errno.EAGAIN, errno.EWOULDBLOCK}: - probe_records.raise_probe_error(contract.STABLE_ERROR_PROBE_ACTIVE) - raise - return fd, (fd_stat.st_dev, fd_stat.st_ino) - except probe_records.ProbeOperationError: - _close_fd_quietly(fd) - raise - except OSError as exc: - _close_fd_quietly(fd) - if exc.errno == errno.ELOOP: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - raise - - -def _open_ledger_fd( - path: Path, - *, - replay_identity: tuple[int, int] | None, - replay_size: int, -) -> tuple[int, tuple[int, int], int]: - probe_durability._mkdir(path.parent, mode=contract.ATTEMPT_DIR_MODE, parents=True) - try: - path_stat = path.lstat() - except FileNotFoundError: - path_stat = None - except OSError: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - if path_stat is not None and ( - stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) - ): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - - fd: int | None = None - try: - fd = probe_durability._open_append(path) - fd_stat = os.fstat(fd) - if not stat.S_ISREG(fd_stat.st_mode): - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - identity = (fd_stat.st_dev, fd_stat.st_ino) - if replay_identity is not None: - if identity != replay_identity or fd_stat.st_size != replay_size: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - elif fd_stat.st_size != 0: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - return fd, identity, fd_stat.st_size - except probe_records.ProbeOperationError: - _close_fd_quietly(fd) - raise - except OSError as exc: - _close_fd_quietly(fd) - if exc.errno == errno.ELOOP: - probe_records.raise_probe_error(contract.STABLE_ERROR_STALE_ATTEMPT) - raise - - -def _validate_attempt_id_for_error(value: str) -> str: - try: - return probe_records.validate_canonical_uuid(value) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - - -def _close_fd_quietly(fd: int | None) -> None: - if fd is None: - return - try: - os.close(fd) - except OSError: - pass diff --git a/solstone/think/sandbox_profile/probe_writer.py b/solstone/think/sandbox_profile/probe_writer.py deleted file mode 100644 index cbe7c101c..000000000 --- a/solstone/think/sandbox_profile/probe_writer.py +++ /dev/null @@ -1,374 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Ordered writer for the sandbox production-probe ledger.""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence -from dataclasses import dataclass, field -from pathlib import Path -from typing import TypeVar - -from solstone.think.sandbox_profile import probe_contract as contract -from solstone.think.sandbox_profile import probe_durability, probe_records, probe_slot - -T = TypeVar("T") - - -@dataclass(slots=True) -class ProbeAttemptWriter: - slot: probe_slot.ProbeSlot - start: probe_records.AttemptStartedRecord - attempt_dir: Path - _proofs: list[probe_records.ProofTerminalRecord] = field(default_factory=list) - _next_proof_index: int = 0 - _proof_terminal_count: int = 0 - _terminal_written: bool = False - _contact_consumed: set[str] = field(default_factory=set) - - def dispatch_contact(self, proof: str, operation: Callable[[], T]) -> T: - with self.slot.operation_guard(): - self._raise_if_not_active_unlocked() - self._reject_after_terminal_unlocked() - self.slot.revalidate_identities_unlocked() - expected = self._next_proof_unlocked() - try: - proof = probe_records.validate_proof_name(proof) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - if proof != expected: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - if proof in self._contact_consumed: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - self._contact_consumed.add(proof) - return operation() - - def write_proof_terminal( - self, - *, - proof: str, - state: str, - checks: Sequence[str], - reason: str | None, - duration_ms: int | None, - finished_at: str | None = None, - ) -> None: - with self.slot.operation_guard(): - self._raise_if_not_active_unlocked() - self._reject_after_terminal_unlocked() - self.slot.revalidate_identities_unlocked() - expected = self._next_proof_unlocked() - try: - proof = probe_records.validate_proof_name(proof) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - if proof != expected: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - self._validate_terminal_contact_unlocked(proof=proof, state=state) - try: - record = probe_records.build_proof_terminal_record( - run_id=self.start.run_id, - attempt_id=self.start.attempt_id, - proof=proof, - state=state, - checks=checks, - reason=reason, - duration_ms=duration_ms, - finished_at=finished_at, - ) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - self._append_record_unlocked( - record.to_json_obj(), - record_type=contract.RECORD_TYPE_PROOF_TERMINAL, - ) - self._proofs.append(record) - self._next_proof_index += 1 - self._proof_terminal_count += 1 - - def write_cancelled_attempt( - self, - *, - proof: str, - state: str, - checks: Sequence[str], - duration_ms: int | None, - finished_at: str | None = None, - ) -> None: - with self.slot.operation_guard(): - self._raise_if_not_active_unlocked() - self._reject_after_terminal_unlocked() - self.slot.revalidate_identities_unlocked() - try: - first = self._build_first_cancelled_record_unlocked( - proof=proof, - state=state, - checks=checks, - duration_ms=duration_ms, - finished_at=finished_at, - ) - except ( - probe_records.ProbeRecordValidationError, - probe_records.ProbeOperationError, - ): - self.slot._poison_unlocked(contract.STABLE_ERROR_INTERNAL_ERROR) - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - - self._append_record_unlocked( - first.to_json_obj(), - record_type=contract.RECORD_TYPE_PROOF_TERMINAL, - ) - self._proofs.append(first) - self._next_proof_index += 1 - self._proof_terminal_count += 1 - - try: - later_shape = _cancellation_later_proof_shape() - except probe_records.ProbeRecordValidationError: - self.slot._poison_unlocked(contract.STABLE_ERROR_INTERNAL_ERROR) - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - for later_proof in self.start.execution_order[self._next_proof_index :]: - try: - record = probe_records.build_proof_terminal_record( - run_id=self.start.run_id, - attempt_id=self.start.attempt_id, - proof=later_proof, - state=later_shape.state, - checks=later_shape.checks, - reason=later_shape.reason, - duration_ms=later_shape.duration_ms, - finished_at=finished_at, - ) - if record.cleanup_state != later_shape.cleanup_state: - raise probe_records.ProbeRecordValidationError( - "invalid cancellation later proof shape" - ) - except probe_records.ProbeRecordValidationError: - self.slot._poison_unlocked(contract.STABLE_ERROR_INTERNAL_ERROR) - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, - proof=later_proof, - ) - self._append_record_unlocked( - record.to_json_obj(), - record_type=contract.RECORD_TYPE_PROOF_TERMINAL, - ) - self._proofs.append(record) - self._next_proof_index += 1 - self._proof_terminal_count += 1 - - try: - terminal = probe_records.build_attempt_terminal_record( - run_id=self.start.run_id, - attempt_id=self.start.attempt_id, - proofs=self._proofs, - finished_at=finished_at, - ) - except probe_records.ProbeRecordValidationError: - self.slot._poison_unlocked(contract.STABLE_ERROR_INTERNAL_ERROR) - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - self._append_record_unlocked( - terminal.to_json_obj(), - record_type=contract.RECORD_TYPE_ATTEMPT_TERMINAL, - ) - self._terminal_written = True - self.slot.mark_spent_unlocked() - - def write_attempt_terminal( - self, - *, - finished_at: str | None = None, - ) -> None: - with self.slot.operation_guard(): - self._raise_if_not_active_unlocked() - self._reject_after_terminal_unlocked() - self.slot.revalidate_identities_unlocked() - if self._proof_terminal_count != len(self.start.execution_order): - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - try: - record = probe_records.build_attempt_terminal_record( - run_id=self.start.run_id, - attempt_id=self.start.attempt_id, - proofs=self._proofs, - finished_at=finished_at, - ) - except probe_records.ProbeRecordValidationError: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - self._append_record_unlocked( - record.to_json_obj(), - record_type=contract.RECORD_TYPE_ATTEMPT_TERMINAL, - ) - self._terminal_written = True - self.slot.mark_spent_unlocked() - - def _append_record_unlocked( - self, record: dict[str, object], *, record_type: str - ) -> None: - data = probe_durability.encode_jsonl_record(record) - self.slot.append_encoded_record_unlocked( - data, - record_type=record_type, - attempt_id=self.start.attempt_id, - ) - - def _raise_if_not_active_unlocked(self) -> None: - self.slot.assert_active_writer_unlocked(self.start.attempt_id) - - def _reject_after_terminal_unlocked(self) -> None: - if self._terminal_written: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, attempt_id=self.start.attempt_id - ) - - def _next_proof_unlocked(self) -> str: - if self._next_proof_index >= len(self.start.execution_order): - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, attempt_id=self.start.attempt_id - ) - return self.start.execution_order[self._next_proof_index] - - def _build_first_cancelled_record_unlocked( - self, - *, - proof: str, - state: str, - checks: Sequence[str], - duration_ms: int | None, - finished_at: str | None, - ) -> probe_records.ProofTerminalRecord: - expected = self._next_proof_unlocked() - proof = probe_records.validate_proof_name(proof) - if proof != expected: - raise probe_records.ProbeRecordValidationError("wrong cancellation proof") - consumed = proof in self._contact_consumed - if consumed and state != contract.PROOF_STATE_FAILED: - raise probe_records.ProbeRecordValidationError("invalid cancellation state") - if not consumed and state != contract.PROOF_STATE_NOT_RUN: - raise probe_records.ProbeRecordValidationError("invalid cancellation state") - return probe_records.build_proof_terminal_record( - run_id=self.start.run_id, - attempt_id=self.start.attempt_id, - proof=proof, - state=state, - checks=checks, - reason=contract.REASON_CANCELLED, - duration_ms=duration_ms, - finished_at=finished_at, - ) - - def _validate_terminal_contact_unlocked(self, *, proof: str, state: str) -> None: - consumed = proof in self._contact_consumed - if state in {contract.PROOF_STATE_PASSED, contract.PROOF_STATE_FAILED}: - if not consumed: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - return - if state == contract.PROOF_STATE_NOT_RUN and consumed: - probe_records.raise_probe_error( - contract.STABLE_ERROR_INTERNAL_ERROR, proof=proof - ) - - -def begin_probe_attempt( - slot: probe_slot.ProbeSlot, - *, - selected: Sequence[str], - execution_order: Sequence[str], - attempt_id: str | None = None, - started_at: str | None = None, -) -> ProbeAttemptWriter: - with slot.operation_guard(): - if slot.state != probe_slot.SLOT_STATE_UNUSED: - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - slot.state = probe_slot.SLOT_STATE_ACTIVE - - attempt_id = attempt_id or probe_records.new_attempt_id() - try: - start = probe_records.build_attempt_started_record( - run_id=slot.run_id, - attempt_id=attempt_id, - selected=selected, - execution_order=execution_order, - started_at=started_at, - ) - except probe_records.ProbeRecordValidationError: - slot.mark_spent_unlocked() - probe_records.raise_probe_error(contract.STABLE_ERROR_INTERNAL_ERROR) - - data = probe_durability.encode_jsonl_record(start.to_json_obj()) - slot.revalidate_identities_unlocked() - try: - slot.check_ledger_capacity_unlocked(data, poison_on_failure=False) - except probe_records.ProbeOperationError: - slot.mark_spent_unlocked() - raise - - attempt_dir = probe_slot.create_attempt_directory_unlocked( - slot, start.attempt_id - ) - slot.append_encoded_record_unlocked( - data, - record_type=contract.RECORD_TYPE_ATTEMPT_STARTED, - attempt_id=start.attempt_id, - capacity_checked=True, - ) - return ProbeAttemptWriter(slot=slot, start=start, attempt_dir=attempt_dir) - - -@dataclass(frozen=True, slots=True) -class _CancellationLaterProofShape: - state: str - checks: tuple[str, ...] - reason: str | None - duration_ms: int | None - cleanup_state: str - - -def _cancellation_later_proof_shape() -> _CancellationLaterProofShape: - try: - shape = contract.CANCELLATION["later_proof"] - except KeyError as exc: - raise probe_records.ProbeRecordValidationError( - "invalid cancellation rule" - ) from exc - if not isinstance(shape, dict): - raise probe_records.ProbeRecordValidationError("invalid cancellation rule") - try: - state = shape[contract.FIELD_STATE] - checks = shape[contract.FIELD_CHECKS] - reason = shape[contract.FIELD_REASON] - duration_ms = shape[contract.FIELD_DURATION_MS] - cleanup_state = shape[contract.FIELD_CLEANUP_STATE] - except KeyError as exc: - raise probe_records.ProbeRecordValidationError( - "invalid cancellation rule" - ) from exc - if not isinstance(state, str): - raise probe_records.ProbeRecordValidationError("invalid cancellation rule") - if not isinstance(checks, tuple) or not all( - isinstance(check, str) for check in checks - ): - raise probe_records.ProbeRecordValidationError("invalid cancellation rule") - if reason is not None and not isinstance(reason, str): - raise probe_records.ProbeRecordValidationError("invalid cancellation rule") - if duration_ms is not None: - probe_records.validate_non_negative_int(duration_ms) - if not isinstance(cleanup_state, str): - raise probe_records.ProbeRecordValidationError("invalid cancellation rule") - return _CancellationLaterProofShape( - state=state, - checks=checks, - reason=reason, - duration_ms=duration_ms, - cleanup_state=cleanup_state, - ) diff --git a/solstone/think/sandbox_profile/scout_provider_child.py b/solstone/think/sandbox_profile/scout_provider_child.py deleted file mode 100644 index 283eba94a..000000000 --- a/solstone/think/sandbox_profile/scout_provider_child.py +++ /dev/null @@ -1,205 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Isolated child entry point for the Scout provider proof.""" - -from __future__ import annotations - -import hashlib -import os -import sys -import warnings -from typing import Any - -import httpx - -from solstone.think.providers.shared import ( - CANNED_GENERATE_MAX_OUTPUT_TOKENS, - CANNED_GENERATE_NUM_RETRIES, - CANNED_GENERATE_THINKING_BUDGET, -) -from solstone.think.sandbox_profile import probe_contract -from solstone.think.sandbox_profile.scout_provider_probe import ( - DEFAULT_GOOGLE_MODEL, - FRAME_PROTOCOL_VERSION, - SCOUT_RESPONSE_SCHEMA, - STDIN_FRAME_MAX_BYTES, - STDOUT_FRAME_MAX_BYTES, - GeminiSingleRequestTransport, - ScoutProbeError, - decode_frame, - decode_model_json, - encode_frame, - scout_prompt, -) - - -def main() -> int: - _make_standard_fds_close_on_exec() - try: - payload = _read_stdin_frame() - result = _run_from_payload(payload) - except ScoutProbeError as exc: - result = _stable_result(exc.reason) - except (OSError, ValueError, TypeError): - result = _stable_result(probe_contract.REASON_INTERNAL_ERROR) - try: - sys.stdout.buffer.write(encode_frame(result, cap=STDOUT_FRAME_MAX_BYTES)) - sys.stdout.buffer.flush() - except OSError: - return 1 - return 0 - - -def _make_standard_fds_close_on_exec() -> None: - for fd in (0, 1, 2): - try: - os.set_inheritable(fd, False) - except OSError: - pass - - -def _read_stdin_frame() -> dict[str, Any]: - data = sys.stdin.buffer.read(STDIN_FRAME_MAX_BYTES + 5) - if len(data) > STDIN_FRAME_MAX_BYTES + 4: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - payload = decode_frame(data, cap=STDIN_FRAME_MAX_BYTES) - if payload.get("protocol_version") != FRAME_PROTOCOL_VERSION: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - if not isinstance(payload.get("api_key"), str) or not payload["api_key"]: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - if not isinstance(payload.get("nonce"), str) or not payload["nonce"]: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - timeout_s = payload.get("timeout_s") - if ( - not isinstance(timeout_s, (int, float)) - or isinstance(timeout_s, bool) - or timeout_s <= 0 - ): - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - return payload - - -def _run_from_payload(payload: dict[str, Any]) -> dict[str, object]: - transport = GeminiSingleRequestTransport() - return _run_completion( - api_key=str(payload["api_key"]), - nonce=str(payload["nonce"]), - timeout_s=float(payload["timeout_s"]), - transport=transport, - ) - - -def _run_completion( - *, - api_key: str, - nonce: str, - timeout_s: float, - transport: GeminiSingleRequestTransport, -) -> dict[str, object]: - try: - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message=r"Cost calculation failed:.*", - category=UserWarning, - ) - result = _complete( - api_key=api_key, - nonce=nonce, - timeout_s=timeout_s, - transport=transport, - ) - except ScoutProbeError as exc: - return _stable_result(exc.reason) - except Exception: - if transport.failure_reason is not None: - return _stable_result(transport.failure_reason) - if transport.request_count == 1: - return _stable_result(probe_contract.REASON_REMOTE_REJECTED) - return _stable_result(probe_contract.REASON_INTERNAL_ERROR) - return result - - -def _complete( - *, - api_key: str, - nonce: str, - timeout_s: float, - transport: GeminiSingleRequestTransport, -) -> dict[str, object]: - import litellm - from litellm.llms.custom_httpx.http_handler import HTTPHandler - - from solstone.think.providers.openhands import ( - _build_generate_llm, - _generate_call_kwargs, - _generate_messages, - _generate_result, - _openhands_import_policy, - ) - - litellm.disable_hf_tokenizer_download = True - with _openhands_import_policy(): - llm, _ = _build_generate_llm( - "google", - DEFAULT_GOOGLE_MODEL, - max_output_tokens=CANNED_GENERATE_MAX_OUTPUT_TOKENS, - thinking_budget=CANNED_GENERATE_THINKING_BUDGET, - timeout_s=timeout_s, - api_key=api_key, - num_retries=CANNED_GENERATE_NUM_RETRIES, - ) - messages = _generate_messages(scout_prompt(nonce), None) - call_kwargs = _generate_call_kwargs( - "google", - DEFAULT_GOOGLE_MODEL, - temperature=0, - json_output=False, - json_schema=SCOUT_RESPONSE_SCHEMA, - thinking_budget=CANNED_GENERATE_THINKING_BUDGET, - responses_api=False, - ) - with httpx.Client( - transport=transport, - trust_env=False, - follow_redirects=False, - timeout=timeout_s, - ) as client: - response = llm.completion( - messages, - client=HTTPHandler(client=client), - **call_kwargs, - ) - generated = _generate_result(response, DEFAULT_GOOGLE_MODEL) - text = generated.get("text") - if not isinstance(text, str): - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) - model_payload = decode_model_json(text) - echo = model_payload["nonce"] - if not isinstance(echo, str): - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) - return { - "protocol_version": FRAME_PROTOCOL_VERSION, - "result": "ok", - "nonce_sha256": hashlib.sha256(echo.encode("utf-8")).hexdigest(), - "finish_reason": generated.get("finish_reason"), - "usage": generated.get("usage"), - } - - -def _stable_result(reason: str) -> dict[str, object]: - if reason not in { - probe_contract.REASON_REMOTE_REJECTED, - probe_contract.REASON_RESPONSE_INVALID, - probe_contract.REASON_INTERNAL_ERROR, - }: - reason = probe_contract.REASON_INTERNAL_ERROR - return { - "protocol_version": FRAME_PROTOCOL_VERSION, - "result": reason, - } - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/solstone/think/sandbox_profile/scout_provider_probe.py b/solstone/think/sandbox_profile/scout_provider_probe.py deleted file mode 100644 index b056fa911..000000000 --- a/solstone/think/sandbox_profile/scout_provider_probe.py +++ /dev/null @@ -1,692 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Parent-side Scout provider proof primitive.""" - -from __future__ import annotations - -import hashlib -import json -import os -import shutil -import signal -import stat -import subprocess -import sys -import threading -import time -from collections.abc import Callable -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import httpx - -from solstone.think.json_codec import reject_duplicate_keys -from solstone.think.models import default_model_for_provider -from solstone.think.providers.shared import ( - CANNED_GENERATE_TIMEOUT_S, -) -from solstone.think.sandbox_profile import ( - capabilities, - envelope, - intent, - probe_contract, -) - -FRAME_PROTOCOL_VERSION = 1 - -STDIN_FRAME_MAX_BYTES = 16_384 -STDOUT_FRAME_MAX_BYTES = 4_096 -STDERR_DRAIN_MAX_BYTES = 16_384 -OUTBOUND_HEADER_MAX_BYTES = 16_384 -OUTBOUND_BODY_MAX_BYTES = 4_096 -INBOUND_RAW_HEADER_MAX_BYTES = 16_384 -TRANSFERRED_ENTITY_MAX_BYTES = 65_536 -DECODED_ENTITY_MAX_BYTES = 65_536 - -ABSOLUTE_DEADLINE_S = 40.0 -WORK_CUTOFF_S = 30.0 -TERM_GRACE_S = 3.0 -KILL_GRACE_S = 5.0 -ABSENCE_GRACE_S = 2.0 - -SCOUT_PROOF_PRIVATE_ROOT_NAME = "scout-provider-proof" -SCOUT_HOME_DIR_NAME = "home" -SCOUT_TMP_DIR_NAME = "tmp" -SCOUT_CWD_DIR_NAME = "cwd" -SCOUT_XDG_CACHE_DIR_NAME = "xdg-cache" -SCOUT_XDG_CONFIG_DIR_NAME = "xdg-config" -SCOUT_XDG_DATA_DIR_NAME = "xdg-data" -SCOUT_XDG_STATE_DIR_NAME = "xdg-state" - -_FRAME_HEADER_BYTES = 4 -_PROVIDER = "google" -_OFFICIAL_GEMINI_HOST = "generativelanguage.googleapis.com" -_CHILD_MODULE = "solstone.think.sandbox_profile.scout_provider_child" -DEFAULT_GOOGLE_MODEL = default_model_for_provider(_PROVIDER) -SCOUT_CHECKS = probe_contract.PROOF_CHECKS[probe_contract.CAPABILITY_SCOUT] -SCOUT_FAILED_REASONS = frozenset( - set(probe_contract.PROOF_SPECIFIC_REASONS[probe_contract.CAPABILITY_SCOUT]) - | set(probe_contract.FAILED_COMMON_REASONS) -) - -SCOUT_RESPONSE_SCHEMA: dict[str, object] = { - "title": "ScoutProviderProbeResponse", - "type": "object", - "additionalProperties": False, - "properties": { - "nonce": {"type": "string"}, - }, - "required": ["nonce"], -} - - -class ScoutProbeError(RuntimeError): - """Stable, non-secret probe error.""" - - def __init__(self, reason: str) -> None: - super().__init__(reason) - self.reason = reason - - -class ProbeTransportError(ScoutProbeError): - pass - - -class ProbeResponseInvalid(ProbeTransportError): - def __init__(self) -> None: - super().__init__(probe_contract.REASON_RESPONSE_INVALID) - - -class ProbeInternalError(ProbeTransportError): - def __init__(self) -> None: - super().__init__(probe_contract.REASON_INTERNAL_ERROR) - - -@dataclass(frozen=True, slots=True) -class ScoutProbeOutcome: - state: str - checks: tuple[str, ...] - reason: str | None - duration_ms: int - - def to_dict(self) -> dict[str, object]: - return { - probe_contract.FIELD_STATE: self.state, - probe_contract.FIELD_CHECKS: self.checks, - probe_contract.FIELD_REASON: self.reason, - probe_contract.FIELD_DURATION_MS: self.duration_ms, - } - - -@dataclass(frozen=True, slots=True) -class ScoutContainment: - root: Path - cwd: Path - env: dict[str, str] - - -@dataclass(slots=True) -class DrainResult: - data: bytes - oversize: bool - - -class _PipeDrainer: - def __init__(self, pipe: Any, *, cap: int) -> None: - self._pipe = pipe - self._cap = cap - self._data = bytearray() - self._oversize = False - self._thread = threading.Thread(target=self._run, daemon=True) - - def start(self) -> None: - self._thread.start() - - def join(self, timeout: float | None) -> DrainResult: - self._thread.join(timeout) - return DrainResult(bytes(self._data), self._oversize or self._thread.is_alive()) - - def _run(self) -> None: - try: - while True: - chunk = self._pipe.read(4096) - if not chunk: - return - remaining = self._cap - len(self._data) - if remaining > 0: - self._data.extend(chunk[:remaining]) - if len(chunk) > remaining: - self._oversize = True - except OSError: - self._oversize = True - - -class GeminiSingleRequestTransport(httpx.BaseTransport): - """HTTPX transport that permits one official Gemini completion request.""" - - def __init__(self, delegate: httpx.BaseTransport | None = None) -> None: - self._delegate = delegate or httpx.HTTPTransport(retries=0) - self.request_count = 0 - self.permitted_request: httpx.Request | None = None - self.failure_reason: str | None = None - - def handle_request(self, request: httpx.Request) -> httpx.Response: - self.request_count += 1 - try: - self._validate_request(request) - response = self._delegate.handle_request(request) - return self._validate_response(request, response) - except ProbeTransportError as exc: - self.failure_reason = exc.reason - raise exc from None - - def close(self) -> None: - self._delegate.close() - - def _validate_request(self, request: httpx.Request) -> None: - if self.request_count != 1: - raise ProbeInternalError() - url = request.url - expected_suffix = f"/models/{DEFAULT_GOOGLE_MODEL}:generateContent" - if ( - request.method != "POST" - or url.scheme != "https" - or url.host != _OFFICIAL_GEMINI_HOST - or not url.path.startswith("/v") - or not url.path.endswith(expected_suffix) - ): - raise ProbeInternalError() - header_bytes = sum( - len(name.encode("ascii", "ignore")) + len(value.encode("utf-8")) + 4 - for name, value in request.headers.multi_items() - ) - if header_bytes > OUTBOUND_HEADER_MAX_BYTES: - raise ProbeInternalError() - body = request.read() - if len(body) > OUTBOUND_BODY_MAX_BYTES: - raise ProbeInternalError() - self.permitted_request = request - - def _validate_response( - self, request: httpx.Request, response: httpx.Response - ) -> httpx.Response: - header_bytes = sum( - len(name.encode("ascii", "ignore")) + len(value.encode("utf-8")) + 4 - for name, value in response.headers.multi_items() - ) - if header_bytes > INBOUND_RAW_HEADER_MAX_BYTES: - response.close() - raise ProbeResponseInvalid() - body = response.read() - response.close() - if len(body) > TRANSFERRED_ENTITY_MAX_BYTES: - raise ProbeResponseInvalid() - return httpx.Response( - status_code=response.status_code, - headers=response.headers, - content=body, - request=request, - extensions=response.extensions, - ) - - -def prove_scout_provider( - journal: Path, - *, - attempt_dir: Path, - cancel_requested: Callable[[], bool], -) -> dict[str, object]: - start = time.monotonic() - deadline = start + ABSOLUTE_DEADLINE_S - checks: tuple[str, ...] = tuple(SCOUT_CHECKS[:0]) - outcome = _failed( - probe_contract.REASON_INTERNAL_ERROR, - checks, - _duration_ms(start), - ) - containment: ScoutContainment | None = None - proc: subprocess.Popen[bytes] | None = None - cleanup_failed = False - - try: - journal_path = Path(journal) - attempt_path = _validate_attempt_dir(journal_path, Path(attempt_dir)) - key = _ready_scout_key(journal_path) - if key is None: - outcome = _failed( - probe_contract.REASON_CAPABILITY_NOT_READY, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ) - elif cancel_requested(): - outcome = _failed( - probe_contract.REASON_CANCELLED, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ) - else: - private_root = attempt_path / SCOUT_PROOF_PRIVATE_ROOT_NAME - if not _cleanup_path_absent(private_root, deadline): - outcome = _failed( - probe_contract.REASON_CLEANUP_UNVERIFIED, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ) - else: - containment = _create_containment(private_root) - work_budget = _work_budget(deadline) - if work_budget <= 0: - outcome = _failed( - probe_contract.REASON_DEADLINE_EXCEEDED, - checks, - _duration_ms(start), - ) - else: - nonce = _new_nonce() - frame = encode_frame( - { - "protocol_version": FRAME_PROTOCOL_VERSION, - "api_key": key, - "nonce": nonce, - "timeout_s": min(CANNED_GENERATE_TIMEOUT_S, work_budget), - }, - cap=STDIN_FRAME_MAX_BYTES, - ) - proc = _spawn_child(containment) - child_result = _drive_child( - proc, frame, deadline, work_budget, cancel_requested - ) - proc = None - if child_result.reason is not None: - outcome = _failed( - child_result.reason, - checks, - _duration_ms(start), - ) - else: - outcome, checks = _outcome_from_child_frame( - child_result.stdout, - nonce=nonce, - start=start, - ) - if cancel_requested(): - outcome = _failed( - probe_contract.REASON_CANCELLED, - checks, - _duration_ms(start), - ) - except ScoutProbeError as exc: - outcome = _failed(exc.reason, checks, _duration_ms(start)) - except (OSError, ValueError, TypeError): - outcome = _failed( - probe_contract.REASON_INTERNAL_ERROR, - checks, - _duration_ms(start), - ) - finally: - if proc is not None: - _terminate_process(proc, deadline) - if containment is not None: - cleanup_failed = not _cleanup_path_absent(containment.root, deadline) - if cleanup_failed: - outcome = _failed( - probe_contract.REASON_CLEANUP_UNVERIFIED, - checks, - _duration_ms(start), - ) - return outcome.to_dict() - - -@dataclass(frozen=True, slots=True) -class _ChildDriveResult: - stdout: bytes - reason: str | None - - -def _outcome_from_child_frame( - frame: bytes, *, nonce: str, start: float -) -> tuple[ScoutProbeOutcome, tuple[str, ...]]: - payload = decode_frame(frame, cap=STDOUT_FRAME_MAX_BYTES) - if payload.get("protocol_version") != FRAME_PROTOCOL_VERSION: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - kind = payload.get("result") - if kind == probe_contract.REASON_REMOTE_REJECTED: - return ( - _failed( - probe_contract.REASON_REMOTE_REJECTED, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ), - tuple(SCOUT_CHECKS[:0]), - ) - if kind == probe_contract.REASON_RESPONSE_INVALID: - return ( - _failed( - probe_contract.REASON_RESPONSE_INVALID, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ), - tuple(SCOUT_CHECKS[:0]), - ) - if kind == probe_contract.REASON_INTERNAL_ERROR: - return ( - _failed( - probe_contract.REASON_INTERNAL_ERROR, - tuple(SCOUT_CHECKS[:0]), - _duration_ms(start), - ), - tuple(SCOUT_CHECKS[:0]), - ) - if kind != "ok": - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - - checks = tuple(SCOUT_CHECKS[:1]) - expected = hashlib.sha256(nonce.encode("utf-8")).hexdigest() - if payload.get("nonce_sha256") != expected: - return ( - _failed( - probe_contract.REASON_CONTENT_MISMATCH, - checks, - _duration_ms(start), - ), - checks, - ) - - checks = tuple(SCOUT_CHECKS[:2]) - if payload.get("finish_reason") != "stop": - return ( - _failed( - probe_contract.REASON_RESPONSE_INVALID, - checks, - _duration_ms(start), - ), - checks, - ) - - checks = tuple(SCOUT_CHECKS[:3]) - if not _usage_valid(payload.get("usage")): - return ( - _failed( - probe_contract.REASON_USAGE_INVALID, - checks, - _duration_ms(start), - ), - checks, - ) - - checks = tuple(SCOUT_CHECKS[:]) - return _passed(_duration_ms(start)), checks - - -def _usage_valid(value: object) -> bool: - if not isinstance(value, dict): - return False - input_tokens = value.get("input_tokens") - output_tokens = value.get("output_tokens") - return ( - type(input_tokens) is int - and input_tokens > 0 - and type(output_tokens) is int - and output_tokens > 0 - ) - - -def _ready_scout_key(journal: Path) -> str | None: - try: - config = capabilities._read_config(journal) - cap = capabilities._scout_status(config, intent.load_intent(journal)) - except (OSError, ValueError): - return None - if cap.state != envelope.CAP_READY: - return None - key = config.get("env", {}).get("GOOGLE_API_KEY") - return key if isinstance(key, str) and key else None - - -def _validate_attempt_dir(journal: Path, attempt_dir: Path) -> Path: - parent = probe_contract.probe_attempts_parent_path(journal).resolve() - try: - current = attempt_dir.lstat() - except OSError: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) from None - if stat.S_ISLNK(current.st_mode) or not stat.S_ISDIR(current.st_mode): - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - if stat.S_IMODE(current.st_mode) != probe_contract.ATTEMPT_DIR_MODE: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - resolved = attempt_dir.resolve() - if resolved.parent != parent: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - return resolved - - -def _create_containment(root: Path) -> ScoutContainment: - root.mkdir(mode=0o700) - paths = { - "HOME": root / SCOUT_HOME_DIR_NAME, - "TMPDIR": root / SCOUT_TMP_DIR_NAME, - "XDG_CACHE_HOME": root / SCOUT_XDG_CACHE_DIR_NAME, - "XDG_CONFIG_HOME": root / SCOUT_XDG_CONFIG_DIR_NAME, - "XDG_DATA_HOME": root / SCOUT_XDG_DATA_DIR_NAME, - "XDG_STATE_HOME": root / SCOUT_XDG_STATE_DIR_NAME, - } - cwd = root / SCOUT_CWD_DIR_NAME - for path in (*paths.values(), cwd): - path.mkdir(mode=0o700) - path.chmod(0o700) - env = { - "LITELLM_MODE": "PRODUCTION", - "LITELLM_LOCAL_MODEL_COST_MAP": "True", - **{key: str(value) for key, value in paths.items()}, - } - return ScoutContainment(root=root, cwd=cwd, env=env) - - -def _cleanup_path_absent(path: Path, deadline: float) -> bool: - try: - if path.is_symlink() or path.is_file(): - path.unlink() - elif path.exists(): - shutil.rmtree(path) - except OSError: - return False - - stop = time.monotonic() + min(ABSENCE_GRACE_S, _remaining(deadline)) - while True: - try: - path.lstat() - except FileNotFoundError: - return True - except OSError: - return False - if time.monotonic() >= stop: - return False - time.sleep(min(0.02, max(0.0, stop - time.monotonic()))) - - -def _work_budget(deadline: float) -> float: - reserve = TERM_GRACE_S + KILL_GRACE_S + ABSENCE_GRACE_S - return max(0.0, min(WORK_CUTOFF_S, deadline - time.monotonic() - reserve)) - - -def _remaining(deadline: float) -> float: - return max(0.0, deadline - time.monotonic()) - - -def _spawn_child(containment: ScoutContainment) -> subprocess.Popen[bytes]: - proc = subprocess.Popen( - [sys.executable, "-m", _CHILD_MODULE], - cwd=containment.cwd, - env=containment.env, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True, - pass_fds=(), - start_new_session=True, - ) - for pipe in (proc.stdin, proc.stdout, proc.stderr): - if pipe is not None: - try: - os.set_inheritable(pipe.fileno(), False) - except OSError: - pass - return proc - - -def _drive_child( - proc: subprocess.Popen[bytes], - frame: bytes, - deadline: float, - work_budget: float, - cancel_requested: Callable[[], bool], -) -> _ChildDriveResult: - if proc.stdout is None or proc.stderr is None or proc.stdin is None: - _terminate_process(proc, deadline) - return _ChildDriveResult(b"", probe_contract.REASON_INTERNAL_ERROR) - stdout = _PipeDrainer(proc.stdout, cap=STDOUT_FRAME_MAX_BYTES + _FRAME_HEADER_BYTES) - stderr = _PipeDrainer(proc.stderr, cap=STDERR_DRAIN_MAX_BYTES) - stdout.start() - stderr.start() - try: - proc.stdin.write(frame) - proc.stdin.close() - except OSError: - _terminate_process(proc, deadline) - return _ChildDriveResult(b"", probe_contract.REASON_INTERNAL_ERROR) - - work_until = min(time.monotonic() + work_budget, deadline) - reason: str | None = None - while proc.poll() is None: - if cancel_requested(): - reason = probe_contract.REASON_CANCELLED - break - if time.monotonic() >= work_until or _remaining(deadline) <= 0: - reason = probe_contract.REASON_DEADLINE_EXCEEDED - break - time.sleep(min(0.02, max(0.0, work_until - time.monotonic()))) - - if reason is not None: - _terminate_process(proc, deadline) - - join_timeout = min(ABSENCE_GRACE_S, _remaining(deadline)) - out = stdout.join(join_timeout) - err = stderr.join(min(ABSENCE_GRACE_S, _remaining(deadline))) - if reason is not None: - return _ChildDriveResult(out.data, reason) - if out.oversize or err.oversize or proc.returncode not in (0, None): - return _ChildDriveResult(out.data, probe_contract.REASON_INTERNAL_ERROR) - return _ChildDriveResult(out.data, None) - - -def _terminate_process(proc: subprocess.Popen[bytes], deadline: float) -> None: - if proc.poll() is not None: - return - try: - os.killpg(proc.pid, signal.SIGTERM) - except OSError: - pass - _wait_bounded(proc, TERM_GRACE_S, deadline) - if proc.poll() is not None: - return - try: - os.killpg(proc.pid, signal.SIGKILL) - except OSError: - pass - _wait_bounded(proc, KILL_GRACE_S, deadline) - - -def _wait_bounded( - proc: subprocess.Popen[bytes], - reserve: float, - deadline: float, -) -> None: - timeout = min(reserve, _remaining(deadline)) - if timeout <= 0: - return - try: - proc.wait(timeout=timeout) - except subprocess.TimeoutExpired: - return - - -def encode_frame(payload: dict[str, object], *, cap: int) -> bytes: - data = json.dumps( - payload, - ensure_ascii=True, - separators=(",", ":"), - sort_keys=True, - ).encode("utf-8") - if len(data) > cap: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - return len(data).to_bytes(_FRAME_HEADER_BYTES, "big") + data - - -def decode_frame(data: bytes, *, cap: int) -> dict[str, Any]: - if len(data) < _FRAME_HEADER_BYTES: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - length = int.from_bytes(data[:_FRAME_HEADER_BYTES], "big") - if length > cap or len(data) != _FRAME_HEADER_BYTES + length: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - try: - payload = json.loads( - data[_FRAME_HEADER_BYTES:].decode("utf-8"), - object_pairs_hook=reject_duplicate_keys, - ) - except (UnicodeDecodeError, json.JSONDecodeError, ValueError): - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) from None - if not isinstance(payload, dict): - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - return payload - - -def decode_model_json(text: str) -> dict[str, Any]: - if len(text.encode("utf-8")) > DECODED_ENTITY_MAX_BYTES: - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) - try: - payload = json.loads(text, object_pairs_hook=reject_duplicate_keys) - except (json.JSONDecodeError, ValueError): - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if not isinstance(payload, dict): - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) - if set(payload) != {"nonce"} or not isinstance(payload.get("nonce"), str): - raise ScoutProbeError(probe_contract.REASON_RESPONSE_INVALID) - return payload - - -def scout_prompt(nonce: str) -> str: - return ( - "Return exactly one JSON object matching the provided schema. " - f"Set nonce to this exact value: {nonce}" - ) - - -def _passed(duration_ms: int) -> ScoutProbeOutcome: - return ScoutProbeOutcome( - state=probe_contract.PROOF_STATE_PASSED, - checks=tuple(SCOUT_CHECKS[:]), - reason=None, - duration_ms=duration_ms, - ) - - -def _failed( - reason: str, checks: tuple[str, ...], duration_ms: int -) -> ScoutProbeOutcome: - if reason not in SCOUT_FAILED_REASONS: - raise ScoutProbeError(probe_contract.REASON_INTERNAL_ERROR) - return ScoutProbeOutcome( - state=probe_contract.PROOF_STATE_FAILED, - checks=checks, - reason=reason, - duration_ms=duration_ms, - ) - - -def _duration_ms(start: float) -> int: - return max(0, int((time.monotonic() - start) * 1000)) - - -def _new_nonce() -> str: - return hashlib.sha256(os.urandom(32)).hexdigest() diff --git a/solstone/think/sandbox_profile/spb_backup_probe.py b/solstone/think/sandbox_profile/spb_backup_probe.py deleted file mode 100644 index 56400e405..000000000 --- a/solstone/think/sandbox_profile/spb_backup_probe.py +++ /dev/null @@ -1,901 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Operated-backup readback primitive for the disposable sandbox profile.""" - -from __future__ import annotations - -import dataclasses -import hashlib -import os -import shutil -import stat -import time -import uuid -from dataclasses import dataclass -from datetime import UTC, datetime -from pathlib import Path -from typing import Any - -from solstone.think.backup import rclone_install, readiness, s3_wipe, state -from solstone.think.backup.hosted import ( - HostedBinding, - HostedCredentials, - HostedCredsUnavailable, - fetch_hosted_credentials, -) -from solstone.think.backup.hosted_provider import ( - HostedResticSession, - hosted_append_only_restic_session, -) -from solstone.think.backup.runner import ( - _PROCESS_GROUP_CLEANUP_UNVERIFIED, - ResticJsonRecordsResult, - ResticResult, - run_restic, - run_restic_json_records, -) -from solstone.think.sandbox_profile import ( - capabilities, - envelope, - intent, - probe_contract, -) - -PRIMITIVE_CHECKS = probe_contract.PROOF_CHECKS[probe_contract.CAPABILITY_SPB][:4] -SPB_FAILED_REASONS = frozenset( - ( - set(probe_contract.PROOF_SPECIFIC_REASONS[probe_contract.CAPABILITY_SPB]) - | set(probe_contract.FAILED_COMMON_REASONS) - ) - - {probe_contract.REASON_CANCELLED} -) - -INSPECT_READY = "ready" -INSPECT_UNAVAILABLE = "unavailable" -INSPECT_RESTIC_MISSING = "restic_missing" -INSPECT_RESTIC_INCOMPATIBLE = "restic_incompatible" -INSPECT_BACKUP_ADAPTER_UNAVAILABLE = "backup_adapter_unavailable" - -ABSOLUTE_DEADLINE_S = 360.0 -BROKER_FETCH_TIMEOUT_S = 30.0 -RESTIC_CHILD_TIMEOUT_S = 60.0 -STORAGE_LIST_TIMEOUT_S = 60.0 -TERM_GRACE_S = 3.0 -KILL_GRACE_S = 5.0 -FINALIZE_RESERVE_S = 2.0 -COORDINATOR_CLEANUP_BUDGET_S = 30.0 -LEASE_SPAWN_FLOOR_S = 75.0 - -SPB_DIR_NAME = "spb" -SOURCE_FILE_NAME = "source.bin" -RESTORE_DIR_NAME = "restore" -LOGICAL_SOURCE_PATH = "/spb/source.bin" -FIXTURE_LENGTH = 4096 -_FIXTURE_MARKER = b"SOLSTONE-SPB-SANDBOX-PROOF-SYNTHETIC-FIXTURE-V1\n" -SPB_SYNTHETIC_FIXTURE_BYTES = ( - _FIXTURE_MARKER + (b"0123456789abcdef-synthetic-spb-proof\n" * 128) -)[:FIXTURE_LENGTH] -if len(SPB_SYNTHETIC_FIXTURE_BYTES) != FIXTURE_LENGTH: # pragma: no cover - raise RuntimeError("invalid SPB fixture length") - - -class _Clock: - def monotonic(self) -> float: - return time.monotonic() - - def utcnow(self) -> datetime: - return datetime.now(UTC) - - -_clock: _Clock = _Clock() - - -class _SpbProbeError(RuntimeError): - def __init__(self, reason: str) -> None: - super().__init__(reason) - self.reason = reason - - -class _SpbProbeInternalError(_SpbProbeError): - def __init__(self) -> None: - super().__init__(probe_contract.REASON_INTERNAL_ERROR) - - -@dataclass(frozen=True) -class _SpbProbeOutcome: - state: str - checks: tuple[str, ...] - reason: str | None - duration_ms: int - - def to_dict(self) -> dict[str, object]: - return { - probe_contract.FIELD_STATE: self.state, - probe_contract.FIELD_CHECKS: list(self.checks), - probe_contract.FIELD_REASON: self.reason, - probe_contract.FIELD_DURATION_MS: self.duration_ms, - } - - -@dataclass(frozen=True) -class _FixtureIdentity: - device: int - inode: int - length: int - digest: str - - -@dataclass(frozen=True) -class _Preflight: - attempt_dir: Path - spb_root: Path - restore_target: Path - fixture_path: Path - fixture: _FixtureIdentity - binding: HostedBinding - proof_binding: HostedBinding - daily_key: str - restic_path: Path - rclone_path: Path - scrub_values: tuple[str, ...] - - -@dataclass(frozen=True) -class _CredentialReceipt: - credentials: HostedCredentials - received_monotonic: float - lease_remaining_s: float - - -def inspect_sandbox_spb_prerequisites(journal_path: Path) -> dict[str, Any]: - _ = Path(journal_path) - restic_path, restic_reason = readiness.inspect_restic_ready( - version_timeout=5.0, - ) - if restic_path is None: - reason = ( - INSPECT_RESTIC_MISSING - if restic_reason == INSPECT_RESTIC_MISSING - else INSPECT_RESTIC_INCOMPATIBLE - ) - return {"state": INSPECT_UNAVAILABLE, "reason": reason} - rclone_path = rclone_install.check_rclone_ready(version_timeout=5.0) - if rclone_path is None: - return { - "state": INSPECT_UNAVAILABLE, - "reason": INSPECT_BACKUP_ADAPTER_UNAVAILABLE, - } - return {"state": INSPECT_READY, "reason": None} - - -def prove_spb_backup(journal_path: Path, *, attempt_dir: Path) -> dict[str, Any]: - start = _clock.monotonic() - deadline = start + ABSOLUTE_DEADLINE_S - checks: tuple[str, ...] = () - outcome = _failed( - reason=probe_contract.REASON_INTERNAL_ERROR, - checks=checks, - duration_ms=0, - ) - try: - preflight = _preflight(Path(journal_path), Path(attempt_dir)) - - _require_remaining( - deadline, - BROKER_FETCH_TIMEOUT_S + STORAGE_LIST_TIMEOUT_S + FINALIZE_RESERVE_S, - ) - list_creds = _fetch_credentials(preflight.proof_binding) - _require_remaining(deadline, STORAGE_LIST_TIMEOUT_S + FINALIZE_RESERVE_S) - _require_lease(list_creds, STORAGE_LIST_TIMEOUT_S + FINALIZE_RESERVE_S) - _prove_prefix_empty(preflight, list_creds.credentials) - - _require_remaining( - deadline, - BROKER_FETCH_TIMEOUT_S - + RESTIC_CHILD_TIMEOUT_S - + TERM_GRACE_S - + KILL_GRACE_S - + FINALIZE_RESERVE_S, - ) - init_creds = _fetch_credentials(preflight.proof_binding) - _require_child_budget_and_lease(deadline, init_creds) - _run_init(preflight, init_creds.credentials) - checks = PRIMITIVE_CHECKS[:1] - - _require_remaining( - deadline, - BROKER_FETCH_TIMEOUT_S - + RESTIC_CHILD_TIMEOUT_S - + TERM_GRACE_S - + KILL_GRACE_S - + FINALIZE_RESERVE_S, - ) - backup_creds = _fetch_credentials(preflight.proof_binding) - _require_child_budget_and_lease(deadline, backup_creds) - _ensure_fixture_unchanged(preflight) - snapshot_id = _run_backup(preflight, backup_creds.credentials) - _ensure_fixture_unchanged(preflight) - checks = PRIMITIVE_CHECKS[:2] - - _require_remaining( - deadline, - BROKER_FETCH_TIMEOUT_S - + RESTIC_CHILD_TIMEOUT_S - + TERM_GRACE_S - + KILL_GRACE_S - + FINALIZE_RESERVE_S, - ) - ls_creds = _fetch_credentials(preflight.proof_binding) - _require_child_budget_and_lease(deadline, ls_creds) - _run_ls(preflight, ls_creds.credentials, snapshot_id) - checks = PRIMITIVE_CHECKS[:3] - - _require_remaining( - deadline, - BROKER_FETCH_TIMEOUT_S - + RESTIC_CHILD_TIMEOUT_S - + TERM_GRACE_S - + KILL_GRACE_S - + FINALIZE_RESERVE_S, - ) - restore_creds = _fetch_credentials(preflight.proof_binding) - _require_child_budget_and_lease(deadline, restore_creds) - _run_restore(preflight, restore_creds.credentials, snapshot_id) - _verify_restore_tree(preflight) - checks = PRIMITIVE_CHECKS[:4] - outcome = _passed(checks=checks, duration_ms=_duration_ms(start)) - except _SpbProbeError as exc: - outcome = _failed( - reason=exc.reason, - checks=checks, - duration_ms=_duration_ms(start), - ) - except Exception: - outcome = _failed( - reason=probe_contract.REASON_INTERNAL_ERROR, - checks=checks, - duration_ms=_duration_ms(start), - ) - return outcome.to_dict() - - -def cleanup_spb_attempt_local( - journal_path: Path, *, attempt_dir: Path -) -> dict[str, Any]: - start = _clock.monotonic() - try: - resolved_attempt = _validate_attempt_dir(Path(journal_path), Path(attempt_dir)) - except _SpbProbeError: - return { - "state": probe_contract.CLEANUP_STATE_UNVERIFIED, - "reason": probe_contract.REASON_CLEANUP_UNVERIFIED, - "duration_ms": _duration_ms(start), - } - deadline = start + COORDINATOR_CLEANUP_BUDGET_S - verified = _cleanup_path_absent(resolved_attempt / SPB_DIR_NAME, deadline) - return { - "state": ( - probe_contract.CLEANUP_STATE_VERIFIED - if verified - else probe_contract.CLEANUP_STATE_UNVERIFIED - ), - "reason": None if verified else probe_contract.REASON_CLEANUP_UNVERIFIED, - "duration_ms": _duration_ms(start), - } - - -def _preflight(journal: Path, attempt_dir: Path) -> _Preflight: - resolved_attempt = _validate_attempt_dir(journal, attempt_dir) - restic_path, restic_reason = readiness.inspect_restic_ready(version_timeout=5.0) - if restic_path is None: - _ = restic_reason - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - rclone_path = rclone_install.check_rclone_ready(version_timeout=5.0) - if rclone_path is None: - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - binding = _load_binding(journal) - if not _spb_capability_ready(journal): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - daily_key = state.get_daily_key(journal) - if daily_key is None: - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - proof_binding = _proof_binding(binding, resolved_attempt.name) - spb_root = resolved_attempt / SPB_DIR_NAME - restore_target = spb_root / RESTORE_DIR_NAME - fixture_path = spb_root / SOURCE_FILE_NAME - _create_fixture(fixture_path) - fixture = _fixture_identity(fixture_path) - if restore_target.exists() or restore_target.is_symlink(): - raise _SpbProbeInternalError() from None - scrub_values = _scrub_values( - binding=binding, - proof_binding=proof_binding, - attempt_dir=resolved_attempt, - spb_root=spb_root, - restore_target=restore_target, - ) - return _Preflight( - attempt_dir=resolved_attempt, - spb_root=spb_root, - restore_target=restore_target, - fixture_path=fixture_path, - fixture=fixture, - binding=binding, - proof_binding=proof_binding, - daily_key=daily_key, - restic_path=restic_path, - rclone_path=rclone_path, - scrub_values=scrub_values, - ) - - -def _validate_attempt_dir(journal: Path, attempt_dir: Path) -> Path: - parent = probe_contract.probe_attempts_parent_path(journal).resolve() - try: - stat_result = attempt_dir.lstat() - except OSError: - raise _SpbProbeInternalError() from None - if stat.S_ISLNK(stat_result.st_mode) or not stat.S_ISDIR(stat_result.st_mode): - raise _SpbProbeInternalError() from None - if stat.S_IMODE(stat_result.st_mode) != probe_contract.ATTEMPT_DIR_MODE: - raise _SpbProbeInternalError() from None - resolved = attempt_dir.resolve() - if resolved.parent != parent: - raise _SpbProbeInternalError() from None - try: - parsed = uuid.UUID(resolved.name) - except ValueError: - raise _SpbProbeInternalError() from None - if str(parsed) != resolved.name: - raise _SpbProbeInternalError() from None - return resolved - - -def _cleanup_path_absent(path: Path, deadline: float) -> bool: - try: - if path.is_symlink() or path.is_file(): - path.unlink() - elif path.exists(): - shutil.rmtree(path) - except OSError: - return False - poll_until = _clock.monotonic() + min(FINALIZE_RESERVE_S, _remaining(deadline)) - while _clock.monotonic() <= poll_until: - try: - path.lstat() - except FileNotFoundError: - return True - except OSError: - return False - if _remaining(deadline) <= 0: - return False - time.sleep(min(0.02, _remaining(deadline))) - return False - - -def _spb_capability_ready(journal: Path) -> bool: - try: - config = capabilities._read_config(journal) - cap = capabilities._spb_status(journal, config, intent.load_intent(journal)) - except (OSError, ValueError, intent.IntentError): - return False - return cap.state == envelope.CAP_READY - - -def _load_binding(journal: Path) -> HostedBinding: - payload = capabilities._read_hosted_binding(journal) - if not isinstance(payload, dict): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - fields = {} - for key in ( - "broker_endpoint", - "account_id", - "instance_id", - "bucket", - "prefix", - "broker_token", - ): - value = payload.get(key) - if not isinstance(value, str) or not value.strip(): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - fields[key] = value - return HostedBinding(**fields) - - -def _proof_binding(binding: HostedBinding, attempt_id: str) -> HostedBinding: - run_segments = _prefix_segments(binding.prefix) - derived_segments = (*run_segments, "proofs", attempt_id) - run_prefix = "/".join(run_segments) - derived_prefix = "/".join(derived_segments) + "/" - if not derived_prefix.startswith(f"{run_prefix}/"): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - if derived_segments[: len(run_segments)] != run_segments: - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - if len(derived_segments) < len(run_segments) + 2: - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - return dataclasses.replace(binding, prefix=derived_prefix) - - -def _prefix_segments(prefix: str) -> tuple[str, ...]: - normalized = prefix[:-1] if prefix.endswith("/") else prefix - if not normalized or normalized.startswith("/"): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - segments = tuple(normalized.split("/")) - if any(segment in {"", ".", ".."} or "\x00" in segment for segment in segments): - raise _SpbProbeError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - return segments - - -def _create_fixture(path: Path) -> None: - try: - path.parent.mkdir(mode=0o700, parents=True, exist_ok=False) - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW - fd = os.open(path, flags, 0o600) - try: - if os.write(fd, SPB_SYNTHETIC_FIXTURE_BYTES) != FIXTURE_LENGTH: - raise _SpbProbeInternalError() from None - os.fsync(fd) - finally: - os.close(fd) - dir_fd = os.open(path.parent, os.O_RDONLY) - try: - os.fsync(dir_fd) - finally: - os.close(dir_fd) - except OSError: - raise _SpbProbeInternalError() from None - - -def _fixture_identity(path: Path) -> _FixtureIdentity: - try: - stat_result = path.lstat() - except OSError: - raise _SpbProbeInternalError() from None - if ( - stat.S_ISLNK(stat_result.st_mode) - or not stat.S_ISREG(stat_result.st_mode) - or stat_result.st_nlink != 1 - or stat_result.st_size != FIXTURE_LENGTH - ): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - try: - data = path.read_bytes() - except OSError: - raise _SpbProbeInternalError() from None - return _FixtureIdentity( - device=stat_result.st_dev, - inode=stat_result.st_ino, - length=len(data), - digest=hashlib.sha256(data).hexdigest(), - ) - - -def _ensure_fixture_unchanged(preflight: _Preflight) -> None: - if _fixture_identity(preflight.fixture_path) != preflight.fixture: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - - -def _fetch_credentials(binding: HostedBinding) -> _CredentialReceipt: - try: - credentials = fetch_hosted_credentials(binding, scope="operated") - except HostedCredsUnavailable: - raise _SpbProbeError(probe_contract.REASON_REMOTE_REJECTED) from None - received_wall = _clock.utcnow() - received_monotonic = _clock.monotonic() - expires_at = _parse_expires_at(credentials.expires_at) - lease_remaining = (expires_at - received_wall).total_seconds() - if lease_remaining <= 0: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return _CredentialReceipt( - credentials=credentials, - received_monotonic=received_monotonic, - lease_remaining_s=lease_remaining, - ) - - -def _parse_expires_at(value: str) -> datetime: - if len(value) != 20 or value[4] != "-" or value[7] != "-": - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if value[10] != "T" or value[13] != ":" or value[16] != ":" or value[19] != "Z": - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - parts = value[:19] - try: - parsed = datetime.strptime(parts, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=UTC) - except ValueError: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return parsed - - -def _require_lease(receipt: _CredentialReceipt, required_s: float) -> None: - elapsed = max(0.0, _clock.monotonic() - receipt.received_monotonic) - if receipt.lease_remaining_s - elapsed <= required_s: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - - -def _require_child_budget_and_lease( - deadline: float, - receipt: _CredentialReceipt, -) -> None: - _require_remaining( - deadline, - RESTIC_CHILD_TIMEOUT_S + TERM_GRACE_S + KILL_GRACE_S + FINALIZE_RESERVE_S, - ) - _require_lease(receipt, LEASE_SPAWN_FLOOR_S) - - -def _prove_prefix_empty(preflight: _Preflight, credentials: HostedCredentials) -> None: - try: - keys, uploads = s3_wipe.list_prefix_contents( - endpoint=credentials.endpoint, - bucket=preflight.proof_binding.bucket, - prefix=preflight.proof_binding.prefix, - access_key_id=credentials.access_key_id, - secret_access_key=credentials.secret_access_key, - session_token=credentials.session_token, - timeout=STORAGE_LIST_TIMEOUT_S, - budget_s=STORAGE_LIST_TIMEOUT_S, - ) - except Exception: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if keys or uploads: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - - -def _run_init(preflight: _Preflight, credentials: HostedCredentials) -> None: - _run_restic_phase(preflight, credentials, ["init"]) - - -def _run_backup(preflight: _Preflight, credentials: HostedCredentials) -> str: - result = _run_restic_records_phase( - preflight, - credentials, - ["backup", "--stdin", "--stdin-filename", LOGICAL_SOURCE_PATH], - stdin_bytes=SPB_SYNTHETIC_FIXTURE_BYTES, - ) - records = list(result.consume_records()) - return _validate_backup_records(records) - - -def _run_ls( - preflight: _Preflight, - credentials: HostedCredentials, - snapshot_id: str, -) -> None: - result = _run_restic_records_phase( - preflight, - credentials, - ["ls", "--long", snapshot_id], - scrub_values=(snapshot_id,), - ) - records = list(result.consume_records()) - _validate_ls_records(records, snapshot_id, preflight) - - -def _run_restore( - preflight: _Preflight, - credentials: HostedCredentials, - snapshot_id: str, -) -> None: - result = _run_restic_records_phase( - preflight, - credentials, - ["restore", snapshot_id, "--target", str(preflight.restore_target)], - scrub_values=(snapshot_id,), - ) - _validate_restore_records(list(result.consume_records())) - - -def _run_restic_phase( - preflight: _Preflight, - credentials: HostedCredentials, - args: list[str], -) -> ResticResult: - with hosted_append_only_restic_session( - preflight.proof_binding, - rclone_path=preflight.rclone_path, - initial_credentials=credentials, - ) as session: - result = run_restic( - _session_args(session, args), - repository=session.destination.repository, - password=preflight.daily_key, - restic_path=preflight.restic_path, - backend_env=session.backend_env, - json=False, - timeout=RESTIC_CHILD_TIMEOUT_S, - process_group=True, - scrub_values=(*preflight.scrub_values, session.destination.repository), - terminate_grace_s=TERM_GRACE_S, - kill_grace_s=KILL_GRACE_S, - ) - _check_restic_result(result) - return result - - -def _session_args(session: HostedResticSession, args: list[str]) -> list[str]: - return ["--no-cache", *session.global_options, *args] - - -def _run_restic_records_phase( - preflight: _Preflight, - credentials: HostedCredentials, - args: list[str], - *, - stdin_bytes: bytes | None = None, - scrub_values: tuple[str, ...] = (), -) -> ResticJsonRecordsResult: - with hosted_append_only_restic_session( - preflight.proof_binding, - rclone_path=preflight.rclone_path, - initial_credentials=credentials, - ) as session: - result = run_restic_json_records( - _session_args(session, args), - repository=session.destination.repository, - password=preflight.daily_key, - restic_path=preflight.restic_path, - backend_env=session.backend_env, - timeout=RESTIC_CHILD_TIMEOUT_S, - stdin_bytes=stdin_bytes, - scrub_values=( - *preflight.scrub_values, - session.destination.repository, - *scrub_values, - ), - terminate_grace_s=TERM_GRACE_S, - kill_grace_s=KILL_GRACE_S, - ) - _check_restic_result(result) - if not result.has_records: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return result - - -def _check_restic_result( - result: ResticResult | ResticJsonRecordsResult, -) -> None: - if _PROCESS_GROUP_CLEANUP_UNVERIFIED in result.stderr: - raise _SpbProbeError(probe_contract.REASON_CLEANUP_UNVERIFIED) from None - if result.returncode == 124: - raise _SpbProbeError(probe_contract.REASON_DEADLINE_EXCEEDED) from None - if result.returncode != 0: - raise _SpbProbeError(probe_contract.REASON_REMOTE_REJECTED) from None - - -def _validate_record(record: object) -> dict[str, object]: - if not isinstance(record, dict): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return record - - -def _validate_message_type(record: dict[str, object]) -> str: - message_type = record.get("message_type") - if not isinstance(message_type, str): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if "struct_type" not in record: - return message_type - struct_type = record.get("struct_type") - if not isinstance(struct_type, str) or struct_type != message_type: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return message_type - - -def _validate_terminal_summary_records(records: list[object]) -> dict[str, object]: - summary: dict[str, object] | None = None - for raw_record in records: - record = _validate_record(raw_record) - message_type = _validate_message_type(record) - if summary is not None: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if message_type == "status": - continue - if message_type != "summary": - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - summary = record - if summary is None: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return summary - - -def _require_exact_int( - record: dict[str, object], - field: str, - expected: int, -) -> None: - value = record.get(field) - if type(value) is not int: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if value != expected: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - - -def _is_lowercase_hex_id(value: str) -> bool: - return len(value) == 64 and all(char in "0123456789abcdef" for char in value) - - -def _validate_backup_records(records: list[object]) -> str: - summary = _validate_terminal_summary_records(records) - _require_exact_int(summary, "total_files_processed", 1) - _require_exact_int(summary, "total_bytes_processed", FIXTURE_LENGTH) - snapshot_id = summary.get("snapshot_id") - if not isinstance(snapshot_id, str) or not _is_lowercase_hex_id(snapshot_id): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - return snapshot_id - - -def _validate_ls_records( - records: list[object], - snapshot_id: str, - preflight: _Preflight, -) -> None: - snapshot_count = 0 - node_counts = { - ("/spb", "dir"): 0, - (LOGICAL_SOURCE_PATH, "file"): 0, - } - for raw_record in records: - record = _validate_record(raw_record) - message_type = _validate_message_type(record) - if message_type == "snapshot": - snapshot_count += 1 - snapshot_record_id = record.get("id") - if not isinstance(snapshot_record_id, str): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if snapshot_record_id != snapshot_id: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - paths = record.get("paths") - if not isinstance(paths, list): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if paths != [LOGICAL_SOURCE_PATH]: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - continue - if message_type != "node": - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - path = record.get("path") - node_type = record.get("type") - if not isinstance(path, str) or not isinstance(node_type, str): - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if _is_physical_source_path(path, preflight): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - node_key = (path, node_type) - if node_key == (LOGICAL_SOURCE_PATH, "file"): - _require_exact_int(record, "size", FIXTURE_LENGTH) - node_counts[node_key] += 1 - continue - if node_key == ("/spb", "dir"): - node_counts[node_key] += 1 - continue - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - if snapshot_count != 1: - raise _SpbProbeError(probe_contract.REASON_RESPONSE_INVALID) from None - if any(count != 1 for count in node_counts.values()): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - - -def _validate_restore_records(records: list[object]) -> None: - summary = _validate_terminal_summary_records(records) - _require_exact_int(summary, "total_files", 2) - _require_exact_int(summary, "files_restored", 2) - _require_exact_int(summary, "total_bytes", FIXTURE_LENGTH) - _require_exact_int(summary, "bytes_restored", FIXTURE_LENGTH) - - -def _verify_restore_tree(preflight: _Preflight) -> None: - expected = {".", "spb", "spb/source.bin"} - observed = {"."} - try: - root_stat = preflight.restore_target.lstat() - except OSError: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - try: - paths = list(preflight.restore_target.rglob("*")) - except OSError: - raise _SpbProbeInternalError() from None - for path in paths: - rel = path.relative_to(preflight.restore_target).as_posix() - if _is_physical_source_path(rel, preflight): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - try: - stat_result = path.lstat() - except OSError: - raise _SpbProbeInternalError() from None - if stat.S_ISLNK(stat_result.st_mode): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - if stat.S_ISDIR(stat_result.st_mode): - observed.add(rel) - continue - if stat.S_ISREG(stat_result.st_mode): - observed.add(rel) - continue - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - if observed != expected: - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - restored = preflight.restore_target / "spb" / SOURCE_FILE_NAME - identity = _fixture_identity(restored) - if ( - identity.length != preflight.fixture.length - or identity.length != FIXTURE_LENGTH - or identity.digest != preflight.fixture.digest - ): - raise _SpbProbeError(probe_contract.REASON_CONTENT_MISMATCH) from None - - -def _is_physical_source_path(path: str, preflight: _Preflight) -> bool: - physical = preflight.spb_root.as_posix() - return physical in path or preflight.attempt_dir.as_posix() in path - - -def _require_remaining(deadline: float, required_s: float) -> None: - if _remaining(deadline) <= required_s: - raise _SpbProbeError(probe_contract.REASON_DEADLINE_EXCEEDED) from None - - -def _remaining(deadline: float) -> float: - return max(0.0, deadline - _clock.monotonic()) - - -def _duration_ms(start: float) -> int: - return max(0, int((_clock.monotonic() - start) * 1000)) - - -def _passed(*, checks: tuple[str, ...], duration_ms: int) -> _SpbProbeOutcome: - if checks != PRIMITIVE_CHECKS: - raise RuntimeError("SPB passed outcome requires all primitive checks") - return _SpbProbeOutcome( - state=probe_contract.PROOF_STATE_PASSED, - checks=checks, - reason=None, - duration_ms=duration_ms, - ) - - -def _failed( - *, - reason: str, - checks: tuple[str, ...], - duration_ms: int, -) -> _SpbProbeOutcome: - if reason not in SPB_FAILED_REASONS: - raise RuntimeError("unsupported SPB failure reason") - if PRIMITIVE_CHECKS[: len(checks)] != checks: - raise RuntimeError("SPB failed outcome requires ordered primitive prefix") - return _SpbProbeOutcome( - state=probe_contract.PROOF_STATE_FAILED, - checks=checks, - reason=reason, - duration_ms=duration_ms, - ) - - -def _scrub_values( - *, - binding: HostedBinding, - proof_binding: HostedBinding, - attempt_dir: Path, - spb_root: Path, - restore_target: Path, -) -> tuple[str, ...]: - values = { - binding.broker_endpoint, - binding.account_id, - binding.instance_id, - binding.bucket, - binding.prefix, - binding.broker_token, - proof_binding.prefix, - str(attempt_dir), - str(spb_root), - str(restore_target), - LOGICAL_SOURCE_PATH, - } - return tuple(value for value in values if value) - - -__all__ = [ - "cleanup_spb_attempt_local", - "inspect_sandbox_spb_prerequisites", - "prove_spb_backup", -] diff --git a/solstone/think/sandbox_profile/spl_readiness.py b/solstone/think/sandbox_profile/spl_readiness.py deleted file mode 100644 index 1fc8a75f3..000000000 --- a/solstone/think/sandbox_profile/spl_readiness.py +++ /dev/null @@ -1,334 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Pure-read SPL proof readiness observer. - -The SPL relay proof and the follow-on P4e1 observer/landing lode both need the -same correlated pre-authorization view: one Callosum connection collects -``supervisor/status`` and ``link/health`` in arrival order, keeps that -connection open, and re-verifies freshness, process identity, relay generation, -and secure-listener acceptance immediately before the strict authorization -write. This module starts no services, creates no endpoints, writes no files, -and intentionally ignores Convey's cached HTTP status surface. - -The secure-listener acceptance check is a bounded connect-and-close to -127.0.0.1:7657 with no bytes sent. Convey logs the accepted connection and EOF -close as two info records; that is the accepted tradeoff for an external -bound/accepting probe. CLI and HTTP diagnostics remain byte-identical. -""" - -from __future__ import annotations - -import json -import os -import socket -import threading -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import psutil - -from solstone.think.callosum import CallosumConnection -from solstone.think.link.paths import DEFAULT_RELAY_URL - -STATUS_MAX_AGE_SECONDS = 5.0 -READINESS_WINDOW_SECONDS = 31.0 -SECURE_LISTENER_HOST = "127.0.0.1" -SECURE_LISTENER_PORT = 7657 -SECURE_LISTENER_TIMEOUT_SECONDS = 0.4 - - -class SplReadinessError(RuntimeError): - """Stable-code refusal from the SPL readiness observer.""" - - def __init__(self, code: str) -> None: - self.code = code - super().__init__(code) - - -@dataclass(frozen=True, slots=True) -class SplReadinessSnapshot: - supervisor_ref: str - spl_pid: int - spl_ref: str - convey_pid: int - convey_ref: str - spl_connection_state: str - listen_generation: int - link_health_observed_at_monotonic: float - observed_relay_origin: str - secure_listener_bound_accepting: bool - supervisor_observed_at_monotonic: float - - -@dataclass(frozen=True, slots=True) -class _ProcessIdentity: - pid: int - ref: str - create_time: float - - -class SplReadinessObserver: - """One-window SPL readiness collector held open until authorization.""" - - def __init__( - self, - journal: Path, - *, - window_seconds: float = READINESS_WINDOW_SECONDS, - ) -> None: - self._journal = Path(journal) - self._window_seconds = window_seconds - self._lock = threading.Lock() - self._ready = threading.Event() - self._conn: CallosumConnection | None = None - self._latest_supervisor: tuple[float, dict[str, Any]] | None = None - self._latest_link: tuple[float, dict[str, Any]] | None = None - self._processes: dict[str, _ProcessIdentity] = {} - - def __enter__(self) -> SplReadinessObserver: - sock_path = self._journal / "health" / "callosum.sock" - if not sock_path.exists(): - raise SplReadinessError("callosum_socket_missing") - self._conn = CallosumConnection(socket_path=sock_path) - self._conn.start(callback=self._on_callosum) - return self - - def __exit__(self, *_args: object) -> None: - self.close() - - def close(self) -> None: - conn = self._conn - self._conn = None - if conn is not None: - conn.stop() - - def wait_snapshot(self, *, deadline: float | None = None) -> SplReadinessSnapshot: - stop = min( - time.monotonic() + self._window_seconds, - deadline if deadline is not None else float("inf"), - ) - while True: - timeout = max(0.0, stop - time.monotonic()) - if timeout <= 0: - raise SplReadinessError("readiness_window_timeout") - if self._ready.wait(timeout=timeout): - break - snapshot = self._build_snapshot() - self._processes = { - "spl": _capture_process(snapshot.spl_pid, snapshot.spl_ref), - "convey": _capture_process(snapshot.convey_pid, snapshot.convey_ref), - } - return snapshot - - def reverify_before_authorization( - self, - snapshot: SplReadinessSnapshot, - ) -> None: - supervisor_observed, supervisor = self._latest_supervisor_event() - if time.monotonic() - supervisor_observed > STATUS_MAX_AGE_SECONDS: - raise SplReadinessError("supervisor_status_stale") - services = _services_by_name(supervisor) - crashed = _crashed_names(supervisor) - if "spl" in crashed or "convey" in crashed: - raise SplReadinessError("service_crashed") - supervisor_entry = _required_service(services, "supervisor") - if supervisor_entry["ref"] != snapshot.supervisor_ref: - raise SplReadinessError("supervisor_ref_changed") - spl = _required_service(services, "spl") - convey = _required_service(services, "convey") - _require_service_identity(spl, snapshot.spl_pid, snapshot.spl_ref) - _require_service_identity(convey, snapshot.convey_pid, snapshot.convey_ref) - _verify_process_identity(self._processes["spl"]) - _verify_process_identity(self._processes["convey"]) - link_observed, link = self._latest_link_event() - _validate_link_health(link) - if str(link.get("state")) != snapshot.spl_connection_state: - raise SplReadinessError("link_state_changed") - if int(link["listen_generation"]) != snapshot.listen_generation: - raise SplReadinessError("link_generation_changed") - if link_observed < snapshot.link_health_observed_at_monotonic: - raise SplReadinessError("link_health_regressed") - if not _secure_listener_accepting(): - raise SplReadinessError("secure_listener_unavailable") - - def _on_callosum(self, message: dict[str, Any]) -> None: - observed_at = time.monotonic() - tract = message.get("tract") - event = message.get("event") - with self._lock: - if tract == "supervisor" and event == "status": - self._latest_supervisor = (observed_at, dict(message)) - elif tract == "link" and event == "health": - self._latest_link = (observed_at, dict(message)) - if self._latest_supervisor is not None and self._latest_link is not None: - self._ready.set() - - def _build_snapshot(self) -> SplReadinessSnapshot: - supervisor_observed, supervisor = self._latest_supervisor_event() - link_observed, link = self._latest_link_event() - if time.monotonic() - supervisor_observed > STATUS_MAX_AGE_SECONDS: - raise SplReadinessError("supervisor_status_stale") - services = _services_by_name(supervisor) - crashed = _crashed_names(supervisor) - if "spl" in crashed or "convey" in crashed: - raise SplReadinessError("service_crashed") - supervisor_entry = _required_service(services, "supervisor") - spl = _required_service(services, "spl") - convey = _required_service(services, "convey") - _validate_link_health(link) - relay_origin = observed_relay_origin(self._journal) - listener_ok = _secure_listener_accepting() - if not listener_ok: - raise SplReadinessError("secure_listener_unavailable") - return SplReadinessSnapshot( - supervisor_ref=str(supervisor_entry["ref"]), - spl_pid=int(spl["pid"]), - spl_ref=str(spl["ref"]), - convey_pid=int(convey["pid"]), - convey_ref=str(convey["ref"]), - spl_connection_state=str(link["state"]), - listen_generation=int(link["listen_generation"]), - link_health_observed_at_monotonic=link_observed, - observed_relay_origin=relay_origin, - secure_listener_bound_accepting=listener_ok, - supervisor_observed_at_monotonic=supervisor_observed, - ) - - def _latest_supervisor_event(self) -> tuple[float, dict[str, Any]]: - with self._lock: - event = self._latest_supervisor - if event is None: - raise SplReadinessError("supervisor_status_missing") - return event - - def _latest_link_event(self) -> tuple[float, dict[str, Any]]: - with self._lock: - event = self._latest_link - if event is None: - raise SplReadinessError("link_health_missing") - return event - - -def observed_relay_origin(journal: Path) -> str: - if os.environ.get("SOL_LINK_RELAY_URL", "").strip(): - raise SplReadinessError("relay_env_override") - config_path = Path(journal) / "config" / "journal.json" - try: - raw = json.loads(config_path.read_text("utf-8")) - except FileNotFoundError: - raw = {} - except (json.JSONDecodeError, OSError): - raise SplReadinessError("relay_config_unreadable") from None - link_cfg = raw.get("link") if isinstance(raw, dict) else None - if isinstance(link_cfg, dict) and "relay_url" in link_cfg: - value = link_cfg.get("relay_url") - if isinstance(value, str) and value.strip(): - raise SplReadinessError("relay_config_override") - return DEFAULT_RELAY_URL - - -def open_spl_readiness_observer(journal: Path) -> SplReadinessObserver: - return SplReadinessObserver(journal) - - -def _services_by_name(message: dict[str, Any]) -> dict[str, dict[str, Any]]: - services = message.get("services") - if not isinstance(services, list): - raise SplReadinessError("supervisor_services_invalid") - out: dict[str, dict[str, Any]] = {} - for service in services: - if not isinstance(service, dict): - continue - name = service.get("name") - if isinstance(name, str): - out[name] = service - return out - - -def _crashed_names(message: dict[str, Any]) -> set[str]: - crashed = message.get("crashed") - if not isinstance(crashed, list): - return set() - names: set[str] = set() - for service in crashed: - if isinstance(service, dict) and isinstance(service.get("name"), str): - names.add(str(service["name"])) - return names - - -def _required_service( - services: dict[str, dict[str, Any]], - name: str, -) -> dict[str, Any]: - service = services.get(name) - if service is None: - raise SplReadinessError(f"{name}_service_missing") - if not isinstance(service.get("ref"), str) or not service.get("ref"): - raise SplReadinessError(f"{name}_service_ref_missing") - if not isinstance(service.get("pid"), int): - raise SplReadinessError(f"{name}_service_pid_missing") - return service - - -def _require_service_identity( - service: dict[str, Any], - expected_pid: int, - expected_ref: str, -) -> None: - if service["pid"] != expected_pid or service["ref"] != expected_ref: - raise SplReadinessError("service_identity_changed") - - -def _capture_process(pid: int, ref: str) -> _ProcessIdentity: - identity = _ProcessIdentity(pid=pid, ref=ref, create_time=_process_create_time(pid)) - _verify_process_identity(identity) - return identity - - -def _verify_process_identity(identity: _ProcessIdentity) -> None: - try: - os.kill(identity.pid, 0) - except OSError: - raise SplReadinessError("process_not_live") from None - if _process_create_time(identity.pid) != identity.create_time: - raise SplReadinessError("process_replaced") - - -def _process_create_time(pid: int) -> float: - try: - return psutil.Process(pid).create_time() - except psutil.Error: - raise SplReadinessError("process_create_time_unavailable") from None - - -def _validate_link_health(message: dict[str, Any]) -> None: - state = message.get("state") - generation = message.get("listen_generation") - if state != "connected": - raise SplReadinessError("link_state_not_connected") - if not isinstance(generation, int): - raise SplReadinessError("link_generation_missing") - - -def _secure_listener_accepting() -> bool: - try: - sock = socket.create_connection( - (SECURE_LISTENER_HOST, SECURE_LISTENER_PORT), - timeout=SECURE_LISTENER_TIMEOUT_SECONDS, - ) - except OSError: - return False - with sock: - return True - - -__all__ = [ - "SplReadinessError", - "SplReadinessObserver", - "SplReadinessSnapshot", - "open_spl_readiness_observer", - "observed_relay_origin", -] diff --git a/solstone/think/sandbox_profile/spl_relay_tunnel.py b/solstone/think/sandbox_profile/spl_relay_tunnel.py deleted file mode 100644 index b84660b36..000000000 --- a/solstone/think/sandbox_profile/spl_relay_tunnel.py +++ /dev/null @@ -1,693 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""SPL relay tunnel proof primitive. - -This library-only primitive performs the transport/identity half of the SPL -production proof. It creates an in-memory ECDSA-P256 client key and CSR, signs -it with the existing home CA, writes one strict attempt authorization, enrolls -and dials the pinned production relay, and returns an opaque lease plus a closed -diagnostic. It does not register observers, transfer segments, write probe -ledger records, start services, add endpoints, or create persistent identity -files. - -Failure mapping: -- readiness or relay pin refusal -> capability_not_ready, checks=[] -- strict store rejection while creating authorization with no mutation -> - capability_not_ready, checks=[] -- strict store rejection while creating authorization with ambiguous mutation -> - cleanup_unverified, checks=[] -- strict store rejection during cleanup -> cleanup_unverified, overriding all - prior reasons -- local or request timeout -> deadline_exceeded, checks earned so far -- enrollment HTTP error, relay upgrade refusal, or inner-TLS authorization - refusal -> remote_rejected, checks earned so far -- malformed enrollment response, malformed upgrade, or malformed mux/session - success -> response_invalid, checks earned so far -- unexpected local failure -> internal_error, checks earned so far - -Cancellation intentionally is not part of this primitive's diagnostic -vocabulary. Cooperative cancellation and outer task cancellation run bounded -cleanup; verified cleanup re-raises asyncio.CancelledError so the coordinator -can record probe_contract.REASON_CANCELLED. If cleanup is ambiguous, this -primitive returns cleanup_unverified instead because a possible leaked -authorization must not be hidden by cancellation. - -RelayTunnelLease.close() returns None on verified success and is a no-op after -success. On ambiguous cleanup it raises RelayTunnelCloseError with only the -stable reason cleanup_unverified, leaves the lease open/retryable, and continues -to own the authorization. The proof diagnostic already returned on pass is -immutable; a later close failure is recorded separately by the coordinator. -The composing coordinator reserves the full 20s SPL cleanup budget; this -primitive consumes at most CLEANUP_SHIELD_SECONDS of that budget. -The secure listener's touch_last_seen path is a non-strict writer and may -independently normalize unrelated entries while the lease is live. This -primitive's cleanup guarantee is scoped to its own removal: inside the cleanup -lock it re-reads the current raw store and verifies that removal touched only -the attempt entry present in that read. - -Attestation note: current SPL relay enrollment verifies the home-signed ES256 -attestation and uses its device_fp claim as the device identity; the relay does -not parse or recompute the client certificate. The certificate is for the home -secure listener's inner TLS authorization. The external proto/tokens.md text is -older and contradictory; this module follows current relay behavior. -""" - -from __future__ import annotations - -import asyncio -import base64 -import contextlib -import hashlib -import json -import logging -import stat -import time -import urllib.parse -from collections.abc import Callable, Iterator -from dataclasses import dataclass -from pathlib import Path - -import websockets -from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509.oid import NameOID - -from solstone.think.link import client as link_client -from solstone.think.link.auth import ( - AuthorizedClients, - StrictAuthorizationError, - StrictAuthorizationReceipt, -) -from solstone.think.link.ca import ( - ca_is_present, - cert_fingerprint, - load_ca, - mint_attestation, - sign_csr, -) -from solstone.think.link.paths import DEFAULT_RELAY_URL -from solstone.think.sandbox_profile import probe_contract, spl_readiness - -WORK_DEADLINE_SECONDS = 60.0 -ENROLLMENT_DEADLINE_SECONDS = 30.0 -CLEANUP_SHIELD_SECONDS = 15.0 -SPL_CHECKS = probe_contract.PROOF_CHECKS[probe_contract.CAPABILITY_SPL][:3] -ALLOWED_FAILED_REASONS = frozenset( - { - probe_contract.REASON_CAPABILITY_NOT_READY, - probe_contract.REASON_DEADLINE_EXCEEDED, - probe_contract.REASON_REMOTE_REJECTED, - probe_contract.REASON_RESPONSE_INVALID, - probe_contract.REASON_CLEANUP_UNVERIFIED, - probe_contract.REASON_INTERNAL_ERROR, - } -) - - -class SplRelayTunnelError(RuntimeError): - """Stable-reason proof failure.""" - - def __init__( - self, - reason: str, - *, - checks: tuple[str, ...] | None = None, - ) -> None: - self.reason = reason - self.checks = checks - super().__init__(reason) - - -class RelayTunnelCloseError(RuntimeError): - """Stable-reason lease cleanup failure.""" - - def __init__(self) -> None: - self.reason = probe_contract.REASON_CLEANUP_UNVERIFIED - super().__init__(self.reason) - - -@dataclass(frozen=True, slots=True) -class SplRelayTunnelOutcome: - state: str - checks: tuple[str, ...] - reason: str | None - duration_ms: int - - def to_dict(self) -> dict[str, object]: - return { - probe_contract.FIELD_STATE: self.state, - probe_contract.FIELD_CHECKS: self.checks, - probe_contract.FIELD_REASON: self.reason, - probe_contract.FIELD_DURATION_MS: self.duration_ms, - } - - -class RelayTunnelLease: - def __init__( - self, - *, - session: link_client.TunnelSession, - store: AuthorizedClients, - receipt: StrictAuthorizationReceipt, - ) -> None: - self._session: link_client.TunnelSession | None = session - self._store = store - self._receipt: StrictAuthorizationReceipt | None = receipt - self._closed = False - self._transport_closed = False - - def __repr__(self) -> str: - state = "closed" if self._closed else "open" - return f"RelayTunnelLease(state={state}, )" - - def __reduce__(self) -> object: - raise TypeError("RelayTunnelLease is not serializable") - - def __copy__(self) -> object: - raise TypeError("RelayTunnelLease is not copyable") - - def __deepcopy__(self, _memo: dict[int, object]) -> object: - raise TypeError("RelayTunnelLease is not copyable") - - async def __aenter__(self) -> RelayTunnelLease: - return self - - async def __aexit__(self, *_args: object) -> None: - await self.close() - - @property - def is_closed(self) -> bool: - return self._closed - - async def request( - self, - method: str, - path: str, - *, - headers: dict[str, str] | None = None, - body: bytes | link_client.BodySource = b"", - ) -> tuple[int, dict[str, str], bytes]: - session = self._require_session() - return await session.request(method, path, headers=headers, body=body) - - async def stream_request( - self, - method: str, - path: str, - *, - headers: dict[str, str] | None = None, - body: bytes | link_client.BodySource = b"", - ) -> tuple[int, dict[str, str], bytes, object]: - session = self._require_session() - return await session.stream_request(method, path, headers=headers, body=body) - - async def close(self) -> None: - if self._closed: - return - ok = await _cleanup_transport_and_authorization( - session=self._session, - store=self._store, - receipt=self._receipt, - transport_already_closed=self._transport_closed, - ) - if not ok: - raise RelayTunnelCloseError() - self._closed = True - self._transport_closed = True - self._session = None - self._receipt = None - - def _require_session(self) -> link_client.TunnelSession: - if self._closed or self._session is None: - raise RuntimeError("relay tunnel lease is closed") - return self._session - - -async def prove_spl_relay_tunnel( - journal: Path, - *, - attempt_dir: Path, - cancel_requested: Callable[[], bool], -) -> tuple[RelayTunnelLease | None, dict[str, object]]: - start = time.monotonic() - work_deadline = start + WORK_DEADLINE_SECONDS - checks: tuple[str, ...] = tuple(SPL_CHECKS[:0]) - session: link_client.TunnelSession | None = None - store: AuthorizedClients | None = None - receipt: StrictAuthorizationReceipt | None = None - - try: - _check_cancel(cancel_requested) - _validate_attempt_dir(Path(journal), attempt_dir) - _assert_pinned_origin(Path(journal)) - with spl_readiness.open_spl_readiness_observer(Path(journal)) as observer: - snapshot = observer.wait_snapshot(deadline=work_deadline) - _check_cancel(cancel_requested) - identity = _build_ephemeral_identity(Path(journal), attempt_dir) - _assert_fingerprint_binding(identity) - _assert_enrollment_origin() - _assert_pinned_origin(Path(journal)) - observer.reverify_before_authorization(snapshot) - _check_cancel(cancel_requested) - store = AuthorizedClients(_authorized_clients_path(Path(journal))) - try: - receipt = store.add_attempt_client_strict( - fingerprint=identity.fingerprint, - device_label=_authorization_label(attempt_dir), - instance_id=identity.home_instance_id, - network="pl-via-spl", - ) - except StrictAuthorizationError as exc: - if exc.mutated: - return None, _failed( - probe_contract.REASON_CLEANUP_UNVERIFIED, - checks, - _duration_ms(start), - ).to_dict() - return None, _failed( - probe_contract.REASON_CAPABILITY_NOT_READY, - checks, - _duration_ms(start), - ).to_dict() - - _check_cancel(cancel_requested) - with _suppress_client_boundary_logs(identity.fingerprint): - enrolled = await _enroll_with_deadline(identity, work_deadline) - checks = tuple(SPL_CHECKS[:1]) - session = await _dial_with_deadline(enrolled, work_deadline) - checks = tuple(SPL_CHECKS[:3]) - _redact_session_task_names(session) - lease = RelayTunnelLease(session=session, store=store, receipt=receipt) - return lease, _passed(_duration_ms(start)).to_dict() - except asyncio.CancelledError: - if receipt is None: - raise - ok = await _cleanup_with_shield(session=session, store=store, receipt=receipt) - if ok: - raise - return None, _failed( - probe_contract.REASON_CLEANUP_UNVERIFIED, - checks, - _duration_ms(start), - ).to_dict() - except SplRelayTunnelError as exc: - if exc.checks is not None: - checks = exc.checks - ok = True - if receipt is not None: - ok = await _cleanup_with_shield( - session=session, store=store, receipt=receipt - ) - reason = exc.reason if ok else probe_contract.REASON_CLEANUP_UNVERIFIED - return None, _failed(reason, checks, _duration_ms(start)).to_dict() - except spl_readiness.SplReadinessError: - return None, _failed( - probe_contract.REASON_CAPABILITY_NOT_READY, - checks, - _duration_ms(start), - ).to_dict() - except Exception: - ok = True - if receipt is not None: - ok = await _cleanup_with_shield( - session=session, store=store, receipt=receipt - ) - reason = ( - probe_contract.REASON_INTERNAL_ERROR - if ok - else probe_contract.REASON_CLEANUP_UNVERIFIED - ) - return None, _failed(reason, checks, _duration_ms(start)).to_dict() - - -def _passed(duration_ms: int) -> SplRelayTunnelOutcome: - return SplRelayTunnelOutcome( - state=probe_contract.PROOF_STATE_PASSED, - checks=tuple(SPL_CHECKS), - reason=None, - duration_ms=duration_ms, - ) - - -def _failed( - reason: str, - checks: tuple[str, ...], - duration_ms: int, -) -> SplRelayTunnelOutcome: - if reason not in ALLOWED_FAILED_REASONS: - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) - return SplRelayTunnelOutcome( - state=probe_contract.PROOF_STATE_FAILED, - checks=checks, - reason=reason, - duration_ms=duration_ms, - ) - - -async def _enroll_with_deadline( - identity: link_client.ClientIdentity, - work_deadline: float, -) -> link_client.EnrolledDevice: - timeout = min(ENROLLMENT_DEADLINE_SECONDS, _remaining(work_deadline)) - if timeout <= 0: - raise SplRelayTunnelError(probe_contract.REASON_DEADLINE_EXCEEDED) - try: - return await asyncio.wait_for( - link_client.Client.enroll_device_async( - DEFAULT_RELAY_URL, - identity, - timeout=timeout, - ), - timeout=timeout, - ) - except asyncio.TimeoutError: - raise SplRelayTunnelError(probe_contract.REASON_DEADLINE_EXCEEDED) from None - except RuntimeError as exc: - text = str(exc) - if "missing string field" in text or "unexpected JSON response" in text: - raise SplRelayTunnelError(probe_contract.REASON_RESPONSE_INVALID) from None - raise SplRelayTunnelError(probe_contract.REASON_REMOTE_REJECTED) from None - except Exception as exc: - if exc.__class__.__name__.endswith("TimeoutException"): - raise SplRelayTunnelError(probe_contract.REASON_DEADLINE_EXCEEDED) from None - raise SplRelayTunnelError(probe_contract.REASON_REMOTE_REJECTED) from None - - -async def _dial_with_deadline( - enrolled: link_client.EnrolledDevice, - work_deadline: float, -) -> link_client.TunnelSession: - url = link_client._relay_dial_url(DEFAULT_RELAY_URL, enrolled) - _assert_ws_origin(url) - timeout = _remaining(work_deadline) - if timeout <= 0: - raise SplRelayTunnelError(probe_contract.REASON_DEADLINE_EXCEEDED) - try: - link_client.LOG.info( - "client %s: dialing %s", - enrolled.identity.fingerprint, - link_client._redact_url(url), - ) - ws = await asyncio.wait_for( - websockets.connect(url, max_size=None), timeout=timeout - ) - except asyncio.TimeoutError: - raise SplRelayTunnelError(probe_contract.REASON_DEADLINE_EXCEEDED) from None - except Exception as exc: - text = str(exc).lower() - if "malformed" in text or "invalid" in text: - raise SplRelayTunnelError(probe_contract.REASON_RESPONSE_INVALID) from None - raise SplRelayTunnelError(probe_contract.REASON_REMOTE_REJECTED) from None - - try: - session = await asyncio.wait_for( - link_client._open_tunnel_session( - link_client._WsEncryptedTransport(ws), - enrolled.identity, - ), - timeout=max(0.0, _remaining(work_deadline)), - ) - except asyncio.TimeoutError: - with contextlib.suppress(Exception): - await ws.close() - raise SplRelayTunnelError( - probe_contract.REASON_DEADLINE_EXCEEDED, - checks=tuple(SPL_CHECKS[:2]), - ) from None - except Exception as exc: - with contextlib.suppress(Exception): - await ws.close() - if "handshake" in str(exc).lower() or "certificate" in str(exc).lower(): - raise SplRelayTunnelError( - probe_contract.REASON_REMOTE_REJECTED, - checks=tuple(SPL_CHECKS[:2]), - ) from None - raise SplRelayTunnelError( - probe_contract.REASON_RESPONSE_INVALID, - checks=tuple(SPL_CHECKS[:2]), - ) from None - if not session.is_alive: - with contextlib.suppress(Exception): - await session.close() - raise SplRelayTunnelError( - probe_contract.REASON_RESPONSE_INVALID, - checks=tuple(SPL_CHECKS[:2]), - ) - return session - - -async def _cleanup_with_shield( - *, - session: link_client.TunnelSession | None, - store: AuthorizedClients | None, - receipt: StrictAuthorizationReceipt, -) -> bool: - cleanup_task = asyncio.create_task( - asyncio.wait_for( - _cleanup_transport_and_authorization( - session=session, - store=store, - receipt=receipt, - ), - timeout=CLEANUP_SHIELD_SECONDS, - ), - name="spl-proof-cleanup", - ) - try: - return await asyncio.shield(cleanup_task) - except asyncio.CancelledError: - try: - return await cleanup_task - except Exception: - return False - except Exception: - return False - - -async def _cleanup_transport_and_authorization( - *, - session: link_client.TunnelSession | None, - store: AuthorizedClients | None, - receipt: StrictAuthorizationReceipt | None, - transport_already_closed: bool = False, -) -> bool: - if receipt is None or store is None: - return session is None - ok = True - if session is not None and not transport_already_closed: - try: - await session.close() - except Exception: - return False - try: - store.remove_attempt_client_strict(receipt) - except StrictAuthorizationError: - ok = False - return ok - - -def _check_cancel(cancel_requested: Callable[[], bool]) -> None: - if cancel_requested(): - raise asyncio.CancelledError - - -def _duration_ms(start: float) -> int: - return max(0, int((time.monotonic() - start) * 1000)) - - -def _remaining(deadline: float) -> float: - return max(0.0, deadline - time.monotonic()) - - -def _validate_attempt_dir(journal: Path, attempt_dir: Path) -> Path: - parent = probe_contract.probe_attempts_parent_path(journal).resolve() - try: - current = attempt_dir.lstat() - except OSError: - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) from None - if stat.S_ISLNK(current.st_mode) or not stat.S_ISDIR(current.st_mode): - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) - if stat.S_IMODE(current.st_mode) != probe_contract.ATTEMPT_DIR_MODE: - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) - if attempt_dir.resolve().parent != parent: - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) - return attempt_dir.resolve() - - -def _authorization_label(attempt_dir: Path) -> str: - digest = hashlib.sha256(attempt_dir.name.encode("ascii")).hexdigest()[:32] - return f"sandbox-spl-{digest}" - - -def _build_ephemeral_identity( - journal: Path, - attempt_dir: Path, -) -> link_client.ClientIdentity: - ca_root = journal / "link" / "ca" - if not ca_is_present(ca_root): - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - try: - home_ca = load_ca(ca_root) - state = _read_link_state(journal) - except (OSError, ValueError): - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - private_key, private_key_pem, csr_pem = _build_csr( - _authorization_label(attempt_dir) - ) - del private_key - try: - client_cert_pem, fingerprint = sign_csr( - home_ca, - csr_pem, - _authorization_label(attempt_dir), - ) - attestation = mint_attestation(home_ca, state["instance_id"], fingerprint) - except (OSError, ValueError): - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - ca_chain_pem = home_ca.cert.public_bytes(serialization.Encoding.PEM).decode("ascii") - return link_client.ClientIdentity( - private_key_pem=private_key_pem.decode("ascii"), - client_cert_pem=client_cert_pem, - ca_chain_pem=ca_chain_pem, - fingerprint=fingerprint, - home_instance_id=state["instance_id"], - home_label=state["home_label"], - home_attestation=attestation, - local_endpoints=(), - ) - - -def _build_csr(label: str) -> tuple[ec.EllipticCurvePrivateKey, bytes, str]: - private_key = ec.generate_private_key(ec.SECP256R1()) - csr = ( - x509.CertificateSigningRequestBuilder() - .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, label[:64])])) - .sign(private_key, hashes.SHA256()) - ) - private_key_pem = private_key.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - serialization.NoEncryption(), - ) - return ( - private_key, - private_key_pem, - csr.public_bytes(serialization.Encoding.PEM).decode("ascii"), - ) - - -def _read_link_state(journal: Path) -> dict[str, str]: - try: - raw = json.loads((journal / "link" / "state.json").read_text("utf-8")) - except (FileNotFoundError, json.JSONDecodeError, OSError): - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) from None - if not isinstance(raw, dict): - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - instance_id = raw.get("instance_id") - home_label = raw.get("home_label") or "solstone" - if not isinstance(instance_id, str) or not instance_id: - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - if not isinstance(home_label, str) or not home_label: - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - return {"instance_id": instance_id, "home_label": home_label} - - -def _assert_fingerprint_binding(identity: link_client.ClientIdentity) -> None: - try: - cert_fp = cert_fingerprint(identity.client_cert_pem) - attestation_fp = _attestation_device_fp(identity.home_attestation) - except (ValueError, json.JSONDecodeError): - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) from None - if cert_fp != identity.fingerprint or attestation_fp != identity.fingerprint: - raise SplRelayTunnelError(probe_contract.REASON_INTERNAL_ERROR) - - -def _attestation_device_fp(attestation: str) -> str: - parts = attestation.split(".") - if len(parts) != 3: - raise ValueError("bad attestation") - payload = parts[1] + "=" * (-len(parts[1]) % 4) - claims = json.loads(base64.urlsafe_b64decode(payload.encode("ascii"))) - value = claims.get("device_fp") if isinstance(claims, dict) else None - if not isinstance(value, str) or not value: - raise ValueError("bad device_fp") - return value - - -def _assert_pinned_origin(journal: Path) -> None: - if spl_readiness.observed_relay_origin(journal) != DEFAULT_RELAY_URL: - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - _assert_http_origin(f"{DEFAULT_RELAY_URL}/enroll/device") - - -def _assert_enrollment_origin() -> None: - _assert_http_origin(link_client._enroll_device_endpoint(DEFAULT_RELAY_URL)) - - -def _assert_http_origin(url: str) -> None: - parsed = urllib.parse.urlparse(url) - if f"{parsed.scheme}://{parsed.netloc}" != DEFAULT_RELAY_URL: - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - - -def _assert_ws_origin(url: str) -> None: - parsed = urllib.parse.urlparse(url) - expected = link_client._to_ws(DEFAULT_RELAY_URL) - if f"{parsed.scheme}://{parsed.netloc}" != expected: - raise SplRelayTunnelError(probe_contract.REASON_CAPABILITY_NOT_READY) - - -def _authorized_clients_path(journal: Path) -> Path: - return journal / "link" / "authorized_clients.json" - - -def _redact_session_task_names(session: link_client.TunnelSession) -> None: - reader = getattr(session, "_reader_task", None) - if isinstance(reader, asyncio.Task): - reader.set_name("spl-proof-relay-reader") - keepalive = getattr(session, "_keepalive_task", None) - if isinstance(keepalive, asyncio.Task): - keepalive.set_name("spl-proof-relay-keepalive") - - -class _ProofLogFilter(logging.Filter): - def __init__(self, fingerprint: str) -> None: - super().__init__() - self._fingerprint = fingerprint - - def filter(self, record: logging.LogRecord) -> bool: - if record.name != link_client.LOG.name: - return True - message = record.getMessage() - if self._fingerprint in message and ( - "enrolling device token" in message - or "enroll complete" in message - or "dialing " in message - ): - return False - return True - - -@contextlib.contextmanager -def _suppress_client_boundary_logs(fingerprint: str) -> Iterator[None]: - logger = logging.getLogger(link_client.LOG.name) - log_filter = _ProofLogFilter(fingerprint) - logger.addFilter(log_filter) - try: - yield - finally: - logger.removeFilter(log_filter) - - -__all__ = [ - "CLEANUP_SHIELD_SECONDS", - "ENROLLMENT_DEADLINE_SECONDS", - "RelayTunnelCloseError", - "RelayTunnelLease", - "SplRelayTunnelError", - "SplRelayTunnelOutcome", - "WORK_DEADLINE_SECONDS", - "prove_spl_relay_tunnel", -] diff --git a/solstone/think/sol_cli.py b/solstone/think/sol_cli.py index 35dd7589b..52ada5610 100644 --- a/solstone/think/sol_cli.py +++ b/solstone/think/sol_cli.py @@ -160,7 +160,6 @@ COMMANDS: dict[str, Command] = { "talent": Command("solstone.think.talent_cli", "service"), "link": Command("solstone.think.link", "universal"), "spl": Command("solstone.think.spl", "service"), - "sandbox-profile": Command("solstone.think.sandbox_profile.cli", "service"), "navigate": Command("solstone.think.tools.navigate", "service"), "identity": Command("solstone.think.tools.sol", "service"), "engage": Command("solstone.think.engage", "service"), diff --git a/tests/sandbox_profile/__init__.py b/tests/sandbox_profile/__init__.py deleted file mode 100644 index 35469207f..000000000 --- a/tests/sandbox_profile/__init__.py +++ /dev/null @@ -1,241 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Shared helpers for sandbox profile tests.""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -from typer.testing import CliRunner - -from solstone.think.sandbox_profile import cli, manifest, probe_contract, probe_records - -RUN_ID = "86d9eb6c-d64e-4ae5-b29e-524ddf57a013" -OTHER_RUN_ID = "11111111-1111-4111-8111-111111111111" -ATTEMPT_ID = "22222222-2222-4222-8222-222222222222" -OTHER_ATTEMPT_ID = "33333333-3333-4333-8333-333333333333" -THIRD_ATTEMPT_ID = "44444444-4444-4444-8444-444444444444" -FIXED_TS = "2026-01-01T00:00:00.000Z" - - -def write_marker( - journal: Path, - *, - run_id: str = RUN_ID, - payload: dict[str, Any] | None = None, -) -> dict[str, Any]: - marker_payload = { - "kind": manifest.MARKER_KIND, - "contract_version": manifest.CONTRACT_VERSION, - "profile": manifest.PROFILE, - "run_id": run_id, - "journal_path": str(journal.resolve()), - } - if payload is not None: - marker_payload = payload - journal.mkdir(parents=True, exist_ok=True) - (journal / ".solstone-sandbox.json").write_text( - json.dumps(marker_payload, indent=2) + "\n", - encoding="utf-8", - ) - return marker_payload - - -def sandbox_journal(tmp_path: Path, monkeypatch, *, run_id: str = RUN_ID) -> Path: - journal = tmp_path / "journal" - write_marker(journal, run_id=run_id) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal.resolve())) - return journal - - -def invoke(args: list[str], *, input_text: str | None = None): - return CliRunner().invoke( - cli.app, - args, - input=input_text, - catch_exceptions=False, - ) - - -def output_json(result) -> dict[str, Any]: - return json.loads(result.output) - - -def read_json(path: Path) -> dict[str, Any]: - return json.loads(path.read_text("utf-8")) - - -def write_attempt_dir( - journal: Path, attempt_id: str = ATTEMPT_ID, *, mode: int = 0o700 -) -> Path: - path = probe_contract.probe_attempts_parent_path(journal) / attempt_id - path.mkdir(mode=mode, parents=True, exist_ok=True) - path.chmod(mode) - return path - - -def write_ledger(journal: Path, records: list[dict[str, object]]) -> Path: - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - lines = [ - json.dumps(record, ensure_ascii=False, separators=(",", ":"), sort_keys=True) - for record in records - ] - path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8") - return path - - -def start_record( - *, - run_id: str = RUN_ID, - attempt_id: str = ATTEMPT_ID, - selected: tuple[str, ...] | None = None, - execution_order: tuple[str, ...] | None = None, -) -> dict[str, object]: - selected = selected or probe_contract.CAPABILITY_ORDER[:1] - execution_order = execution_order or selected - return probe_records.build_attempt_started_record( - run_id=run_id, - attempt_id=attempt_id, - selected=selected, - execution_order=execution_order, - started_at=FIXED_TS, - ).to_json_obj() - - -def proof_record( - *, - run_id: str = RUN_ID, - attempt_id: str = ATTEMPT_ID, - proof: str | None = None, - state: str = probe_contract.PROOF_STATE_PASSED, - checks: tuple[str, ...] | None = None, - reason: str | None = None, - duration_ms: int | None = 1, -) -> dict[str, object]: - proof = proof or probe_contract.CAPABILITY_ORDER[0] - if checks is None: - checks = ( - probe_contract.PROOF_CHECKS[proof] - if state == probe_contract.PROOF_STATE_PASSED - else () - ) - return probe_records.build_proof_terminal_record( - run_id=run_id, - attempt_id=attempt_id, - proof=proof, - state=state, - checks=checks, - reason=reason, - duration_ms=duration_ms, - finished_at=FIXED_TS, - ).to_json_obj() - - -def terminal_record( - *, - run_id: str = RUN_ID, - attempt_id: str = ATTEMPT_ID, - proofs: list[dict[str, object]] | None = None, -) -> dict[str, object]: - proof_records = [ - probe_records.validate_proof_terminal_payload(proof) - for proof in (proofs or [proof_record()]) - ] - return probe_records.build_attempt_terminal_record( - run_id=run_id, - attempt_id=attempt_id, - proofs=proof_records, - finished_at=FIXED_TS, - ).to_json_obj() - - -def complete_attempt_records( - *, - run_id: str = RUN_ID, - attempt_id: str = ATTEMPT_ID, - selected: tuple[str, ...] | None = None, - execution_order: tuple[str, ...] | None = None, - proof_overrides: dict[str, dict[str, object]] | None = None, -) -> list[dict[str, object]]: - selected = selected or probe_contract.CAPABILITY_ORDER[:1] - execution_order = execution_order or selected - records: list[dict[str, object]] = [ - start_record( - run_id=run_id, - attempt_id=attempt_id, - selected=selected, - execution_order=execution_order, - ) - ] - proofs: list[dict[str, object]] = [] - for proof in execution_order: - override = (proof_overrides or {}).get(proof, {}) - proof_payload = proof_record( - run_id=run_id, - attempt_id=attempt_id, - proof=proof, - state=override.get("state", probe_contract.PROOF_STATE_PASSED), # type: ignore[arg-type] - checks=override.get("checks"), # type: ignore[arg-type] - reason=override.get("reason"), # type: ignore[arg-type] - duration_ms=override.get("duration_ms", 1), # type: ignore[arg-type] - ) - proofs.append(proof_payload) - records.append(proof_payload) - records.append( - terminal_record( - run_id=run_id, - attempt_id=attempt_id, - proofs=proofs, - ) - ) - return records - - -def scout_payload(secret: str = "fake-google-key") -> dict[str, str]: - return { - "google_api_key": secret, - "dispatch_token": "dispatch-token", - "account_id": "acct-scout", - "created_at": "2026-01-01T00:00:00Z", - } - - -def spl_payload() -> dict[str, object]: - return { - "service": "spl", - "state": "approved", - "approved_at": "2026-01-01T00:00:00Z", - } - - -def spb_payload(journal: Path, *, instance_id: str | None = None) -> dict[str, str]: - state = read_json(journal / "link" / "state.json") - return { - "broker_endpoint": "https://broker.example.invalid", - "account_id": "acct-backup", - "instance_id": instance_id or str(state["instance_id"]), - "bucket": "sandbox-bucket", - "prefix": "sandbox-prefix/", - "broker_token": "broker-token", - } - - -def spp_payload(*, endpoint_url: str = "http://127.0.0.1:9100") -> dict[str, str]: - return { - "endpoint_url": endpoint_url, - "served_model_id": "synthetic-model", - "credential": "spp-secret", - "account_id": "acct-spp", - "created_at": "2026-01-01T00:00:00Z", - } - - -def prepare_ok(journal: Path) -> dict[str, Any]: - result = invoke(["prepare", "--json"]) - assert result.exit_code == 0, result.output - assert (journal / "health" / "sandbox-profile" / "intent.json").exists() - return output_json(result) diff --git a/tests/sandbox_profile/test_apply.py b/tests/sandbox_profile/test_apply.py deleted file mode 100644 index c92b4f838..000000000 --- a/tests/sandbox_profile/test_apply.py +++ /dev/null @@ -1,150 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -import pytest - -from tests.sandbox_profile import ( - invoke, - output_json, - prepare_ok, - read_json, - sandbox_journal, - scout_payload, - spb_payload, - spp_payload, -) - - -def test_local_apply_capabilities_compose_existing_owner_functions( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - scout = invoke(["apply", "scout", "--json"], input_text=json.dumps(scout_payload())) - spb = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - spp = invoke(["apply", "spp", "--json"], input_text=json.dumps(spp_payload())) - - assert scout.exit_code == 0 - assert spb.exit_code == 0 - assert spp.exit_code == 0 - config = read_json(journal / "config" / "journal.json") - intent_payload = read_json(journal / "health" / "sandbox-profile" / "intent.json") - assert config["env"]["GOOGLE_API_KEY"] == "fake-google-key" - assert config["services"]["scout"]["key_fingerprint_sha256"] - assert ( - intent_payload["observed_at_apply"]["scout"]["account_id"] - == config["services"]["scout"]["account_id"] - ) - assert config["backup"]["mode"] == "operated" - assert config["backup"]["enabled"] is True - assert ( - journal / "backup" / "hosted" / "binding.json" - ).stat().st_mode & 0o777 == 0o600 - assert config["services"]["confidential"]["credential_fingerprint_sha256"] - - -@pytest.mark.parametrize( - ("capability", "payload_factory", "residual"), - [ - ("scout", lambda journal: scout_payload(), "scout_block_missing"), - ("spb", spb_payload, "spb_binding_missing"), - ("spp", lambda journal: spp_payload(), "spp_block_missing"), - ], -) -@pytest.mark.parametrize("fail_on_replace", [1, 2]) -def test_apply_atomic_faults_are_intent_first_and_status_names_partial_state( - tmp_path, - monkeypatch, - capability: str, - payload_factory, - residual: str, - fail_on_replace: int, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - real_replace = __import__("os").replace - calls = {"count": 0} - - def flaky_replace(src, dst): - calls["count"] += 1 - if calls["count"] == fail_on_replace: - raise OSError("forced commit failure") - real_replace(src, dst) - - monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", flaky_replace) - - result = invoke( - ["apply", capability, "--json"], - input_text=json.dumps(payload_factory(journal)), - ) - status = invoke(["status", "--json"]) - status_body = output_json(status) - cap_status = next( - cap for cap in status_body["capabilities"] if cap["name"] == capability - ) - - assert result.exit_code == 1 - if fail_on_replace == 1: - assert cap_status["state"] == "not_applied" - else: - assert status.exit_code == 3 - assert cap_status["state"] == "degraded" - assert residual in cap_status["residuals"] - - -@pytest.mark.parametrize( - ("fail_on_replace", "residual"), - [ - (2, "spb_binding_missing"), - (3, "spb_binding_missing"), - (4, "spb_binding_missing"), - (5, "spb_backup_config_incomplete"), - (6, "spb_backup_config_incomplete"), - (7, "intent_finalize_missing"), - ], -) -def test_spb_apply_fault_boundaries_are_named_and_retry_converges( - tmp_path, - monkeypatch, - fail_on_replace: int, - residual: str, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - payload = spb_payload(journal) - real_replace = __import__("os").replace - calls = {"count": 0} - - def flaky_replace(src, dst): - calls["count"] += 1 - if calls["count"] == fail_on_replace: - raise OSError("forced commit failure") - real_replace(src, dst) - - monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", flaky_replace) - - failed = invoke(["apply", "spb", "--json"], input_text=json.dumps(payload)) - status = invoke(["status", "--json"]) - status_body = output_json(status) - spb = next(cap for cap in status_body["capabilities"] if cap["name"] == "spb") - - assert failed.exit_code == 1 - assert status.exit_code == 3 - assert spb["state"] == "degraded" - assert residual in spb["residuals"] - - monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", real_replace) - retry = invoke(["apply", "spb", "--json"], input_text=json.dumps(payload)) - retry_body = output_json(retry) - retry_spb = next(cap for cap in retry_body["capabilities"] if cap["name"] == "spb") - - assert retry.exit_code == 0 - assert retry_spb["state"] == "ready" diff --git a/tests/sandbox_profile/test_cli_registration.py b/tests/sandbox_profile/test_cli_registration.py deleted file mode 100644 index 5fd7658ad..000000000 --- a/tests/sandbox_profile/test_cli_registration.py +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -from solstone.think import sol_cli -from solstone.think.generated.access_rejections import JOURNAL_ACCESS_ONLY_COMMANDS -from solstone.think.sandbox_profile import cli -from tests.sandbox_profile import invoke, output_json, sandbox_journal - - -def test_sandbox_profile_registered_as_service_command() -> None: - command = sol_cli.COMMANDS["sandbox-profile"] - - assert command.module == "solstone.think.sandbox_profile.cli" - assert command.surface == "service" - assert "sandbox-profile" in sol_cli.service_help_group().commands - - -def test_sandbox_profile_module_exposes_main() -> None: - assert callable(cli.main) - - -def test_sandbox_profile_not_a_journal_access_rejection() -> None: - assert "sandbox-profile" not in JOURNAL_ACCESS_ONLY_COMMANDS - - -def test_apply_runtime_refuses_with_supported_capability_list( - tmp_path, - monkeypatch, -) -> None: - sandbox_journal(tmp_path, monkeypatch) - - result = invoke(["apply", "runtime", "--json"], input_text="{}") - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "unsupported_capability_action" - assert "scout, spl, spb, spp" in body["next_actions"][0] - - -def test_apply_unknown_capability_refuses(tmp_path, monkeypatch) -> None: - sandbox_journal(tmp_path, monkeypatch) - - result = invoke(["apply", "missing", "--json"], input_text="{}") - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "unknown_capability" - - -def test_describe_is_marker_free_preflight(tmp_path, monkeypatch) -> None: - journal = tmp_path / "unmarked" - journal.mkdir() - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) - - result = invoke(["describe", "--json"]) - body = output_json(result) - - assert result.exit_code == 0 - assert body["run_id"] is None - assert body["state"] == "ok" - assert not (journal / ".solstone-sandbox.json").exists() diff --git a/tests/sandbox_profile/test_disable.py b/tests/sandbox_profile/test_disable.py deleted file mode 100644 index 476745da5..000000000 --- a/tests/sandbox_profile/test_disable.py +++ /dev/null @@ -1,132 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -from solstone.think.services.scout import DisableOutcome -from tests.sandbox_profile import ( - invoke, - output_json, - prepare_ok, - read_json, - sandbox_journal, - scout_payload, - spb_payload, -) - - -def test_disable_is_idempotent_and_leaves_runtime_state(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - first = invoke(["disable", "--json"]) - second = invoke(["disable", "--json"]) - - assert first.exit_code == 0 - assert second.exit_code == 0 - assert (journal / "link" / "state.json").exists() - assert (journal / "link" / "ca" / "cert.pem").exists() - assert "production-side reconciler" in output_json(first)["next_actions"][0] - - -def test_disable_missing_applied_spb_binding_reports_cleanup_failed( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - apply = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - assert apply.exit_code == 0 - (journal / "backup" / "hosted" / "binding.json").unlink() - - result = invoke(["disable", "--json"]) - body = output_json(result) - spb = next(cap for cap in body["capabilities"] if cap["name"] == "spb") - - assert result.exit_code == 3 - assert body["state"] == "cleanup_failed" - assert spb["state"] == "cleanup_failed" - assert "spb_binding_missing" in spb["residuals"] - assert "production-side reconciler" in body["next_actions"][0] - - -def test_clean_disable_after_spb_restore_converges_twice(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - apply = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - assert apply.exit_code == 0 - binding = read_json(journal / "backup" / "hosted" / "binding.json") - (journal / "backup" / "hosted" / "binding.json").unlink() - failed = invoke(["disable", "--json"]) - assert failed.exit_code == 3 - (journal / "backup" / "hosted" / "binding.json").write_text( - json.dumps(binding, indent=2) + "\n", - encoding="utf-8", - ) - - clean = invoke(["disable", "--json"]) - again = invoke(["disable", "--json"]) - - assert clean.exit_code == 0 - assert again.exit_code == 0 - assert not (journal / "backup" / "hosted" / "binding.json").exists() - - -def test_disable_still_applied_capability_is_cleanup_failed( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - apply = invoke(["apply", "scout", "--json"], input_text=json.dumps(scout_payload())) - assert apply.exit_code == 0 - monkeypatch.setattr( - "solstone.think.services.scout.disable_scout", - lambda: DisableOutcome(was_enabled=False, env_key_preserved=False), - ) - - result = invoke(["disable", "scout", "--json"]) - body = output_json(result) - scout = next(cap for cap in body["capabilities"] if cap["name"] == "scout") - - assert result.exit_code == 3 - assert body["state"] == "cleanup_failed" - assert scout["state"] == "cleanup_failed" - assert "cleanup_still_applied" in scout["residuals"] - - -def test_disable_logs_and_classifies_owner_io_failures( - tmp_path, - monkeypatch, - caplog, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - apply = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - assert apply.exit_code == 0 - - def fail_clear_backup_config() -> None: - raise OSError("forced local artifact failure") - - monkeypatch.setattr( - "solstone.think.backup.state.clear_backup_config", - fail_clear_backup_config, - ) - - result = invoke(["disable", "spb", "--json"]) - body = output_json(result) - spb = next(cap for cap in body["capabilities"] if cap["name"] == "spb") - - assert result.exit_code == 3 - assert "local_artifact_io_failed" in spb["residuals"] - assert "capability=spb" in caplog.text - assert "exception_type=OSError" in caplog.text - assert "broker-token" not in caplog.text diff --git a/tests/sandbox_profile/test_envelope.py b/tests/sandbox_profile/test_envelope.py deleted file mode 100644 index 799a8072f..000000000 --- a/tests/sandbox_profile/test_envelope.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -import pytest - -from solstone.think.sandbox_profile import envelope, manifest -from tests.sandbox_profile import invoke, output_json, prepare_ok, sandbox_journal - - -def test_envelope_field_order_types_and_default_json_output( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - - result = invoke(["describe", "--profile", "full", "--contract-version", "1"]) - raw = json.loads(result.output) - - assert result.exit_code == 0 - assert list(raw) == [ - "contract_version", - "action", - "profile", - "run_id", - "state", - "capabilities", - "next_actions", - "error", - ] - assert raw["contract_version"] == 1 - assert raw["action"] == "describe" - assert raw["profile"] == "full" - assert raw["run_id"] is None - assert [cap["name"] for cap in raw["capabilities"]] == [ - "scout", - "spl", - "spb", - "spp", - "runtime", - ] - assert raw["error"] is None - assert not (journal / "config" / "journal.json").exists() - - -def test_exit_mapping_distinguishes_internal_failure_from_known_outcomes() -> None: - caps = envelope.empty_capabilities() - assert envelope.Envelope("status", "full", None, "ok", caps).exit_code == 0 - assert envelope.Envelope("status", "full", None, "degraded", caps).exit_code == 3 - assert ( - envelope.Envelope("status", "full", None, "cleanup_failed", caps).exit_code == 3 - ) - assert ( - envelope.error_envelope( - action="status", - code="payload_invalid", - message="invalid", - run_id=None, - ).exit_code - == 2 - ) - assert ( - envelope.error_envelope( - action="status", - code="internal_error", - message="failed", - run_id=None, - ).exit_code - == 1 - ) - - -def test_capability_serializer_enforces_closed_vocabulary() -> None: - with pytest.raises(ValueError, match="unsupported capability name"): - envelope.CapabilityEnvelope("unknown", envelope.CAP_READY).to_json() - with pytest.raises(ValueError, match="unsupported residual code"): - envelope.CapabilityEnvelope( - manifest.CAPABILITY_SCOUT, - envelope.CAP_DEGRADED, - ("typo_residual",), - ).to_json() - - -def test_cli_exit_codes_cover_ok_degraded_error_and_cleanup_failed( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - ok = invoke(["status", "--json"]) - prepare_ok(journal) - error = invoke(["apply", "runtime", "--json"], input_text="{}") - intent_path = journal / "health" / "sandbox-profile" / "intent.json" - payload = json.loads(intent_path.read_text("utf-8")) - for cap in payload["capabilities"]: - if cap["name"] == "spb": - cap["intent_state"] = "applied" - intent_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - degraded = invoke(["status", "--json"]) - cleanup = invoke(["disable", "spb", "--json"]) - - assert ok.exit_code == 0 - assert degraded.exit_code == 3 - assert error.exit_code == 2 - assert cleanup.exit_code == 3 - assert output_json(cleanup)["state"] == "cleanup_failed" diff --git a/tests/sandbox_profile/test_marker.py b/tests/sandbox_profile/test_marker.py deleted file mode 100644 index a66fd8eb8..000000000 --- a/tests/sandbox_profile/test_marker.py +++ /dev/null @@ -1,177 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import os -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import intent, manifest -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ( - OTHER_RUN_ID, - RUN_ID, - invoke, - output_json, - sandbox_journal, - write_marker, -) - -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def _assert_refusal_without_repo_writes(monkeypatch, journal: Path, code: str) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal.resolve())) - before = repository_inventory(REPO_ROOT) - result = invoke(["prepare", "--json"]) - after = repository_inventory(REPO_ROOT) - - assert_inventory_unchanged(before, after) - assert result.exit_code == 2 - payload = output_json(result) - assert payload["state"] == "error" - assert payload["run_id"] is None - assert payload["error"]["code"] == code - - -def test_missing_marker_refuses_before_mkdir_or_repo_write( - tmp_path, monkeypatch -) -> None: - journal = tmp_path / "missing" / "journal" - _assert_refusal_without_repo_writes(monkeypatch, journal, "sandbox_marker_missing") - assert not journal.exists() - - -def test_marker_refusal_matrix_zero_side_effects(tmp_path, monkeypatch) -> None: - cases = [ - ("symlink", "sandbox_marker_symlink"), - ("not_regular", "sandbox_marker_not_regular"), - ("unparseable", "sandbox_marker_unparseable"), - ("non_object", "sandbox_marker_non_object"), - ("wrong_kind", "sandbox_marker_wrong_kind"), - ("wrong_contract", "sandbox_marker_wrong_contract_version"), - ("wrong_profile", "sandbox_marker_wrong_profile"), - ("bad_run_id", "sandbox_marker_bad_run_id"), - ("path_mismatch", "sandbox_marker_path_mismatch"), - ] - for name, code in cases: - journal = tmp_path / name - journal.mkdir() - marker_path = journal / ".solstone-sandbox.json" - payload = { - "kind": manifest.MARKER_KIND, - "contract_version": manifest.CONTRACT_VERSION, - "profile": manifest.PROFILE, - "run_id": RUN_ID, - "journal_path": str(journal.resolve()), - } - if name == "symlink": - target = journal / "target.json" - target.write_text("{}", encoding="utf-8") - marker_path.symlink_to(target) - elif name == "not_regular": - marker_path.mkdir() - elif name == "unparseable": - marker_path.write_text('{"kind": "x"} trailing', encoding="utf-8") - elif name == "non_object": - marker_path.write_text("[]\n", encoding="utf-8") - else: - if name == "wrong_kind": - payload["kind"] = "wrong" - elif name == "wrong_contract": - payload["contract_version"] = 99 - elif name == "wrong_profile": - payload["profile"] = "other" - elif name == "bad_run_id": - payload["run_id"] = RUN_ID.upper() - elif name == "path_mismatch": - payload["journal_path"] = str((tmp_path / "other").resolve()) - marker_path.write_text(json.dumps(payload), encoding="utf-8") - - _assert_refusal_without_repo_writes(monkeypatch, journal, code) - - -def test_duplicate_key_marker_is_unparseable(tmp_path, monkeypatch) -> None: - journal = tmp_path / "journal" - journal.mkdir() - (journal / ".solstone-sandbox.json").write_text( - ( - '{"kind":"solstone-disposable-journal","kind":"x",' - '"contract_version":1,"profile":"full",' - f'"run_id":"{RUN_ID}","journal_path":"{journal.resolve()}"}}' - ), - encoding="utf-8", - ) - _assert_refusal_without_repo_writes( - monkeypatch, journal, "sandbox_marker_unparseable" - ) - - -def test_valid_marker_with_different_intent_run_is_intent_conflict( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - intent.ensure_prepared(journal, OTHER_RUN_ID) - - result = invoke(["prepare", "--json"]) - payload = output_json(result) - - assert result.exit_code == 2 - assert payload["run_id"] == RUN_ID - assert payload["error"]["code"] == "intent_run_mismatch" - assert "owning run" in payload["next_actions"][0] - - -def test_describe_rejects_unsupported_options_before_marker_validation( - tmp_path, - monkeypatch, -) -> None: - journal = tmp_path / "uncreated" - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) - - result = invoke(["describe", "--profile", "other", "--json"]) - payload = output_json(result) - - assert result.exit_code == 2 - assert payload["error"]["code"] == "sandbox_marker_wrong_profile" - assert not journal.exists() - - -@pytest.mark.parametrize("command", ["apply", "disable"]) -def test_mutating_commands_refuse_missing_marker_with_null_run_id( - tmp_path, - monkeypatch, - command: str, -) -> None: - journal = tmp_path / command / "journal" - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) - args = [command, "scout", "--json"] if command == "apply" else [command, "--json"] - result = invoke(args, input_text="{}") - payload = output_json(result) - - assert result.exit_code == 2 - assert payload["run_id"] is None - assert payload["error"]["code"] == "sandbox_marker_missing" - assert not journal.exists() - - -def test_marker_validation_does_not_inject_config_env(tmp_path, monkeypatch) -> None: - journal = tmp_path / "journal" - write_marker(journal) - config_path = journal / "config" / "journal.json" - config_path.parent.mkdir() - config_path.write_text( - json.dumps({"env": {"GOOGLE_API_KEY": "should-not-enter-env"}}), - encoding="utf-8", - ) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal.resolve())) - monkeypatch.delenv("GOOGLE_API_KEY", raising=False) - - result = invoke(["status", "--json"]) - - assert result.exit_code == 0 - assert os.environ.get("GOOGLE_API_KEY") is None diff --git a/tests/sandbox_profile/test_payloads.py b/tests/sandbox_profile/test_payloads.py deleted file mode 100644 index 6e40e01ff..000000000 --- a/tests/sandbox_profile/test_payloads.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -import pytest - -from tests.sandbox_profile import ( - invoke, - output_json, - prepare_ok, - sandbox_journal, - scout_payload, - spb_payload, - spl_payload, - spp_payload, -) - - -@pytest.mark.parametrize( - ("capability", "payload"), - [ - ("scout", {**scout_payload(), "extra": "nope"}), - ("spl", {**spl_payload(), "service": "wrong"}), - ("spp", spp_payload(endpoint_url="not-a-url")), - ], -) -def test_payload_matrix_rejects_invalid_payloads_before_apply( - tmp_path, - monkeypatch, - capability: str, - payload: dict[str, object], -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - result = invoke(["apply", capability, "--json"], input_text=json.dumps(payload)) - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "payload_invalid" - intent_payload = json.loads( - (journal / "health" / "sandbox-profile" / "intent.json").read_text("utf-8") - ) - cap = next( - item for item in intent_payload["capabilities"] if item["name"] == capability - ) - assert cap["intent_state"] == "prepared" - - -def test_spb_rejects_instance_id_mismatch_before_local_mutation( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - payload = spb_payload(journal, instance_id="not-the-runtime-instance") - - result = invoke(["apply", "spb", "--json"], input_text=json.dumps(payload)) - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "spb_instance_mismatch" - assert "instance_id" in body["error"]["message"] - assert "not-the-runtime-instance" not in result.output - assert not (journal / "backup" / "hosted" / "binding.json").exists() - - -@pytest.mark.parametrize("payload_text", ["[]", '{"a": 1} trailing', '{"a":1,"a":2}']) -def test_apply_rejects_non_object_trailing_and_duplicate_key_payloads( - tmp_path, - monkeypatch, - payload_text: str, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - result = invoke(["apply", "scout", "--json"], input_text=payload_text) - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "payload_invalid" - - -def test_apply_rejects_oversized_payload(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - result = invoke(["apply", "scout", "--json"], input_text="x" * (64 * 1024 + 1)) - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "payload_invalid" - - -def test_apply_runtime_and_unknown_capability_name_supported_apply_list( - tmp_path, - monkeypatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - runtime = invoke(["apply", "runtime", "--json"], input_text="{}") - unknown = invoke(["apply", "missing-cap", "--json"], input_text="{}") - - assert runtime.exit_code == 2 - assert output_json(runtime)["error"]["code"] == "unsupported_capability_action" - assert "scout, spl, spb, spp" in output_json(runtime)["next_actions"][0] - assert unknown.exit_code == 2 - assert output_json(unknown)["error"]["code"] == "unknown_capability" - - -def test_spl_apply_uses_consuming_module_enroll_home(monkeypatch, tmp_path) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - calls = [] - - def fake_enroll_home(*args, **kwargs): - calls.append((args, kwargs)) - return "service-token" - - monkeypatch.setattr("solstone.think.services.spl.enroll_home", fake_enroll_home) - - result = invoke(["apply", "spl", "--json"], input_text=json.dumps(spl_payload())) - body = output_json(result) - - assert result.exit_code == 0 - assert calls - assert ( - next(cap for cap in body["capabilities"] if cap["name"] == "spl")["state"] - == "ready" - ) diff --git a/tests/sandbox_profile/test_prepare.py b/tests/sandbox_profile/test_prepare.py deleted file mode 100644 index a8705ded9..000000000 --- a/tests/sandbox_profile/test_prepare.py +++ /dev/null @@ -1,80 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -from solstone.think.sandbox_profile import manifest -from tests.sandbox_profile import invoke, output_json, read_json, sandbox_journal - - -def test_prepare_is_idempotent_and_synthetic_identity_converges( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - - first = invoke(["prepare", "--json"]) - second = invoke(["prepare", "--json"]) - first_body = output_json(first) - second_body = output_json(second) - config = read_json(journal / "config" / "journal.json") - owner = manifest.synthetic_owner_metadata(first_body["run_id"]) - - assert first.exit_code == 0 - assert second.exit_code == 0 - assert first_body == second_body - assert config["setup"]["completed_at"] == owner.setup_completed_at - assert config["identity"]["name"] == owner.identity_name - assert config["identity"]["preferred"] == owner.identity_preferred - assert config["identity"]["timezone"] == owner.identity_timezone - assert config["journal"]["name"] == owner.journal_name - assert read_json(journal / "link" / "state.json")["home_label"] == owner.home_label - assert (journal / "link" / "ca" / "cert.pem").exists() - assert (journal / "link" / "ca" / "private.pem").exists() - - -def test_status_read_verb_does_not_materialize_missing_config( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - - result = invoke(["status", "--json"]) - body = output_json(result) - - assert result.exit_code == 0 - assert body["state"] == "ok" - assert not (journal / "config" / "journal.json").exists() - assert not (journal / "health" / "sandbox-profile" / "intent.json").exists() - - -def test_prepare_refuses_malformed_same_run_intent_before_side_effects( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - intent_path = journal / "health" / "sandbox-profile" / "intent.json" - intent_path.parent.mkdir(parents=True) - intent_path.write_text( - json.dumps( - { - "kind": "wrong", - "contract_version": manifest.CONTRACT_VERSION, - "run_id": "86d9eb6c-d64e-4ae5-b29e-524ddf57a013", - "profile": manifest.PROFILE, - }, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - before = intent_path.read_text("utf-8") - - result = invoke(["prepare", "--json"]) - body = output_json(result) - - assert result.exit_code == 2 - assert body["error"]["code"] == "intent_malformed" - assert body["run_id"] == "86d9eb6c-d64e-4ae5-b29e-524ddf57a013" - assert intent_path.read_text("utf-8") == before - assert not (journal / "config" / "journal.json").exists() - assert not (journal / "link").exists() diff --git a/tests/sandbox_profile/test_probe_bounds.py b/tests/sandbox_profile/test_probe_bounds.py deleted file mode 100644 index f63b131a0..000000000 --- a/tests/sandbox_profile/test_probe_bounds.py +++ /dev/null @@ -1,73 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from solstone.think.sandbox_profile.probe_replay import replay_probe_ledger -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ( - complete_attempt_records, - write_attempt_dir, - write_ledger, -) - - -def _attempt_id(index: int) -> str: - return f"00000000-0000-4000-8000-{index:012d}" - - -def test_byte_size_bound_precedes_read_and_malformed_content(tmp_path) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True) - path.write_bytes(b"{" + (b"x" * probe_contract.MAX_LEDGER_BYTES)) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED - - -def test_malformed_framing_precedes_attempt_count_bound(tmp_path) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True) - start = { - "attempt_id": _attempt_id(0), - "type": probe_contract.RECORD_TYPE_ATTEMPT_STARTED, - } - lines = [ - json.dumps({**start, "attempt_id": _attempt_id(index)}) - for index in range(probe_contract.MAX_ATTEMPTS) - ] - path.write_text("\n".join(lines) + "\n{", encoding="utf-8") - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - - -def test_attempt_count_bound_precedes_semantic_validation(tmp_path) -> None: - journal = tmp_path / "journal" - records: list[dict[str, object]] = [] - for index in range(probe_contract.MAX_ATTEMPTS): - attempt_id = _attempt_id(index) - write_attempt_dir(journal, attempt_id) - records.extend(complete_attempt_records(attempt_id=attempt_id)) - records[0]["unknown"] = True - write_ledger(journal, records) - - before = repository_inventory(Path.cwd()) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - after = repository_inventory(Path.cwd()) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED - assert_inventory_unchanged(before, after) diff --git a/tests/sandbox_profile/test_probe_cardinality.py b/tests/sandbox_profile/test_probe_cardinality.py deleted file mode 100644 index d47897e7e..000000000 --- a/tests/sandbox_profile/test_probe_cardinality.py +++ /dev/null @@ -1,137 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from solstone.think.sandbox_profile.probe_replay import replay_probe_ledger -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot -from solstone.think.sandbox_profile.probe_writer import begin_probe_attempt -from tests.sandbox_profile import ( - ATTEMPT_ID, - FIXED_TS, - OTHER_ATTEMPT_ID, - RUN_ID, - THIRD_ATTEMPT_ID, - complete_attempt_records, - start_record, - write_attempt_dir, - write_ledger, -) - - -def _assert_stale(journal) -> None: - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - - -def test_exact_start_proofs_terminal_cardinality_passes(tmp_path) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - write_attempt_dir(journal) - write_ledger(journal, complete_attempt_records(selected=selected)) - - replay = replay_probe_ledger(journal) - - assert replay.attempt_count == 1 - assert [proof.proof for proof in replay.attempts[0].proofs] == list(selected) - - -def test_missing_proof_terminal_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - records = complete_attempt_records(selected=selected) - write_attempt_dir(journal) - write_ledger(journal, [records[0], records[1], records[-1]]) - - _assert_stale(journal) - - -def test_wrong_proof_order_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - records = complete_attempt_records(selected=selected) - write_attempt_dir(journal) - write_ledger(journal, [records[0], records[2], records[1], records[-1]]) - - _assert_stale(journal) - - -def test_replay_uses_contract_attempt_sequence_order(monkeypatch, tmp_path) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - write_attempt_dir(journal) - write_ledger(journal, complete_attempt_records(selected=selected)) - sequence = tuple(probe_contract.RECORD_CARDINALITY["attempt_sequence"]) - reordered = (sequence[0], sequence[2], sequence[1]) - monkeypatch.setattr( - probe_contract, - "RECORD_CARDINALITY", - { - **probe_contract.RECORD_CARDINALITY, - "attempt_sequence": reordered, - }, - ) - - _assert_stale(journal) - - -def test_duplicate_attempt_id_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - first = complete_attempt_records() - second = complete_attempt_records() - write_attempt_dir(journal, ATTEMPT_ID) - write_ledger(journal, first + second) - - _assert_stale(journal) - - -def test_record_after_nonretry_terminal_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - cancelled = complete_attempt_records( - proof_overrides={ - proof: { - "state": probe_contract.PROOF_STATE_FAILED, - "reason": probe_contract.REASON_CANCELLED, - } - } - ) - next_start = start_record(attempt_id=OTHER_ATTEMPT_ID) - write_attempt_dir(journal, ATTEMPT_ID) - write_attempt_dir(journal, OTHER_ATTEMPT_ID) - write_ledger(journal, cancelled + [next_start]) - - _assert_stale(journal) - - -def test_writer_enforces_contact_and_proof_order(tmp_path) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=THIRD_ATTEMPT_ID, - started_at=FIXED_TS, - ) - writer.dispatch_contact(selected[0], lambda: None) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - writer.dispatch_contact(selected[1], lambda: None) - assert excinfo.value.code == probe_contract.STABLE_ERROR_INTERNAL_ERROR - with pytest.raises(probe_records.ProbeOperationError): - writer.write_attempt_terminal() - - with pytest.raises(probe_records.ProbeOperationError): - writer.write_proof_terminal( - proof=selected[1], - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[selected[1]], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) diff --git a/tests/sandbox_profile/test_probe_concurrency.py b/tests/sandbox_profile/test_probe_concurrency.py deleted file mode 100644 index 0acd507a0..000000000 --- a/tests/sandbox_profile/test_probe_concurrency.py +++ /dev/null @@ -1,312 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import os -import signal -import subprocess -import sys -import threading -import time -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import ( - probe_contract, - probe_durability, - probe_records, -) -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot -from solstone.think.sandbox_profile.probe_writer import begin_probe_attempt -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ATTEMPT_ID, FIXED_TS, RUN_ID - - -def _proof() -> str: - return probe_contract.CAPABILITY_ORDER[0] - - -def _wait_for_file(path: Path, *, timeout: float = 5.0) -> None: - deadline = time.monotonic() + timeout - while not path.exists() and time.monotonic() < deadline: - time.sleep(0.02) - assert path.exists() - - -def _holder_env() -> dict[str, str]: - env = os.environ.copy() - env.setdefault("PYTHONUNBUFFERED", "1") - return env - - -def _spawn_slot_holder(journal: Path, ready: Path, release: Path) -> subprocess.Popen: - holder_code = f""" -import pathlib -import time -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot - -slot = acquire_probe_slot(pathlib.Path({str(journal)!r}), run_id={RUN_ID!r}) -pathlib.Path({str(ready)!r}).write_text("ready", encoding="utf-8") -while not pathlib.Path({str(release)!r}).exists(): - time.sleep(0.02) -slot.release() -""" - return subprocess.Popen( - [sys.executable, "-c", holder_code], - cwd=Path.cwd(), - env=_holder_env(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - -def _release_holder(holder: subprocess.Popen, release: Path) -> None: - release.write_text("release", encoding="utf-8") - stdout, stderr = holder.communicate(timeout=10) - assert holder.returncode == 0, (stdout, stderr) - - -def _kill_holder(holder: subprocess.Popen) -> None: - if holder.poll() is None: - holder.kill() - holder.wait(timeout=5) - - -def test_live_subprocess_owner_blocks_second_probe_slot(tmp_path) -> None: - journal = tmp_path / "journal" - ready = tmp_path / "ready" - release = tmp_path / "release" - holder = _spawn_slot_holder(journal, ready, release) - try: - _wait_for_file(ready) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_PROBE_ACTIVE - _release_holder(holder, release) - finally: - _kill_holder(holder) - - -def test_sigkill_releases_probe_slot_without_dead_owner_cleanup(tmp_path) -> None: - journal = tmp_path / "journal" - ready = tmp_path / "ready" - release = tmp_path / "release" - holder = _spawn_slot_holder(journal, ready, release) - try: - _wait_for_file(ready) - - os.kill(holder.pid, signal.SIGKILL) - stdout, stderr = holder.communicate(timeout=10) - assert holder.returncode == -signal.SIGKILL, (stdout, stderr) - - slot = acquire_probe_slot(journal, run_id=RUN_ID) - try: - assert slot.owned is True - finally: - slot.release() - finally: - _kill_holder(holder) - - -def test_release_and_reacquire_wait_for_blocked_append( - monkeypatch, - tmp_path, -) -> None: - journal = tmp_path / "journal" - proof = _proof() - entered_write = threading.Event() - finish_write = threading.Event() - release_started = threading.Event() - release_finished = threading.Event() - reacquired = threading.Event() - original_write = probe_durability._write_once - - def blocked_write(fd: int, data: bytes) -> int: - entered_write.set() - assert finish_write.wait(timeout=2) - return original_write(fd, data) - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - writer.dispatch_contact(proof, lambda: None) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=(), - reason=probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - duration_ms=1, - finished_at=FIXED_TS, - ) - monkeypatch.setattr(probe_durability, "_write_once", blocked_write) - - def write_terminal() -> None: - writer.write_attempt_terminal(finished_at=FIXED_TS) - - def release_then_reacquire() -> None: - release_started.set() - slot.release() - release_finished.set() - fresh = acquire_probe_slot(journal, run_id=RUN_ID) - try: - reacquired.set() - finally: - fresh.release() - - with ThreadPoolExecutor(max_workers=2) as executor: - terminal_future = executor.submit(write_terminal) - owner_future = None - try: - assert entered_write.wait(timeout=2) - - owner_future = executor.submit(release_then_reacquire) - assert release_started.wait(timeout=2) - time.sleep(0.05) - assert not release_finished.is_set() - assert not reacquired.is_set() - finally: - finish_write.set() - terminal_future.result(timeout=2) - if owner_future is not None: - owner_future.result(timeout=2) - - assert release_finished.is_set() - assert reacquired.is_set() - - -def test_old_lock_owner_detects_replaced_lock_after_new_owner_acquires( - tmp_path, -) -> None: - journal = tmp_path / "journal" - proof = _proof() - old_slot = acquire_probe_slot(journal, run_id=RUN_ID) - lock_path = probe_contract.probe_lock_path(journal) - lock_path.unlink() - lock_path.write_bytes(b"") - new_slot = acquire_probe_slot(journal, run_id=RUN_ID) - try: - before = repository_inventory(journal) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - begin_probe_attempt( - old_slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - after = repository_inventory(journal) - finally: - new_slot.release() - old_slot.release() - - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - assert_inventory_unchanged(before, after) - - -def test_release_and_cancellation_wait_for_in_flight_contact(tmp_path) -> None: - proof = _proof() - - release_journal = tmp_path / "release" - contact_entered = threading.Event() - finish_contact = threading.Event() - release_started = threading.Event() - release_finished = threading.Event() - with acquire_probe_slot(release_journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - - def contact_operation() -> None: - contact_entered.set() - assert finish_contact.wait(timeout=2) - - contact_thread = threading.Thread( - target=lambda: writer.dispatch_contact(proof, contact_operation) - ) - - def release_slot() -> None: - release_started.set() - slot.release() - release_finished.set() - - release_thread = threading.Thread(target=release_slot) - contact_thread.start() - release_thread_started = False - try: - assert contact_entered.wait(timeout=2) - release_thread.start() - release_thread_started = True - assert release_started.wait(timeout=2) - time.sleep(0.05) - assert not release_finished.is_set() - finally: - finish_contact.set() - contact_thread.join(timeout=2) - if release_thread_started: - release_thread.join(timeout=2) - - assert not contact_thread.is_alive() - assert not release_thread.is_alive() - assert release_finished.is_set() - - cancel_journal = tmp_path / "cancel" - selected = probe_contract.CAPABILITY_ORDER[:2] - contact_entered.clear() - finish_contact.clear() - cancel_started = threading.Event() - cancel_finished = threading.Event() - with acquire_probe_slot(cancel_journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - contact_thread = threading.Thread( - target=lambda: writer.dispatch_contact(selected[0], contact_operation) - ) - - def cancel_attempt() -> None: - cancel_started.set() - writer.write_cancelled_attempt( - proof=selected[0], - state=probe_contract.PROOF_STATE_FAILED, - checks=probe_contract.PROOF_CHECKS[selected[0]][:1], - duration_ms=1, - finished_at=FIXED_TS, - ) - cancel_finished.set() - - contact_thread.start() - with ThreadPoolExecutor(max_workers=1) as executor: - cancel_future = None - try: - assert contact_entered.wait(timeout=2) - cancel_future = executor.submit(cancel_attempt) - assert cancel_started.wait(timeout=2) - time.sleep(0.05) - assert not cancel_finished.is_set() - finally: - finish_contact.set() - contact_thread.join(timeout=2) - if cancel_future is not None: - cancel_future.result(timeout=2) - - assert not contact_thread.is_alive() - assert cancel_finished.is_set() diff --git a/tests/sandbox_profile/test_probe_contract.py b/tests/sandbox_profile/test_probe_contract.py deleted file mode 100644 index 84125d5a5..000000000 --- a/tests/sandbox_profile/test_probe_contract.py +++ /dev/null @@ -1,196 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import importlib.util -import json -from pathlib import Path - -from solstone.think.sandbox_profile import manifest, probe_contract, probe_records - -REPO_ROOT = Path(__file__).resolve().parents[2] -SCRIPT = REPO_ROOT / "scripts" / "build_sandbox_probe_contract.py" - - -def _load_builder(): - spec = importlib.util.spec_from_file_location( - "build_sandbox_probe_contract", SCRIPT - ) - assert spec and spec.loader - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_probe_v1_vocabulary_is_pinned() -> None: - assert probe_contract.PROOF_CHECKS == { - "scout": ( - "scout.response_schema", - "scout.nonce_match", - "scout.finish", - "scout.usage", - ), - "spl": ( - "spl.enrollment", - "spl.relay_dial", - "spl.inner_tls", - "spl.observer_registered", - "spl.segment_transferred", - "spl.segment_landed", - "spl.authorization_removed", - ), - "spb": ( - "spb.repository_initialized", - "spb.snapshot_created", - "spb.snapshot_confirmed", - "spb.restore_match", - "spb.local_cleanup", - ), - "spp": ( - "spp.attestation_session", - "spp.text_nonce", - "spp.text_usage", - "spp.transcript_expected", - ), - "runtime": ( - "runtime.supervisor", - "runtime.callosum", - "runtime.listener", - "runtime.sense", - "runtime.task_queue", - "runtime.cortex", - "runtime.talent_output", - "runtime.talent_usage", - "runtime.cadence_contract", - "runtime.cadence_dry_run", - ), - } - assert probe_contract.PROOF_SPECIFIC_REASONS == { - "scout": ( - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - "usage_invalid", - ), - "spl": ( - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - ), - "spb": ( - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - ), - "spp": ( - "attestation_unverified", - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "remote_rejected", - "response_invalid", - "usage_invalid", - ), - "runtime": ( - "cadence_contract_mismatch", - "capability_not_ready", - "content_mismatch", - "deadline_exceeded", - "response_invalid", - "runtime_unavailable", - "usage_invalid", - ), - } - - -def test_probe_contract_agrees_with_manifest_today() -> None: - assert probe_contract.CONTRACT_VERSION == manifest.CONTRACT_VERSION - assert probe_contract.CAPABILITY_SCOUT == manifest.CAPABILITY_SCOUT - assert probe_contract.CAPABILITY_SPL == manifest.CAPABILITY_SPL - assert probe_contract.CAPABILITY_SPB == manifest.CAPABILITY_SPB - assert probe_contract.CAPABILITY_SPP == manifest.CAPABILITY_SPP - assert probe_contract.CAPABILITY_RUNTIME == manifest.CAPABILITY_RUNTIME - assert probe_contract.CAPABILITY_ORDER == manifest.CAPABILITY_ORDER - - -def test_probe_contract_ignores_manifest_monkeypatch(monkeypatch) -> None: - before = probe_contract.contract_payload() - - monkeypatch.setattr(manifest, "CONTRACT_VERSION", 999) - monkeypatch.setattr(manifest, "CAPABILITY_SCOUT", "changed-scout") - monkeypatch.setattr( - manifest, - "CAPABILITY_ORDER", - ("changed-scout", "spl", "spb", "spp", "runtime"), - ) - - assert probe_contract.contract_payload() == before - - -def test_contract_payload_top_level_keys_are_pinned() -> None: - assert set(probe_contract.contract_payload()) == { - "attempt_terminal_reasons", - "attempt_terminal_states", - "cancellation", - "capability_order", - "cleanup_resolution", - "cleanup_states", - "common_reasons", - "contract_version", - "limits", - "not_run_reasons", - "predicate_keys", - "proof_reason_pool", - "proof_terminal_rules", - "proof_terminal_states", - "proofs", - "record_cardinality", - "record_fields", - "record_types", - "retry_eligible_terminals", - "stable_errors", - "terminal_derivation", - } - - -def test_probe_predicate_registry_matches_contract() -> None: - assert tuple(probe_records.PREDICATE_REGISTRY) == probe_contract.PREDICATE_KEYS - - -def test_contract_artifact_matches_constants() -> None: - artifact = ( - REPO_ROOT / "solstone" / "think" / "sandbox_profile" / "probe_contract_v1.json" - ) - actual = json.loads(artifact.read_text(encoding="utf-8")) - assert actual == probe_contract.contract_payload() - - -def test_builder_check_is_bidirectional(monkeypatch, tmp_path, capsys) -> None: - builder = _load_builder() - artifact = ( - tmp_path / "solstone" / "think" / "sandbox_profile" / "probe_contract_v1.json" - ) - monkeypatch.setattr(builder, "ROOT", tmp_path) - monkeypatch.setattr(builder, "ARTIFACT_PATH", artifact) - - builder.write_outputs() - assert builder.check_outputs() == 0 - - artifact.write_text( - artifact.read_text(encoding="utf-8").replace("{", "[", 1), - encoding="utf-8", - ) - assert builder.check_outputs() == 1 - captured = capsys.readouterr() - assert ( - "Sandbox probe contract is stale: " - "solstone/think/sandbox_profile/probe_contract_v1.json. " - "Run: make sandbox-probe-contract" - ) in captured.err diff --git a/tests/sandbox_profile/test_probe_durability.py b/tests/sandbox_profile/test_probe_durability.py deleted file mode 100644 index 239a906fd..000000000 --- a/tests/sandbox_profile/test_probe_durability.py +++ /dev/null @@ -1,254 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import os - -import pytest - -from solstone.think.sandbox_profile import ( - probe_contract, - probe_durability, - probe_records, -) -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot -from solstone.think.sandbox_profile.probe_writer import begin_probe_attempt -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ( - ATTEMPT_ID, - FIXED_TS, - OTHER_ATTEMPT_ID, - RUN_ID, - start_record, -) - - -def test_append_order_is_write_file_fsync_then_directory_fsync(monkeypatch, tmp_path): - events: list[str] = [] - monkeypatch.setattr( - probe_durability, - "_write_once", - lambda _handle, data: events.append("write") or len(data), - ) - monkeypatch.setattr( - probe_durability, - "_fsync_file", - lambda _handle: events.append("file_fsync"), - ) - monkeypatch.setattr( - probe_durability, - "_fsync_directory", - lambda _path: events.append("dir_fsync"), - ) - - fd = os.open(os.devnull, os.O_WRONLY) - try: - data = probe_durability.encode_jsonl_record(start_record()) - probe_durability.append_jsonl_strict(fd, tmp_path, data) - finally: - os.close(fd) - - assert events == ["write", "file_fsync", "dir_fsync"] - - -@pytest.mark.parametrize( - "seam", - [ - "_write_once", - "_fsync_file", - "_fsync_directory", - ], -) -def test_append_faults_surface_record_write_failed(monkeypatch, tmp_path, seam) -> None: - if seam == "_write_once": - monkeypatch.setattr( - probe_durability, - seam, - lambda *_args: (_ for _ in ()).throw(OSError("secret")), - ) - else: - monkeypatch.setattr( - probe_durability, - seam, - lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("secret")), - ) - - fd = os.open(os.devnull, os.O_WRONLY) - try: - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - data = probe_durability.encode_jsonl_record(start_record()) - probe_durability.append_jsonl_strict(fd, tmp_path, data) - finally: - os.close(fd) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - assert "secret" not in str(excinfo.value) - assert excinfo.value.__cause__ is None - - -def test_append_short_write_surfaces_record_write_failed(monkeypatch, tmp_path) -> None: - monkeypatch.setattr(probe_durability, "_write_once", lambda _handle, _data: 0) - - fd = os.open(os.devnull, os.O_WRONLY) - try: - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - data = probe_durability.encode_jsonl_record(start_record()) - probe_durability.append_jsonl_strict(fd, tmp_path, data) - finally: - os.close(fd) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - - -def test_attempt_parent_fsync_failure_surfaces_record_write_failed( - monkeypatch, tmp_path -) -> None: - def fail_directory_fsync(_path): - raise OSError("secret") - - monkeypatch.setattr(probe_durability, "_fsync_directory", fail_directory_fsync) - - with acquire_probe_slot(tmp_path / "journal", run_id=RUN_ID) as slot: - proof = probe_contract.CAPABILITY_ORDER[0] - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - assert "secret" not in str(excinfo.value) - - -def test_attempt_directory_collision_fails_closed_to_stale(tmp_path) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - attempt_path = probe_contract.probe_attempts_parent_path(journal) / ATTEMPT_ID - attempt_path.mkdir(parents=True) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - - -def test_start_record_write_failure_poisons_slot_before_retry( - monkeypatch, tmp_path -) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - write_calls = 0 - - def fail_write(_fd, _data): - nonlocal write_calls - write_calls += 1 - raise OSError("secret") - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - monkeypatch.setattr(probe_durability, "_write_once", fail_write) - with pytest.raises(probe_records.ProbeOperationError) as first: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - assert first.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - assert write_calls == 1 - - before = repository_inventory(journal) - with pytest.raises(probe_records.ProbeOperationError) as second: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=OTHER_ATTEMPT_ID, - started_at=FIXED_TS, - ) - after = repository_inventory(journal) - - assert second.value.code == probe_contract.STABLE_ERROR_INTERNAL_ERROR - assert write_calls == 1 - assert not ( - probe_contract.probe_attempts_parent_path(journal) / OTHER_ATTEMPT_ID - ).exists() - assert_inventory_unchanged(before, after) - - -def test_attempt_directory_collision_poisons_slot_before_retry(tmp_path) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - attempt_path = probe_contract.probe_attempts_parent_path(journal) / ATTEMPT_ID - attempt_path.mkdir(parents=True) - with pytest.raises(probe_records.ProbeOperationError) as first: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - assert first.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - - with pytest.raises(probe_records.ProbeOperationError) as second: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=OTHER_ATTEMPT_ID, - started_at=FIXED_TS, - ) - - assert second.value.code == probe_contract.STABLE_ERROR_INTERNAL_ERROR - assert not ( - probe_contract.probe_attempts_parent_path(journal) / OTHER_ATTEMPT_ID - ).exists() - - -def test_writer_record_write_failure_poisons_later_contact( - monkeypatch, tmp_path -) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - writer.dispatch_contact(proof, lambda: None) - - def fail_write(_fd, _data): - raise OSError("secret") - - monkeypatch.setattr(probe_durability, "_write_once", fail_write) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - assert excinfo.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - - with pytest.raises(probe_records.ProbeOperationError) as blocked: - writer.dispatch_contact(proof, lambda: None) - assert blocked.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED diff --git a/tests/sandbox_profile/test_probe_forward_barriers.py b/tests/sandbox_profile/test_probe_forward_barriers.py deleted file mode 100644 index 9ddfc1807..000000000 --- a/tests/sandbox_profile/test_probe_forward_barriers.py +++ /dev/null @@ -1,334 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import os -import threading -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path -from typing import Any - -import pytest - -from solstone.think.sandbox_profile import ( - probe_contract, - probe_durability, - probe_records, - probe_replay, -) -from solstone.think.sandbox_profile.probe_slot import ProbeSlot, acquire_probe_slot -from solstone.think.sandbox_profile.probe_writer import ( - ProbeAttemptWriter, - begin_probe_attempt, -) -from tests.sandbox_profile import ( - ATTEMPT_ID, - FIXED_TS, - OTHER_ATTEMPT_ID, - RUN_ID, - start_record, - write_attempt_dir, - write_ledger, -) - - -def _proof() -> str: - return probe_contract.CAPABILITY_ORDER[0] - - -def _begin( - journal: Path, - *, - selected: tuple[str, ...] | None = None, -) -> tuple[ProbeSlot, ProbeAttemptWriter]: - selected = selected or (_proof(),) - slot = acquire_probe_slot(journal, run_id=RUN_ID) - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - return slot, writer - - -def _assert_probe_error(excinfo, code: str) -> None: - assert excinfo.value.code == code - assert excinfo.value.__cause__ is None - - -def test_acquire_releases_lock_when_replay_escapes_memory_error( - monkeypatch, - tmp_path, -) -> None: - journal = tmp_path / "journal" - sentinel = MemoryError("replay sentinel") - original_replay = probe_replay.replay_probe_ledger - - def fail_replay(_journal: Path) -> probe_records.ProbeReplay: - raise sentinel - - monkeypatch.setattr(probe_replay, "replay_probe_ledger", fail_replay) - with pytest.raises(MemoryError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - assert excinfo.value is sentinel - - monkeypatch.setattr(probe_replay, "replay_probe_ledger", original_replay) - slot = acquire_probe_slot(journal, run_id=RUN_ID) - try: - assert slot.owned is True - finally: - slot.release() - - -def test_complete_terminal_survives_reported_fsync_error_but_slot_is_poisoned( - monkeypatch, - tmp_path, -) -> None: - journal = tmp_path / "journal" - proof = _proof() - slot, writer = _begin(journal) - try: - writer.dispatch_contact(proof, lambda: None) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=(), - reason=probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - duration_ms=1, - finished_at=FIXED_TS, - ) - - def fsync_then_report_failure(fd: int) -> None: - os.fsync(fd) - raise OSError("reported after durable bytes") - - monkeypatch.setattr(probe_durability, "_fsync_file", fsync_then_report_failure) - with pytest.raises(probe_records.ProbeOperationError) as write_error: - writer.write_attempt_terminal(finished_at=FIXED_TS) - _assert_probe_error( - write_error, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - ) - - replay = probe_replay.replay_probe_ledger(journal) - assert replay.attempt_count == 1 - assert ( - replay.attempts[0].terminal.state == probe_contract.ATTEMPT_STATE_DEGRADED - ) - assert ( - replay.attempts[0].terminal.terminal_reason - == probe_contract.ATTEMPT_TERMINAL_REASON_PROOF_FAILED - ) - - with pytest.raises(probe_records.ProbeOperationError) as poisoned: - writer.write_attempt_terminal(finished_at=FIXED_TS) - _assert_probe_error(poisoned, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED) - finally: - slot.release() - - -@pytest.mark.parametrize( - ("crash_point", "expected_error"), - [ - ("before_start_barrier", None), - ("during_start_barrier", probe_contract.STABLE_ERROR_STALE_ATTEMPT), - ("after_start_barrier", probe_contract.STABLE_ERROR_STALE_ATTEMPT), - ], -) -def test_crash_states_around_start_forward_barrier( - tmp_path, - crash_point: str, - expected_error: str | None, -) -> None: - journal = tmp_path / crash_point - if crash_point == "before_start_barrier": - slot = acquire_probe_slot(journal, run_id=RUN_ID) - slot.release() - elif crash_point == "during_start_barrier": - write_attempt_dir(journal) - else: - write_attempt_dir(journal) - write_ledger(journal, [start_record()]) - - if expected_error is None: - slot = acquire_probe_slot(journal, run_id=RUN_ID) - try: - assert slot.owned is True - finally: - slot.release() - return - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - _assert_probe_error(excinfo, expected_error) - - -def test_contact_waits_for_start_fsync_and_revalidates_held_ledger_identity( - monkeypatch, - tmp_path, -) -> None: - journal = tmp_path / "journal" - proof = _proof() - fsync_entered = threading.Event() - finish_fsync = threading.Event() - original_fsync = probe_durability._fsync_file - - def blocked_fsync(fd: int) -> None: - fsync_entered.set() - assert finish_fsync.wait(timeout=2) - original_fsync(fd) - - def begin_attempt() -> ProbeAttemptWriter: - slot = acquire_probe_slot(journal, run_id=RUN_ID) - return begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - - monkeypatch.setattr(probe_durability, "_fsync_file", blocked_fsync) - with ThreadPoolExecutor(max_workers=1) as executor: - begin_future = executor.submit(begin_attempt) - try: - assert fsync_entered.wait(timeout=2) - assert not begin_future.done() - - ledger_path = probe_contract.probe_ledger_path(journal) - ledger_path.unlink() - ledger_path.write_bytes(b"") - finally: - finish_fsync.set() - writer = begin_future.result(timeout=2) - with pytest.raises(probe_records.ProbeOperationError) as contact: - writer.dispatch_contact(proof, lambda: None) - _assert_probe_error(contact, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - writer.slot.release() - - -@pytest.mark.parametrize( - "terminal_class", - ["cleanup_unverified", "cancelled", "internal_error"], -) -def test_nonretryable_terminals_block_same_slot_begin_and_fresh_acquire( - tmp_path, - terminal_class: str, -) -> None: - journal = tmp_path / terminal_class - proof = _proof() - slot, writer = _begin(journal) - try: - if terminal_class == "cancelled": - writer.write_cancelled_attempt( - proof=proof, - state=probe_contract.PROOF_STATE_NOT_RUN, - checks=(), - duration_ms=None, - finished_at=FIXED_TS, - ) - else: - reason = ( - probe_contract.REASON_CLEANUP_UNVERIFIED - if terminal_class == "cleanup_unverified" - else probe_contract.REASON_INTERNAL_ERROR - ) - writer.dispatch_contact(proof, lambda: None) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=(), - reason=reason, - duration_ms=1, - finished_at=FIXED_TS, - ) - writer.write_attempt_terminal(finished_at=FIXED_TS) - - with pytest.raises(probe_records.ProbeOperationError) as same_slot: - begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=OTHER_ATTEMPT_ID, - started_at=FIXED_TS, - ) - _assert_probe_error(same_slot, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - finally: - slot.release() - - with pytest.raises(probe_records.ProbeOperationError) as fresh: - acquire_probe_slot(journal, run_id=RUN_ID) - _assert_probe_error(fresh, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -@pytest.mark.parametrize("exc_type", [MemoryError, KeyboardInterrupt, SystemExit]) -def test_decode_seam_does_not_convert_escaping_exception_classes( - monkeypatch, - tmp_path, - exc_type: type[BaseException], -) -> None: - journal = tmp_path / "journal" - sentinel = exc_type("decode sentinel") - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("{}\n", encoding="utf-8") - - class RaisingDecoder: - def __init__(self, *_args: Any, **_kwargs: Any) -> None: - return None - - def raw_decode(self, _text: str) -> tuple[dict[str, object], int]: - raise sentinel - - monkeypatch.setattr(probe_replay.json, "JSONDecoder", RaisingDecoder) - - with pytest.raises(exc_type) as excinfo: - probe_replay.replay_probe_ledger(journal) - assert excinfo.value is sentinel - - -@pytest.mark.parametrize("exc_type", [MemoryError, KeyboardInterrupt, SystemExit]) -def test_durability_seam_does_not_convert_escaping_exception_classes( - monkeypatch, - tmp_path, - exc_type: type[BaseException], -) -> None: - sentinel = exc_type("durability sentinel") - - def fail_fsync(_fd: int) -> None: - raise sentinel - - monkeypatch.setattr(probe_durability, "_fsync_file", fail_fsync) - fd = os.open(os.devnull, os.O_WRONLY) - try: - with pytest.raises(exc_type) as excinfo: - data = probe_durability.encode_jsonl_record(start_record()) - probe_durability.append_jsonl_strict(fd, tmp_path, data) - finally: - os.close(fd) - assert excinfo.value is sentinel - - -def test_deep_json_recursion_redacts_raw_exception_text( - tmp_path, - caplog, -) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - depth = 10_000 - path.write_text("[" * depth + "0" + "]" * depth + "\n", encoding="utf-8") - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - probe_replay.replay_probe_ledger(journal) - - exc = excinfo.value - assert exc.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - assert str(exc) == probe_contract.STABLE_ERROR_STALE_ATTEMPT - assert exc.__cause__ is None - for forbidden in ("RecursionError", "maximum recursion"): - assert forbidden not in str(exc) - assert forbidden not in repr(exc) - assert forbidden not in caplog.text diff --git a/tests/sandbox_profile/test_probe_framing.py b/tests/sandbox_profile/test_probe_framing.py deleted file mode 100644 index a9f9a8658..000000000 --- a/tests/sandbox_profile/test_probe_framing.py +++ /dev/null @@ -1,206 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import os - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from solstone.think.sandbox_profile.probe_replay import replay_probe_ledger -from tests.sandbox_profile import ( - ATTEMPT_ID, - complete_attempt_records, - write_attempt_dir, - write_ledger, -) - - -def _assert_stale(journal) -> None: - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - - -def test_replay_accepts_arbitrary_field_order(tmp_path) -> None: - journal = tmp_path / "journal" - records = complete_attempt_records() - write_attempt_dir(journal) - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - "\n".join( - json.dumps(dict(reversed(list(record.items())))) for record in records - ) - + "\n", - encoding="utf-8", - ) - - replay = replay_probe_ledger(journal) - assert replay.attempt_count == 1 - - -def test_replay_rejects_missing_final_lf(tmp_path) -> None: - journal = tmp_path / "journal" - records = complete_attempt_records() - write_attempt_dir(journal) - path = write_ledger(journal, records) - path.write_text(path.read_text(encoding="utf-8").rstrip("\n"), encoding="utf-8") - - _assert_stale(journal) - - -def test_replay_rejects_blank_lines(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - path = write_ledger(journal, complete_attempt_records()) - path.write_text(path.read_text(encoding="utf-8") + "\n", encoding="utf-8") - - _assert_stale(journal) - - -def test_replay_rejects_duplicate_fields(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - path = write_ledger(journal, records) - duplicate_line = ( - path.read_text(encoding="utf-8") - .splitlines()[0] - .replace("{", '{"run_id":"x",', 1) - ) - path.write_text(duplicate_line + "\n", encoding="utf-8") - - _assert_stale(journal) - - -def test_replay_rejects_unknown_fields(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - records[0]["unknown"] = True - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_retired_discriminator_field(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - retired_key = "record_kind" - records[0][retired_key] = records[0].pop("type") - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_old_attempt_terminal_reason_field(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - records[-1]["reason"] = records[-1]["terminal_reason"] - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_old_attempt_terminal_duration_field(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - records[-1]["duration_ms"] = 1 - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_missing_type(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - del records[0]["type"] - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_missing_terminal_reason(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - del records[-1]["terminal_reason"] - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_mixed_old_and_new_rows(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - records = complete_attempt_records() - retired_key = "record_kind" - records[1][retired_key] = records[1].pop("type") - write_ledger(journal, records) - - _assert_stale(journal) - - -def test_replay_rejects_trailing_bytes_invalid_utf8_and_invalid_json(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - path = write_ledger(journal, complete_attempt_records()) - path.write_text(path.read_text(encoding="utf-8").replace("\n", " \n", 1)) - _assert_stale(journal) - - path.write_bytes(b"\xff\n") - _assert_stale(journal) - - path.write_text("{\n", encoding="utf-8") - _assert_stale(journal) - - -def test_replay_rejects_non_object_json(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal, ATTEMPT_ID) - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("[]\n", encoding="utf-8") - - _assert_stale(journal) - - -def test_replay_rejects_symlinked_ledger_before_decode(tmp_path) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - target = tmp_path / "target.jsonl" - target.write_text("[]\n", encoding="utf-8") - path.symlink_to(target) - - _assert_stale(journal) - - -def test_replay_rejects_fifo_ledger_before_decode(tmp_path) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - os.mkfifo(path) - - _assert_stale(journal) - - -def test_replay_maps_deep_json_recursion_to_stale(tmp_path) -> None: - journal = tmp_path / "journal" - path = probe_contract.probe_ledger_path(journal) - path.parent.mkdir(parents=True, exist_ok=True) - depth = 10_000 - path.write_text("[" * depth + "0" + "]" * depth + "\n", encoding="utf-8") - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_STALE_ATTEMPT - assert excinfo.value.__cause__ is None diff --git a/tests/sandbox_profile/test_probe_lock.py b/tests/sandbox_profile/test_probe_lock.py deleted file mode 100644 index 31008129f..000000000 --- a/tests/sandbox_profile/test_probe_lock.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import RUN_ID - - -def test_acquire_probe_slot_is_single_process_nonblocking(tmp_path) -> None: - journal = tmp_path / "journal" - first = acquire_probe_slot(journal, run_id=RUN_ID) - assert first.owned is True - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_PROBE_ACTIVE - first.release() - - -def test_probe_active_does_not_mutate_repository_inventory(tmp_path) -> None: - journal = tmp_path / "journal" - first = acquire_probe_slot(journal, run_id=RUN_ID) - before = repository_inventory(Path.cwd()) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - after = repository_inventory(Path.cwd()) - first.release() - - assert excinfo.value.code == probe_contract.STABLE_ERROR_PROBE_ACTIVE - assert_inventory_unchanged(before, after) diff --git a/tests/sandbox_profile/test_probe_records.py b/tests/sandbox_profile/test_probe_records.py deleted file mode 100644 index 9449d7b70..000000000 --- a/tests/sandbox_profile/test_probe_records.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from tests.sandbox_profile import ATTEMPT_ID, FIXED_TS, RUN_ID, proof_record - - -def test_canonical_uuid_rejects_noncanonical_text() -> None: - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_canonical_uuid(RUN_ID.upper()) - - -def test_selected_must_be_nonempty_duplicate_free_canonical_subsequence() -> None: - assert probe_records.validate_selected([probe_contract.CAPABILITY_ORDER[0]]) == ( - probe_contract.CAPABILITY_ORDER[0], - ) - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_selected([]) - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_selected( - [probe_contract.CAPABILITY_ORDER[1], probe_contract.CAPABILITY_ORDER[0]] - ) - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_selected( - [probe_contract.CAPABILITY_ORDER[0], probe_contract.CAPABILITY_ORDER[0]] - ) - - -def test_execution_order_must_be_duplicate_free_permutation() -> None: - selected = probe_contract.CAPABILITY_ORDER[:2] - assert probe_records.validate_execution_order( - list(reversed(selected)), selected - ) == ( - selected[1], - selected[0], - ) - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_execution_order([selected[0], selected[0]], selected) - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_execution_order([selected[0]], selected) - - -def test_duration_rejects_bool_before_int() -> None: - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.validate_non_negative_int(True) - - -def test_proof_terminal_semantics_for_passed_failed_and_not_run() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - passed = probe_records.validate_proof_terminal_payload(proof_record(proof=proof)) - assert passed.state == probe_contract.PROOF_STATE_PASSED - - failed = probe_records.validate_proof_terminal_payload( - proof_record( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=probe_contract.PROOF_CHECKS[proof][:1], - reason=probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - ) - ) - assert failed.cleanup_state == probe_contract.DECLARED_CLEANUP_STATES[proof] - - not_run = probe_records.validate_proof_terminal_payload( - proof_record( - proof=proof, - state=probe_contract.PROOF_STATE_NOT_RUN, - reason=probe_contract.REASON_DEPENDENCY_FAILED, - duration_ms=None, - ) - ) - assert not_run.cleanup_state == probe_contract.CLEANUP_STATE_VERIFIED - - -def test_dependency_failed_is_valid_only_for_not_run() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.build_proof_terminal_record( - run_id=RUN_ID, - attempt_id=ATTEMPT_ID, - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=(), - reason=probe_contract.REASON_DEPENDENCY_FAILED, - duration_ms=1, - finished_at=FIXED_TS, - ) - - -def test_operation_error_carries_only_closed_fields() -> None: - exc = probe_records.ProbeOperationError( - probe_contract.STABLE_ERROR_STALE_ATTEMPT, - attempt_id=ATTEMPT_ID, - record_type=probe_contract.RECORD_TYPE_PROOF_TERMINAL, - proof=probe_contract.CAPABILITY_ORDER[0], - ) - assert str(exc) == probe_contract.STABLE_ERROR_STALE_ATTEMPT - assert exc.__cause__ is None - with pytest.raises(ValueError): - probe_records.ProbeOperationError( - probe_contract.STABLE_ERROR_STALE_ATTEMPT, - proof="free-text", - ) diff --git a/tests/sandbox_profile/test_probe_redaction.py b/tests/sandbox_profile/test_probe_redaction.py deleted file mode 100644 index 769318e3e..000000000 --- a/tests/sandbox_profile/test_probe_redaction.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import os -import sys - -import pytest - -from solstone.think.sandbox_profile import ( - probe_contract, - probe_durability, - probe_records, - probe_slot, -) -from tests.sandbox_profile import RUN_ID, start_record - - -def test_write_error_secret_absent_from_exception_argv_and_logs( - tmp_path, monkeypatch, caplog -) -> None: - secret = "recognizable-secret-probe-token" - - def fail_write(_fd, _data): - raise OSError(secret) - - monkeypatch.setattr(probe_durability, "_write_once", fail_write) - - fd = os.open(os.devnull, os.O_WRONLY) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - data = probe_durability.encode_jsonl_record(start_record()) - probe_durability.append_jsonl_strict(fd, tmp_path, data) - os.close(fd) - - assert excinfo.value.code == probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED - assert secret not in str(excinfo.value) - assert secret not in caplog.text - assert secret not in " ".join(sys.argv) - assert excinfo.value.__cause__ is None - - -def test_lock_acquire_oserror_secret_absent_from_exception_repr_and_logs( - tmp_path, monkeypatch, caplog -) -> None: - secret = "recognizable-secret-lock-path" - - def fail_lock(_path): - raise OSError(secret) - - monkeypatch.setattr(probe_slot, "_open_lock_fd", fail_lock) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - probe_slot.acquire_probe_slot(tmp_path / secret, run_id=RUN_ID) - - exc = excinfo.value - assert exc.code == probe_contract.STABLE_ERROR_INTERNAL_ERROR - assert secret not in str(exc) - assert secret not in repr(exc) - assert exc.__cause__ is None - assert secret not in caplog.text - - -def test_operation_error_optional_fields_reject_free_text() -> None: - with pytest.raises(probe_records.ProbeRecordValidationError): - probe_records.ProbeOperationError( - probe_contract.STABLE_ERROR_INTERNAL_ERROR, - attempt_id="recognizable-secret-probe-token", - ) - with pytest.raises(ValueError): - probe_records.ProbeOperationError( - probe_contract.STABLE_ERROR_INTERNAL_ERROR, - record_type="recognizable-secret-probe-token", - ) - with pytest.raises(ValueError): - probe_records.ProbeOperationError( - probe_contract.STABLE_ERROR_INTERNAL_ERROR, - proof="recognizable-secret-probe-token", - ) diff --git a/tests/sandbox_profile/test_probe_replay.py b/tests/sandbox_profile/test_probe_replay.py deleted file mode 100644 index af85cb3b2..000000000 --- a/tests/sandbox_profile/test_probe_replay.py +++ /dev/null @@ -1,205 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import probe_contract, probe_records -from solstone.think.sandbox_profile.probe_replay import replay_probe_ledger -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ( - ATTEMPT_ID, - RUN_ID, - complete_attempt_records, - terminal_record, - write_attempt_dir, - write_ledger, -) - - -def _assert_error(journal: Path, code: str) -> None: - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - replay_probe_ledger(journal) - assert excinfo.value.code == code - - -def test_empty_ledger_is_retry_permitted(tmp_path) -> None: - replay = replay_probe_ledger(tmp_path / "journal") - - assert replay.retry_permitted is True - assert replay.attempt_count == 0 - assert replay.ledger_size_bytes == 0 - assert replay.ledger_identity is None - - -def test_ok_terminal_is_retry_permitted(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - write_ledger(journal, complete_attempt_records()) - - before = repository_inventory(Path.cwd()) - replay = replay_probe_ledger(journal) - after = repository_inventory(Path.cwd()) - - assert replay.retry_permitted is True - assert replay.run_id == RUN_ID - ledger_stat = probe_contract.probe_ledger_path(journal).stat() - assert replay.ledger_size_bytes == ledger_stat.st_size - assert replay.ledger_identity == (ledger_stat.st_dev, ledger_stat.st_ino) - assert_inventory_unchanged(before, after) - - -def test_degraded_proof_failed_with_verified_cleanup_is_retry_permitted( - tmp_path, -) -> None: - journal = tmp_path / "journal" - proof = probe_contract.CAPABILITY_ORDER[0] - write_attempt_dir(journal) - write_ledger( - journal, - complete_attempt_records( - proof_overrides={ - proof: { - "state": probe_contract.PROOF_STATE_FAILED, - "reason": probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - } - } - ), - ) - - replay = replay_probe_ledger(journal) - - assert replay.retry_permitted is True - - -def test_cleanup_unverified_cancelled_and_internal_terminals_are_stale( - tmp_path, -) -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - cases = [ - { - "state": probe_contract.PROOF_STATE_FAILED, - "reason": probe_contract.REASON_CLEANUP_UNVERIFIED, - }, - { - "state": probe_contract.PROOF_STATE_FAILED, - "reason": probe_contract.REASON_CANCELLED, - }, - { - "state": probe_contract.PROOF_STATE_FAILED, - "reason": probe_contract.REASON_INTERNAL_ERROR, - }, - ] - for index, override in enumerate(cases): - journal = tmp_path / f"journal-{index}" - write_attempt_dir(journal) - write_ledger( - journal, complete_attempt_records(proof_overrides={proof: override}) - ) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -def test_incomplete_attempt_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - write_ledger(journal, complete_attempt_records()[:1]) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -def test_orphan_attempt_directory_is_stale_and_does_not_mutate_repo(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal) - - before = repository_inventory(Path.cwd()) - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - after = repository_inventory(Path.cwd()) - - assert_inventory_unchanged(before, after) - - -def test_missing_attempt_directory_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - write_ledger(journal, complete_attempt_records()) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -def test_wrong_attempt_directory_mode_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - write_attempt_dir(journal, mode=0o755) - write_ledger(journal, complete_attempt_records()) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -def test_attempt_directory_symlink_is_stale(tmp_path) -> None: - journal = tmp_path / "journal" - target = tmp_path / "target" - target.mkdir() - parent = probe_contract.probe_attempts_parent_path(journal) - parent.mkdir(parents=True) - (parent / ATTEMPT_ID).symlink_to(target, target_is_directory=True) - write_ledger(journal, complete_attempt_records()) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -@pytest.mark.parametrize( - "later_override", - [ - { - "state": probe_contract.PROOF_STATE_PASSED, - "checks": None, - "reason": None, - "duration_ms": 1, - }, - { - "state": probe_contract.PROOF_STATE_FAILED, - "checks": (), - "reason": probe_contract.REASON_INTERNAL_ERROR, - "duration_ms": 1, - }, - { - "state": probe_contract.PROOF_STATE_NOT_RUN, - "checks": (), - "reason": probe_contract.REASON_DEPENDENCY_FAILED, - "duration_ms": None, - }, - ], -) -def test_cancelled_terminal_requires_exact_contiguous_not_run_suffix( - tmp_path, - later_override, -) -> None: - journal = tmp_path / "journal" - selected = probe_contract.CAPABILITY_ORDER[:2] - records = complete_attempt_records(selected=selected) - first_proof = records[1] - later_proof = records[2] - first_proof["state"] = probe_contract.PROOF_STATE_FAILED - first_proof["checks"] = [] - first_proof["reason"] = probe_contract.REASON_CANCELLED - first_proof["duration_ms"] = 1 - later_proof["state"] = later_override["state"] - later_proof["checks"] = ( - list(probe_contract.PROOF_CHECKS[selected[1]]) - if later_override["checks"] is None - else list(later_override["checks"]) - ) - later_proof["reason"] = later_override["reason"] - later_proof["duration_ms"] = later_override["duration_ms"] - records[-1] = { - **terminal_record(proofs=[first_proof]), - "attempt_id": ATTEMPT_ID, - "state": probe_contract.ATTEMPT_STATE_CANCELLED, - "terminal_reason": probe_contract.REASON_CANCELLED, - } - write_attempt_dir(journal) - write_ledger(journal, records) - - _assert_error(journal, probe_contract.STABLE_ERROR_STALE_ATTEMPT) diff --git a/tests/sandbox_profile/test_probe_slot_runtime.py b/tests/sandbox_profile/test_probe_slot_runtime.py deleted file mode 100644 index 7d2704987..000000000 --- a/tests/sandbox_profile/test_probe_slot_runtime.py +++ /dev/null @@ -1,835 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import os -import threading -from pathlib import Path - -import pytest - -from solstone.think.sandbox_profile import ( - probe_contract, - probe_durability, - probe_records, - probe_slot, -) -from solstone.think.sandbox_profile.probe_slot import acquire_probe_slot -from solstone.think.sandbox_profile.probe_writer import ( - ProbeAttemptWriter, - begin_probe_attempt, -) -from tests._repo_inventory import assert_inventory_unchanged, repository_inventory -from tests.sandbox_profile import ( - ATTEMPT_ID, - FIXED_TS, - OTHER_ATTEMPT_ID, - RUN_ID, - THIRD_ATTEMPT_ID, - complete_attempt_records, - write_attempt_dir, - write_ledger, -) - - -def _attempt_id(index: int) -> str: - return f"00000000-0000-4000-8000-{index:012d}" - - -def _proof() -> str: - return probe_contract.CAPABILITY_ORDER[0] - - -def _begin( - slot: probe_slot.ProbeSlot, - *, - attempt_id: str = ATTEMPT_ID, -) -> ProbeAttemptWriter: - proof = _proof() - return begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=attempt_id, - started_at=FIXED_TS, - ) - - -def _write_passed_proof(writer: ProbeAttemptWriter) -> None: - proof = _proof() - writer.dispatch_contact(proof, lambda: None) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - - -def _complete_attempt(writer: ProbeAttemptWriter) -> None: - _write_passed_proof(writer) - writer.write_attempt_terminal(finished_at=FIXED_TS) - - -def _assert_probe_error(excinfo, code: str) -> None: - assert excinfo.value.code == code - assert excinfo.value.__cause__ is None - - -def _ledger_rows(journal: Path) -> list[dict[str, object]]: - path = probe_contract.probe_ledger_path(journal) - return [ - json.loads(line) - for line in path.read_text(encoding="utf-8").splitlines() - if line - ] - - -def test_begin_validation_failure_spends_slot_without_mutation(tmp_path) -> None: - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - before = repository_inventory(journal) - with pytest.raises(probe_records.ProbeOperationError) as first: - begin_probe_attempt( - slot, - selected=(), - execution_order=(), - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - after_first = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as second: - _begin(slot, attempt_id=OTHER_ATTEMPT_ID) - after_second = repository_inventory(journal) - - _assert_probe_error(first, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - _assert_probe_error(second, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after_first) - assert_inventory_unchanged(before, after_second) - assert not probe_contract.probe_attempts_parent_path(journal).exists() - - -def test_begin_after_release_is_internal_without_mutation(tmp_path) -> None: - journal = tmp_path / "journal" - slot = acquire_probe_slot(journal, run_id=RUN_ID) - slot.release() - before = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - _begin(slot) - after = repository_inventory(journal) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after) - assert not probe_contract.probe_attempts_parent_path(journal).exists() - - -def test_append_after_release_is_internal_without_mutation(tmp_path) -> None: - journal = tmp_path / "journal" - proof = _proof() - slot = acquire_probe_slot(journal, run_id=RUN_ID) - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - slot.release() - before = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - after = repository_inventory(journal) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after) - - -def test_unlocked_append_after_release_raises_stable_error_without_mutation( - tmp_path, -) -> None: - journal = tmp_path / "journal" - slot = acquire_probe_slot(journal, run_id=RUN_ID) - slot.release() - before = repository_inventory(journal) - - with slot.operation_guard(): - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - slot.append_encoded_record_unlocked( - b"{}\n", - record_type=probe_contract.RECORD_TYPE_ATTEMPT_STARTED, - attempt_id=ATTEMPT_ID, - ) - after = repository_inventory(journal) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_PROBE_ACTIVE) - assert_inventory_unchanged(before, after) - - -def test_second_begin_after_active_and_terminal_are_internal_without_mutation( - tmp_path, -) -> None: - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - before_active_retry = repository_inventory(journal) - with pytest.raises(probe_records.ProbeOperationError) as active_retry: - _begin(slot, attempt_id=OTHER_ATTEMPT_ID) - after_active_retry = repository_inventory(journal) - - _complete_attempt(writer) - before_terminal_retry = repository_inventory(journal) - with pytest.raises(probe_records.ProbeOperationError) as terminal_retry: - _begin(slot, attempt_id=THIRD_ATTEMPT_ID) - after_terminal_retry = repository_inventory(journal) - - _assert_probe_error(active_retry, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - _assert_probe_error(terminal_retry, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before_active_retry, after_active_retry) - assert_inventory_unchanged(before_terminal_retry, after_terminal_retry) - assert not ( - probe_contract.probe_attempts_parent_path(journal) / OTHER_ATTEMPT_ID - ).exists() - assert not ( - probe_contract.probe_attempts_parent_path(journal) / THIRD_ATTEMPT_ID - ).exists() - - -def test_second_begin_from_foreign_thread_while_active_is_internal_without_mutation( - tmp_path, -) -> None: - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - _begin(slot) - before = repository_inventory(journal) - result: list[str] = [] - - def try_second_begin() -> None: - try: - _begin(slot, attempt_id=OTHER_ATTEMPT_ID) - except probe_records.ProbeOperationError as exc: - result.append(exc.code) - - thread = threading.Thread(target=try_second_begin) - thread.start() - thread.join(timeout=2) - after = repository_inventory(journal) - - assert not thread.is_alive() - assert result == [probe_contract.STABLE_ERROR_INTERNAL_ERROR] - assert_inventory_unchanged(before, after) - assert not ( - probe_contract.probe_attempts_parent_path(journal) / OTHER_ATTEMPT_ID - ).exists() - - -def test_nested_begin_reentrancy_fails_without_deadlock_or_mutation(tmp_path) -> None: - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - before = repository_inventory(journal) - with slot.operation_guard(): - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - _begin(slot) - after = repository_inventory(journal) - - writer = _begin(slot) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after) - assert writer.start.attempt_id == ATTEMPT_ID - - -def test_start_prospective_overflow_spends_without_directory_or_append( - monkeypatch, tmp_path -) -> None: - journal = tmp_path / "journal" - oversized = b"x" * (probe_contract.MAX_LEDGER_BYTES + 1) - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - before = repository_inventory(journal) - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: oversized, - ) - with pytest.raises(probe_records.ProbeOperationError) as first: - _begin(slot) - after_first = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as second: - _begin(slot, attempt_id=OTHER_ATTEMPT_ID) - after_second = repository_inventory(journal) - - _assert_probe_error(first, probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - _assert_probe_error(second, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after_first) - assert_inventory_unchanged(before, after_second) - assert probe_contract.probe_ledger_path(journal).read_bytes() == b"" - assert not probe_contract.probe_attempts_parent_path(journal).exists() - - -def test_attempt_count_allows_sixty_fourth_and_refuses_existing_sixty_four( - tmp_path, -) -> None: - proof = _proof() - journal_63 = tmp_path / "journal-63" - records_63: list[dict[str, object]] = [] - for index in range(probe_contract.MAX_ATTEMPTS - 1): - attempt_id = _attempt_id(index) - write_attempt_dir(journal_63, attempt_id) - records_63.extend(complete_attempt_records(attempt_id=attempt_id)) - write_ledger(journal_63, records_63) - - with acquire_probe_slot(journal_63, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=(proof,), - execution_order=(proof,), - attempt_id=_attempt_id(probe_contract.MAX_ATTEMPTS - 1), - started_at=FIXED_TS, - ) - assert writer.start.attempt_id == _attempt_id(probe_contract.MAX_ATTEMPTS - 1) - - journal_64 = tmp_path / "journal-64" - records_64: list[dict[str, object]] = [] - for index in range(probe_contract.MAX_ATTEMPTS): - attempt_id = _attempt_id(index) - write_attempt_dir(journal_64, attempt_id) - records_64.extend(complete_attempt_records(attempt_id=attempt_id)) - ledger = write_ledger(journal_64, records_64) - before = ledger.read_bytes() - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal_64, run_id=RUN_ID) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - assert ledger.read_bytes() == before - assert not ( - probe_contract.probe_attempts_parent_path(journal_64) - / _attempt_id(probe_contract.MAX_ATTEMPTS) - ).exists() - - -@pytest.mark.parametrize("path_kind", ["ledger", "lock"]) -@pytest.mark.parametrize("node_kind", ["symlink", "fifo"]) -def test_nonregular_ledger_and_lock_paths_fail_stale_before_acquire( - tmp_path, - path_kind: str, - node_kind: str, -) -> None: - journal = tmp_path / f"{path_kind}-{node_kind}" - path = ( - probe_contract.probe_ledger_path(journal) - if path_kind == "ledger" - else probe_contract.probe_lock_path(journal) - ) - path.parent.mkdir(parents=True, exist_ok=True) - if node_kind == "symlink": - target = tmp_path / f"{path_kind}-target" - target.write_text("", encoding="utf-8") - path.symlink_to(target) - else: - os.mkfifo(path) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - acquire_probe_slot(journal, run_id=RUN_ID) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - assert not probe_contract.probe_attempts_parent_path(journal).exists() - - -@pytest.mark.parametrize("path_kind", ["ledger", "lock"]) -def test_canonical_path_replacement_poisons_before_begin( - tmp_path, - path_kind: str, -) -> None: - journal = tmp_path / f"replace-{path_kind}" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - path = ( - probe_contract.probe_ledger_path(journal) - if path_kind == "ledger" - else probe_contract.probe_lock_path(journal) - ) - path.unlink() - path.write_bytes(b"") - before = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as first: - _begin(slot) - after_first = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as second: - _begin(slot, attempt_id=OTHER_ATTEMPT_ID) - - _assert_probe_error(first, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - _assert_probe_error(second, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert_inventory_unchanged(before, after_first) - assert not ( - probe_contract.probe_attempts_parent_path(journal) / ATTEMPT_ID - ).exists() - - -def test_external_same_inode_ledger_growth_poisons_before_begin(tmp_path) -> None: - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - probe_contract.probe_ledger_path(journal).write_bytes(b"x") - before = repository_inventory(journal) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - _begin(slot) - after = repository_inventory(journal) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - assert_inventory_unchanged(before, after) - assert not probe_contract.probe_attempts_parent_path(journal).exists() - - -def test_start_exact_ledger_bound_is_admitted(monkeypatch, tmp_path) -> None: - journal = tmp_path / "journal" - data = b"x" * probe_contract.MAX_LEDGER_BYTES - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: data, - ) - writer = _begin(slot) - - assert writer.start.attempt_id == ATTEMPT_ID - assert probe_contract.probe_ledger_path(journal).stat().st_size == len(data) - assert (probe_contract.probe_attempts_parent_path(journal) / ATTEMPT_ID).is_dir() - - -def test_proof_and_terminal_prospective_boundaries(monkeypatch, tmp_path) -> None: - proof = _proof() - original_encode = probe_durability.encode_jsonl_record - - proof_journal = tmp_path / "proof" - with acquire_probe_slot(proof_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - remaining = probe_contract.MAX_LEDGER_BYTES - slot.ledger_size_bytes - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: b"x" * remaining, - ) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - assert slot.ledger_size_bytes == probe_contract.MAX_LEDGER_BYTES - - monkeypatch.setattr(probe_durability, "encode_jsonl_record", original_encode) - over_journal = tmp_path / "proof-over" - with acquire_probe_slot(over_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - before = probe_contract.probe_ledger_path(over_journal).stat().st_size - too_large = probe_contract.MAX_LEDGER_BYTES - slot.ledger_size_bytes + 1 - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: b"x" * too_large, - ) - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - after = probe_contract.probe_ledger_path(over_journal).stat().st_size - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED) - assert after == before - - monkeypatch.setattr(probe_durability, "encode_jsonl_record", original_encode) - terminal_journal = tmp_path / "terminal" - with acquire_probe_slot(terminal_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - _write_passed_proof(writer) - remaining = probe_contract.MAX_LEDGER_BYTES - slot.ledger_size_bytes - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: b"x" * remaining, - ) - writer.write_attempt_terminal(finished_at=FIXED_TS) - assert slot.ledger_size_bytes == probe_contract.MAX_LEDGER_BYTES - - monkeypatch.setattr(probe_durability, "encode_jsonl_record", original_encode) - terminal_over_journal = tmp_path / "terminal-over" - with acquire_probe_slot(terminal_over_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - _write_passed_proof(writer) - before = probe_contract.probe_ledger_path(terminal_over_journal).stat().st_size - too_large = probe_contract.MAX_LEDGER_BYTES - slot.ledger_size_bytes + 1 - monkeypatch.setattr( - probe_durability, - "encode_jsonl_record", - lambda _record: b"x" * too_large, - ) - with pytest.raises(probe_records.ProbeOperationError) as terminal_over: - writer.write_attempt_terminal(finished_at=FIXED_TS) - after = probe_contract.probe_ledger_path(terminal_over_journal).stat().st_size - - _assert_probe_error( - terminal_over, probe_contract.STABLE_ERROR_ATTEMPT_LIMIT_REACHED - ) - assert after == before - - -def test_writer_replacement_cannot_escape_poisoned_slot(monkeypatch, tmp_path) -> None: - journal = tmp_path / "journal" - proof = _proof() - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - - def fail_write(_fd, _data): - raise OSError("secret") - - monkeypatch.setattr(probe_durability, "_write_once", fail_write) - with pytest.raises(probe_records.ProbeOperationError) as first: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - replacement = ProbeAttemptWriter( - slot=slot, - start=writer.start, - attempt_dir=writer.attempt_dir, - ) - with pytest.raises(probe_records.ProbeOperationError) as second: - replacement.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - - _assert_probe_error(first, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED) - _assert_probe_error(second, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED) - - -def test_cancelled_attempt_before_any_proof_writes_not_run_suffix_and_terminal( - tmp_path, -) -> None: - selected = probe_contract.CAPABILITY_ORDER[:2] - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - writer.write_cancelled_attempt( - proof=selected[0], - state=probe_contract.PROOF_STATE_NOT_RUN, - checks=(), - duration_ms=None, - finished_at=FIXED_TS, - ) - with pytest.raises(probe_records.ProbeOperationError) as second_begin: - begin_probe_attempt( - slot, - selected=(selected[0],), - execution_order=(selected[0],), - attempt_id=OTHER_ATTEMPT_ID, - started_at=FIXED_TS, - ) - - rows = _ledger_rows(journal) - assert [row["type"] for row in rows] == [ - probe_contract.RECORD_TYPE_ATTEMPT_STARTED, - probe_contract.RECORD_TYPE_PROOF_TERMINAL, - probe_contract.RECORD_TYPE_PROOF_TERMINAL, - probe_contract.RECORD_TYPE_ATTEMPT_TERMINAL, - ] - assert rows[1]["state"] == probe_contract.PROOF_STATE_NOT_RUN - assert rows[1]["reason"] == probe_contract.REASON_CANCELLED - assert rows[2]["state"] == probe_contract.PROOF_STATE_NOT_RUN - assert rows[2]["checks"] == [] - assert rows[2]["reason"] == probe_contract.REASON_CANCELLED - assert rows[-1]["state"] == probe_contract.ATTEMPT_STATE_CANCELLED - assert rows[-1]["terminal_reason"] == probe_contract.REASON_CANCELLED - _assert_probe_error(second_begin, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - with pytest.raises(probe_records.ProbeOperationError) as reacquire: - acquire_probe_slot(journal, run_id=RUN_ID) - _assert_probe_error(reacquire, probe_contract.STABLE_ERROR_STALE_ATTEMPT) - - -def test_cancelled_attempt_later_rows_follow_contract_shape( - monkeypatch, - tmp_path, -) -> None: - selected = probe_contract.CAPABILITY_ORDER[:2] - later_shape = dict(probe_contract.CANCELLATION["later_proof"]) - later_shape[probe_contract.FIELD_REASON] = probe_contract.REASON_DEPENDENCY_FAILED - monkeypatch.setattr( - probe_contract, - "CANCELLATION", - { - **probe_contract.CANCELLATION, - "later_proof": later_shape, - }, - ) - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - writer.write_cancelled_attempt( - proof=selected[0], - state=probe_contract.PROOF_STATE_NOT_RUN, - checks=(), - duration_ms=None, - finished_at=FIXED_TS, - ) - - rows = _ledger_rows(journal) - assert rows[2]["state"] == later_shape[probe_contract.FIELD_STATE] - assert rows[2]["checks"] == list(later_shape[probe_contract.FIELD_CHECKS]) - assert rows[2]["reason"] == probe_contract.REASON_DEPENDENCY_FAILED - assert rows[2]["duration_ms"] == later_shape[probe_contract.FIELD_DURATION_MS] - assert rows[2]["cleanup_state"] == later_shape[probe_contract.FIELD_CLEANUP_STATE] - assert rows[-1]["terminal_reason"] == probe_contract.REASON_CANCELLED - - -def test_cancelled_attempt_after_contact_in_flight_writes_failed_first_row( - tmp_path, -) -> None: - selected = probe_contract.CAPABILITY_ORDER[:2] - journal = tmp_path / "journal" - contact_started = threading.Event() - release_contact = threading.Event() - cancel_result: list[str] = [] - - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = begin_probe_attempt( - slot, - selected=selected, - execution_order=selected, - attempt_id=ATTEMPT_ID, - started_at=FIXED_TS, - ) - - def contact_operation() -> None: - contact_started.set() - assert release_contact.wait(timeout=2) - - contact_thread = threading.Thread( - target=lambda: writer.dispatch_contact(selected[0], contact_operation) - ) - contact_thread.start() - assert contact_started.wait(timeout=2) - - def cancel() -> None: - writer.write_cancelled_attempt( - proof=selected[0], - state=probe_contract.PROOF_STATE_FAILED, - checks=probe_contract.PROOF_CHECKS[selected[0]][:1], - duration_ms=1, - finished_at=FIXED_TS, - ) - cancel_result.append("done") - - cancel_thread = threading.Thread(target=cancel) - cancel_thread.start() - release_contact.set() - contact_thread.join(timeout=2) - cancel_thread.join(timeout=2) - - assert not contact_thread.is_alive() - assert not cancel_thread.is_alive() - assert cancel_result == ["done"] - rows = _ledger_rows(journal) - assert rows[1]["state"] == probe_contract.PROOF_STATE_FAILED - assert rows[1]["checks"] == [probe_contract.PROOF_CHECKS[selected[0]][0]] - assert rows[1]["reason"] == probe_contract.REASON_CANCELLED - assert rows[2]["state"] == probe_contract.PROOF_STATE_NOT_RUN - assert rows[2]["reason"] == probe_contract.REASON_CANCELLED - assert rows[-1]["state"] == probe_contract.ATTEMPT_STATE_CANCELLED - - -def test_malformed_first_cancellation_is_sticky_and_poisons_internal( - tmp_path, -) -> None: - proof = _proof() - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - before_rows = _ledger_rows(journal) - with pytest.raises(probe_records.ProbeOperationError) as malformed: - writer.write_cancelled_attempt( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=(), - duration_ms=1, - finished_at=FIXED_TS, - ) - with pytest.raises(probe_records.ProbeOperationError) as contact: - writer.dispatch_contact(proof, lambda: None) - - _assert_probe_error(malformed, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - _assert_probe_error(contact, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - assert _ledger_rows(journal) == before_rows - - -def test_failed_first_cancellation_append_is_sticky_and_poisons_exact_code( - monkeypatch, - tmp_path, -) -> None: - proof = _proof() - journal = tmp_path / "journal" - with acquire_probe_slot(journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - before_rows = _ledger_rows(journal) - - def fail_write(_fd, _data): - raise OSError("secret") - - monkeypatch.setattr(probe_durability, "_write_once", fail_write) - with pytest.raises(probe_records.ProbeOperationError) as failed_append: - writer.write_cancelled_attempt( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - checks=probe_contract.PROOF_CHECKS[proof][:1], - duration_ms=1, - finished_at=FIXED_TS, - ) - with pytest.raises(probe_records.ProbeOperationError) as contact: - writer.dispatch_contact(proof, lambda: None) - - _assert_probe_error(failed_append, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED) - _assert_probe_error(contact, probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED) - assert _ledger_rows(journal) == before_rows - - -def test_dispatch_contact_return_and_exception_consume_authorization( - tmp_path, -) -> None: - proof = _proof() - - return_journal = tmp_path / "return" - with acquire_probe_slot(return_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - result = object() - - assert writer.dispatch_contact(proof, lambda: result) is result - with pytest.raises(probe_records.ProbeOperationError) as second_return: - writer.dispatch_contact(proof, lambda: object()) - - _assert_probe_error(second_return, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - exception_journal = tmp_path / "exception" - expected = RuntimeError("caller failure") - - def fail_contact() -> None: - raise expected - - with acquire_probe_slot(exception_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - with pytest.raises(RuntimeError) as excinfo: - writer.dispatch_contact(proof, fail_contact) - with pytest.raises(probe_records.ProbeOperationError) as second_exception: - writer.dispatch_contact(proof, lambda: None) - - assert excinfo.value is expected - _assert_probe_error(second_exception, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - -def test_dispatch_contact_reentrant_callback_returns_internal_error( - tmp_path, -) -> None: - proof = _proof() - with acquire_probe_slot(tmp_path / "journal", run_id=RUN_ID) as slot: - writer = _begin(slot) - - def reenter() -> None: - writer.dispatch_contact(proof, lambda: None) - - with pytest.raises(probe_records.ProbeOperationError) as excinfo: - writer.dispatch_contact(proof, reenter) - with pytest.raises(probe_records.ProbeOperationError) as second: - writer.dispatch_contact(proof, lambda: None) - - _assert_probe_error(excinfo, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - _assert_probe_error(second, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - -def test_proof_terminal_requires_matching_contact_consumption(tmp_path) -> None: - proof = _proof() - - passed_journal = tmp_path / "passed" - with acquire_probe_slot(passed_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - with pytest.raises(probe_records.ProbeOperationError) as missing_contact: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_PASSED, - checks=probe_contract.PROOF_CHECKS[proof], - reason=None, - duration_ms=1, - finished_at=FIXED_TS, - ) - - _assert_probe_error(missing_contact, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - not_run_journal = tmp_path / "not-run" - with acquire_probe_slot(not_run_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.dispatch_contact(proof, lambda: None) - with pytest.raises(probe_records.ProbeOperationError) as consumed_not_run: - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_NOT_RUN, - checks=(), - reason=probe_contract.REASON_CANCELLED, - duration_ms=None, - finished_at=FIXED_TS, - ) - - _assert_probe_error(consumed_not_run, probe_contract.STABLE_ERROR_INTERNAL_ERROR) - - clean_not_run_journal = tmp_path / "clean-not-run" - with acquire_probe_slot(clean_not_run_journal, run_id=RUN_ID) as slot: - writer = _begin(slot) - writer.write_proof_terminal( - proof=proof, - state=probe_contract.PROOF_STATE_NOT_RUN, - checks=(), - reason=probe_contract.REASON_CANCELLED, - duration_ms=None, - finished_at=FIXED_TS, - ) - writer.write_attempt_terminal(finished_at=FIXED_TS) diff --git a/tests/sandbox_profile/test_probe_truth_table.py b/tests/sandbox_profile/test_probe_truth_table.py deleted file mode 100644 index 258a03eb1..000000000 --- a/tests/sandbox_profile/test_probe_truth_table.py +++ /dev/null @@ -1,112 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -from solstone.think.sandbox_profile import probe_contract, probe_records -from tests.sandbox_profile import proof_record - - -def _proof(**overrides): - payload = proof_record(**overrides) - return probe_records.validate_proof_terminal_payload(payload) - - -def test_cleanup_unverified_precedes_every_other_reason() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - records = [ - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.REASON_CLEANUP_UNVERIFIED, - ), - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.REASON_CANCELLED, - ), - ] - - assert probe_records.derive_attempt_terminal(records) == ( - probe_contract.ATTEMPT_STATE_DEGRADED, - probe_contract.REASON_CLEANUP_UNVERIFIED, - ) - - -def test_valid_cancelled_suffix_maps_to_cancelled() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - records = [ - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.REASON_CANCELLED, - ), - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_NOT_RUN, - reason=probe_contract.REASON_CANCELLED, - duration_ms=None, - ), - ] - - assert probe_records.derive_attempt_terminal(records) == ( - probe_contract.ATTEMPT_STATE_CANCELLED, - probe_contract.REASON_CANCELLED, - ) - - -def test_internal_error_precedes_generic_proof_failure() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - records = [ - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.REASON_INTERNAL_ERROR, - ), - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - ), - ] - - assert probe_records.derive_attempt_terminal(records) == ( - probe_contract.ATTEMPT_STATE_ERROR, - probe_contract.REASON_INTERNAL_ERROR, - ) - - -def test_generic_failed_row_maps_to_proof_failed() -> None: - proof = probe_contract.CAPABILITY_ORDER[0] - records = [ - _proof( - proof=proof, - state=probe_contract.PROOF_STATE_FAILED, - reason=probe_contract.PROOF_SPECIFIC_REASONS[proof][0], - ) - ] - - assert probe_records.derive_attempt_terminal(records) == ( - probe_contract.ATTEMPT_STATE_DEGRADED, - probe_contract.ATTEMPT_TERMINAL_REASON_PROOF_FAILED, - ) - - -def test_all_passed_maps_to_ok_null() -> None: - records = [_proof(proof=probe_contract.CAPABILITY_ORDER[0])] - - assert probe_records.derive_attempt_terminal(records) == ( - probe_contract.ATTEMPT_STATE_OK, - None, - ) - - -def test_record_write_failed_is_not_a_record_reason_or_state() -> None: - flat_values = set(probe_contract.RECORD_TYPES) - flat_values.update(probe_contract.PROOF_TERMINAL_STATES) - flat_values.update(probe_contract.ATTEMPT_TERMINAL_STATES) - flat_values.update(probe_contract.PROOF_REASON_POOL) - flat_values.update(probe_contract.COMMON_REASONS) - flat_values.update(probe_contract.ATTEMPT_TERMINAL_REASONS) - - assert probe_contract.STABLE_ERROR_RECORD_WRITE_FAILED not in flat_values diff --git a/tests/sandbox_profile/test_redaction.py b/tests/sandbox_profile/test_redaction.py deleted file mode 100644 index 5e056f80b..000000000 --- a/tests/sandbox_profile/test_redaction.py +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import sys - -from tests.sandbox_profile import ( - invoke, - output_json, - prepare_ok, - sandbox_journal, - scout_payload, -) - - -def test_payload_secret_absent_from_argv_output_stderr_logs_and_exception( - tmp_path, - monkeypatch, - caplog, -) -> None: - secret = "recognizable-secret-google-key" - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - def fail_with_secret(_payload): - raise RuntimeError(secret) - - monkeypatch.setattr( - "solstone.think.services.scout.provision_scout_handoff", - fail_with_secret, - ) - result = invoke( - ["apply", "scout", "--json"], input_text=json.dumps(scout_payload(secret)) - ) - body = output_json(result) - - assert result.exit_code == 1 - assert body["error"]["code"] == "internal_error" - assert secret not in result.output - assert secret not in caplog.text - assert secret not in " ".join(sys.argv) - assert result.exception is None or secret not in str(result.exception) - - -def test_human_output_is_redacted_text_not_a_second_json_object( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - - result = invoke(["status", "--human"]) - - assert result.exit_code == 0 - assert result.output.startswith("action: status\n") - assert not result.output.lstrip().startswith("{") diff --git a/tests/sandbox_profile/test_scout_provider_probe.py b/tests/sandbox_profile/test_scout_provider_probe.py deleted file mode 100644 index 17fe4fae1..000000000 --- a/tests/sandbox_profile/test_scout_provider_probe.py +++ /dev/null @@ -1,1378 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import hashlib -import json -import logging -import os -import shutil -import subprocess -import sys -import time -from pathlib import Path -from typing import Any - -import httpx -import pytest - -from solstone.think.sandbox_profile import probe_contract -from solstone.think.sandbox_profile import scout_provider_probe as probe -from tests.sandbox_profile import ( - invoke, - prepare_ok, - read_json, - sandbox_journal, - scout_payload, - write_attempt_dir, -) - -CANARY_KEY = "scout-key-canary-value" -CANARY_DISPATCH_TOKEN = "dispatch-token-canary" -CANARY_ACCOUNT_ID = "acct-scout-canary" -CANARY_NONCE = "nonce-canary-value" -CANARY_INPUT_TOKENS = 1_234_567 -CANARY_OUTPUT_TOKENS = 7_654_321 -CANARY_USAGE_STRINGS = (str(CANARY_INPUT_TOKENS), str(CANARY_OUTPUT_TOKENS)) -CANARY_KEY_FINGERPRINT = hashlib.sha256(CANARY_KEY.encode("utf-8")).hexdigest() -CANARY_MODEL_OUTPUT = json.dumps( - {"nonce": CANARY_NONCE}, - separators=(",", ":"), -) -CANARY_REQUEST_URL = ( - "https://generativelanguage.googleapis.com/v1alpha/models/" - "gemini-3.5-flash:generateContent" -) -CANARIES = ( - CANARY_KEY, - "x-goog-api-key", - CANARY_NONCE, - probe.scout_prompt(CANARY_NONCE), - CANARY_MODEL_OUTPUT, - CANARY_DISPATCH_TOKEN, - CANARY_ACCOUNT_ID, - CANARY_KEY_FINGERPRINT, - "gemini-3.5-flash", - "gemini", - CANARY_REQUEST_URL, - *CANARY_USAGE_STRINGS, -) - - -def _ready_scout_journal(tmp_path: Path, monkeypatch) -> tuple[Path, Path]: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - payload = scout_payload(CANARY_KEY) - payload["dispatch_token"] = CANARY_DISPATCH_TOKEN - payload["account_id"] = CANARY_ACCOUNT_ID - apply = invoke(["apply", "scout", "--json"], input_text=json.dumps(payload)) - assert apply.exit_code == 0, apply.output - return journal, write_attempt_dir(journal) - - -# The pristine-parent absence invariant — that proving Scout pulls no provider -# module into the calling interpreter — is only meaningful when that interpreter -# started pristine, which a shared pytest worker never is. Deleting the modules to -# manufacture that precondition corrupted every later test in the worker: a -# re-import rebuilds openhands' classes, so pydantic then rejects a live ``LLM`` as -# not an ``LLM``. The invariant is proved at full strength in a dedicated -# subprocess instead (see ``test_pristine_parent_imports_no_provider_modules``); -# in-process checks stay scoped to state this process legitimately owns. -def _snapshot(journal: Path) -> tuple[dict[str, str], bytes]: - return dict(os.environ), (journal / "config" / "journal.json").read_bytes() - - -def _assert_snapshot_unchanged( - journal: Path, - snapshot: tuple[dict[str, str], bytes], -) -> None: - before_env, before_config = snapshot - assert dict(os.environ) == before_env - assert (journal / "config" / "journal.json").read_bytes() == before_config - - -def _assert_attempt_empty(attempt: Path) -> None: - assert list(attempt.iterdir()) == [] - - -def _assert_canaries_absent(text: str) -> None: - for canary in CANARIES: - assert canary not in text - - -def _assert_outcome_canaries_absent(outcome: dict[str, object]) -> None: - _assert_canaries_absent(json.dumps(outcome, sort_keys=True, default=str)) - - -def _assert_child_env_canary_clean(env: dict[str, str]) -> None: - expected = { - "HOME", - "TMPDIR", - "XDG_CACHE_HOME", - "XDG_CONFIG_HOME", - "XDG_DATA_HOME", - "XDG_STATE_HOME", - "LITELLM_MODE", - "LITELLM_LOCAL_MODEL_COST_MAP", - } - assert set(env) == expected - for key, value in env.items(): - _assert_canaries_absent(key) - _assert_canaries_absent(value) - for forbidden in ( - "SSL_CERT_FILE", - "REQUESTS_CA_BUNDLE", - "CURL_CA_BUNDLE", - "GEMINI_API_BASE", - "GOOGLE_API_KEY", - ): - assert forbidden not in env - assert not any(key.endswith(("_PROXY", "_proxy")) for key in env) - - -def _assert_child_stdin_asymmetry(frame: bytes) -> None: - assert CANARY_KEY.encode("utf-8") in frame - assert CANARY_NONCE.encode("utf-8") in frame - assert CANARY_DISPATCH_TOKEN.encode("utf-8") not in frame - assert CANARY_ACCOUNT_ID.encode("utf-8") not in frame - assert CANARY_KEY_FINGERPRINT.encode("utf-8") not in frame - - -def _assert_proof_path_names_canary_clean(captured: dict[str, Any]) -> None: - paths = [Path(captured["cwd"]), Path(captured["cwd"]).parent] - paths.extend(Path(value) for value in captured["env"].values()) - for path in paths: - _assert_canaries_absent(path.name) - _assert_canaries_absent(str(path)) - - -def _assert_surviving_attempt_files_canary_clean(attempt: Path) -> None: - # The applied Scout key retained in journal/config/journal.json is sanctioned. - # This sweep is scoped to proof-created attempt state only. - for path in attempt.rglob("*"): - _assert_canaries_absent(path.name) - _assert_canaries_absent(str(path)) - if path.is_file(): - data = path.read_bytes() - for canary in CANARIES: - assert canary.encode("utf-8") not in data - - -def _forbid_spawn(_containment: probe.ScoutContainment): - raise AssertionError("Scout provider proof spawned a child") - - -def _write_json(path: Path, payload: dict[str, Any]) -> None: - path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - - -def _ok_child_code() -> str: - return f""" -import hashlib -import sys -from solstone.think.sandbox_profile.scout_provider_probe import ( - FRAME_PROTOCOL_VERSION, - STDIN_FRAME_MAX_BYTES, - STDOUT_FRAME_MAX_BYTES, - decode_frame, - encode_frame, -) - -payload = decode_frame(sys.stdin.buffer.read(), cap=STDIN_FRAME_MAX_BYTES) -nonce = payload["nonce"] -out = {{ - "protocol_version": FRAME_PROTOCOL_VERSION, - "result": "ok", - "nonce_sha256": hashlib.sha256(nonce.encode("utf-8")).hexdigest(), - "finish_reason": "stop", - "usage": {{ - "input_tokens": {CANARY_INPUT_TOKENS}, - "output_tokens": {CANARY_OUTPUT_TOKENS}, - }}, -}} -sys.stdout.buffer.write(encode_frame(out, cap=STDOUT_FRAME_MAX_BYTES)) -sys.stdout.buffer.flush() -""" - - -def _stderr_flood_child_code() -> str: - return """ -import os -import sys -os.write(2, b"x" * 200000) -sys.stderr.flush() -""" + _ok_child_code() - - -def _sleep_child_code() -> str: - return """ -import time -time.sleep(60) -""" - - -def _silent_child_code() -> str: - return """ -import sys -sys.stdin.buffer.read() -""" - - -def _oversize_stdout_child_code() -> str: - length = probe.STDOUT_FRAME_MAX_BYTES + 1 - return f""" -import sys -sys.stdin.buffer.read() -sys.stdout.buffer.write({length}.to_bytes(4, "big") + b"x" * {length}) -sys.stdout.buffer.flush() -""" - - -def _spawn_code(code: str, captured: dict[str, Any]): - def spawn(containment: probe.ScoutContainment): - captured["env"] = dict(containment.env) - captured["cwd"] = containment.cwd - proc = subprocess.Popen( - [sys.executable, "-c", code], - cwd=containment.cwd, - env=containment.env, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True, - pass_fds=(), - start_new_session=True, - ) - captured["pid"] = proc.pid - return proc - - return spawn - - -def test_parent_success_uses_contained_env(tmp_path, monkeypatch, caplog) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - before_env = dict(os.environ) - before_config = (journal / "config" / "journal.json").read_bytes() - captured: dict[str, Any] = {} - monkeypatch.setattr(probe, "_spawn_child", _spawn_code(_ok_child_code(), captured)) - monkeypatch.setattr(probe, "_new_nonce", lambda: CANARY_NONCE) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - assert outcome["reason"] is None - assert outcome["checks"] == probe_contract.PROOF_CHECKS["scout"] - assert dict(os.environ) == before_env - assert (journal / "config" / "journal.json").read_bytes() == before_config - assert set(captured["env"]) == { - "HOME", - "TMPDIR", - "XDG_CACHE_HOME", - "XDG_CONFIG_HOME", - "XDG_DATA_HOME", - "XDG_STATE_HOME", - "LITELLM_MODE", - "LITELLM_LOCAL_MODEL_COST_MAP", - } - assert captured["env"]["LITELLM_MODE"] == "PRODUCTION" - assert captured["env"]["LITELLM_LOCAL_MODEL_COST_MAP"] == "True" - for forbidden in ( - "PATH", - "GOOGLE_API_KEY", - "SSL_CERT_FILE", - "REQUESTS_CA_BUNDLE", - "CURL_CA_BUNDLE", - "HTTP_PROXY", - "HTTPS_PROXY", - "ALL_PROXY", - ): - assert forbidden not in captured["env"] - assert list(attempt.iterdir()) == [] - _assert_canaries_absent(repr(outcome)) - _assert_canaries_absent(caplog.text) - - -@pytest.mark.parametrize( - "case", - [ - "missing_scout_block", - "missing_recorded_account_id", - "mismatched_account_id", - "mismatched_key_fingerprint_sha256", - "empty_key", - "missing_key", - ], -) -def test_parent_refuses_unowned_scout_without_spawning( - tmp_path, - monkeypatch, - case: str, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - config_path = journal / "config" / "journal.json" - intent_path = journal / "health" / "sandbox-profile" / "intent.json" - config = read_json(config_path) - intent_payload = read_json(intent_path) - - if case == "missing_scout_block": - config["services"].pop("scout", None) - _write_json(config_path, config) - elif case == "missing_recorded_account_id": - intent_payload["observed_at_apply"]["scout"].pop("account_id", None) - _write_json(intent_path, intent_payload) - elif case == "mismatched_account_id": - config["services"]["scout"]["account_id"] = "acct-other" - _write_json(config_path, config) - elif case == "mismatched_key_fingerprint_sha256": - intent_payload["observed_at_apply"]["scout"]["key_fingerprint_sha256"] = ( - "0" * 64 - ) - _write_json(intent_path, intent_payload) - elif case == "empty_key": - config["env"]["GOOGLE_API_KEY"] = "" - _write_json(config_path, config) - elif case == "missing_key": - config["env"].pop("GOOGLE_API_KEY", None) - _write_json(config_path, config) - else: # pragma: no cover - parametrization guard - raise AssertionError(case) - - snapshot = _snapshot(journal) - monkeypatch.setattr(probe, "_spawn_child", _forbid_spawn) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == probe.SCOUT_CHECKS[:0] - _assert_snapshot_unchanged(journal, snapshot) - _assert_attempt_empty(attempt) - - -def test_parent_cancels_before_contact_without_spawning(tmp_path, monkeypatch) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - snapshot = _snapshot(journal) - monkeypatch.setattr(probe, "_spawn_child", _forbid_spawn) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: True, - ) - - assert outcome["reason"] == probe_contract.REASON_CANCELLED - assert outcome["checks"] == probe.SCOUT_CHECKS[:0] - _assert_snapshot_unchanged(journal, snapshot) - _assert_attempt_empty(attempt) - - -def test_parent_cancellation_after_earned_facts_preserves_prefix( - tmp_path, - monkeypatch, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - snapshot = _snapshot(journal) - nonce = "earned-cancel-nonce" - monkeypatch.setattr(probe, "_new_nonce", lambda: nonce) - monkeypatch.setattr(probe, "_spawn_child", lambda _containment: object()) - - def drive(_proc, _frame, _deadline, _work_budget, _cancel_requested): - return probe._ChildDriveResult( - _child_frame( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(nonce.encode("utf-8")).hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": 1, "output_tokens": 1}, - } - ), - None, - ) - - calls = {"count": 0} - - def cancel_requested() -> bool: - calls["count"] += 1 - return calls["count"] > 1 - - monkeypatch.setattr(probe, "_drive_child", drive) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=cancel_requested, - ) - - assert outcome["reason"] == probe_contract.REASON_CANCELLED - assert outcome["checks"] == probe.SCOUT_CHECKS[:] - _assert_snapshot_unchanged(journal, snapshot) - _assert_attempt_empty(attempt) - - -def test_parent_cleanup_unverified_overrides_post_fact_cancellation( - tmp_path, - monkeypatch, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - snapshot = _snapshot(journal) - nonce = "cleanup-cancel-nonce" - monkeypatch.setattr(probe, "_new_nonce", lambda: nonce) - monkeypatch.setattr(probe, "_spawn_child", lambda _containment: object()) - - def drive(_proc, _frame, _deadline, _work_budget, _cancel_requested): - return probe._ChildDriveResult( - _child_frame( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(nonce.encode("utf-8")).hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": 1, "output_tokens": 1}, - } - ), - None, - ) - - calls = {"cancel": 0, "cleanup": 0} - - def cancel_requested() -> bool: - calls["cancel"] += 1 - return calls["cancel"] > 1 - - real_cleanup = probe._cleanup_path_absent - - def cleanup(path: Path, deadline: float) -> bool: - calls["cleanup"] += 1 - cleaned = real_cleanup(path, deadline) - return cleaned and calls["cleanup"] != 2 - - monkeypatch.setattr(probe, "_drive_child", drive) - monkeypatch.setattr(probe, "_cleanup_path_absent", cleanup) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=cancel_requested, - ) - - assert outcome["reason"] == probe_contract.REASON_CLEANUP_UNVERIFIED - assert outcome["checks"] == probe.SCOUT_CHECKS[:] - _assert_snapshot_unchanged(journal, snapshot) - _assert_attempt_empty(attempt) - - -def test_parent_cleanup_unverified_overrides_success(tmp_path, monkeypatch) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - monkeypatch.setattr(probe, "_spawn_child", _spawn_code(_ok_child_code(), {})) - calls = {"count": 0} - - def cleanup(path: Path, deadline: float) -> bool: - calls["count"] += 1 - return calls["count"] == 1 - - monkeypatch.setattr(probe, "_cleanup_path_absent", cleanup) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert outcome["state"] == probe_contract.PROOF_STATE_FAILED - assert outcome["reason"] == probe_contract.REASON_CLEANUP_UNVERIFIED - assert outcome["checks"] == probe_contract.PROOF_CHECKS["scout"] - - -def test_parent_canaries_absent_from_debug_logs_argv_and_outcomes( - tmp_path, - monkeypatch, - caplog, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - config_path = journal / "config" / "journal.json" - outcomes: list[dict[str, object]] = [] - caplog.clear() - - with caplog.at_level(logging.DEBUG): - monkeypatch.setattr(probe, "_spawn_child", _spawn_code(_ok_child_code(), {})) - monkeypatch.setattr(probe, "_new_nonce", lambda: CANARY_NONCE) - outcomes.append( - probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - - monkeypatch.setattr( - probe, - "_spawn_child", - _spawn_code("import sys\nsys.stdout.buffer.write(b'bad-frame')\n", {}), - ) - outcomes.append( - probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - - config = read_json(config_path) - config["env"]["GOOGLE_API_KEY"] = "" - _write_json(config_path, config) - monkeypatch.setattr(probe, "_spawn_child", _forbid_spawn) - outcomes.append( - probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - - assert outcomes[0]["state"] == probe_contract.PROOF_STATE_PASSED - assert outcomes[1]["state"] == probe_contract.PROOF_STATE_FAILED - assert outcomes[2]["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - _assert_canaries_absent(caplog.text) - _assert_canaries_absent(" ".join(sys.argv)) - for outcome in outcomes: - _assert_outcome_canaries_absent(outcome) - - -def test_primitive_exceptions_are_canary_clean() -> None: - exceptions: list[BaseException] = [] - - with pytest.raises(probe.ScoutProbeError) as excinfo: - probe.decode_frame(b"", cap=probe.STDIN_FRAME_MAX_BYTES) - exceptions.append(excinfo.value) - - transport = probe.GeminiSingleRequestTransport() - with pytest.raises(probe.ProbeInternalError) as excinfo: - transport.handle_request( - httpx.Request( - "GET", - CANARY_REQUEST_URL, - headers={"x-goog-api-key": CANARY_KEY}, - content=CANARY_MODEL_OUTPUT.encode("utf-8"), - ) - ) - exceptions.append(excinfo.value) - transport.close() - - inbound = probe.GeminiSingleRequestTransport( - httpx.MockTransport( - lambda request: httpx.Response( - 200, - headers={"x-test": "x" * (probe.INBOUND_RAW_HEADER_MAX_BYTES + 1)}, - request=request, - ) - ) - ) - with pytest.raises(probe.ProbeResponseInvalid) as excinfo: - inbound.handle_request( - httpx.Request( - "POST", - CANARY_REQUEST_URL, - headers={"x-goog-api-key": CANARY_KEY}, - content=b"{}", - ) - ) - exceptions.append(excinfo.value) - inbound.close() - - for exc in exceptions: - assert exc.__cause__ is None - _assert_canaries_absent(str(exc)) - _assert_canaries_absent(repr(exc)) - - -def test_parent_canaries_absent_from_child_env_stdin_paths_and_attempt_files( - tmp_path, - monkeypatch, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - config_path = journal / "config" / "journal.json" - real_drive_child = probe._drive_child - pass_capture: dict[str, Any] = {} - failure_capture: dict[str, Any] = {} - - def capture_drive(captured: dict[str, Any]): - def drive(proc, frame, deadline, work_budget, cancel_requested): - captured["stdin"] = frame - return real_drive_child( - proc, - frame, - deadline, - work_budget, - cancel_requested, - ) - - return drive - - monkeypatch.setattr(probe, "_new_nonce", lambda: CANARY_NONCE) - monkeypatch.setattr( - probe, "_spawn_child", _spawn_code(_ok_child_code(), pass_capture) - ) - monkeypatch.setattr(probe, "_drive_child", capture_drive(pass_capture)) - pass_outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert pass_outcome["state"] == probe_contract.PROOF_STATE_PASSED - _assert_child_env_canary_clean(pass_capture["env"]) - _assert_child_stdin_asymmetry(pass_capture["stdin"]) - _assert_proof_path_names_canary_clean(pass_capture) - _assert_surviving_attempt_files_canary_clean(attempt) - - monkeypatch.setattr( - probe, - "_spawn_child", - _spawn_code( - "import sys\nsys.stdout.buffer.write(b'bad-frame')\n", failure_capture - ), - ) - monkeypatch.setattr(probe, "_drive_child", capture_drive(failure_capture)) - failure_outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert failure_outcome["state"] == probe_contract.PROOF_STATE_FAILED - _assert_child_env_canary_clean(failure_capture["env"]) - _assert_child_stdin_asymmetry(failure_capture["stdin"]) - _assert_proof_path_names_canary_clean(failure_capture) - _assert_surviving_attempt_files_canary_clean(attempt) - - config = read_json(config_path) - config["env"]["GOOGLE_API_KEY"] = "" - _write_json(config_path, config) - monkeypatch.setattr(probe, "_spawn_child", _forbid_spawn) - refusal_outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert refusal_outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - _assert_surviving_attempt_files_canary_clean(attempt) - - -def test_deadline_reserves_fit_absolute_bound() -> None: - assert ( - probe.WORK_CUTOFF_S - + probe.TERM_GRACE_S - + probe.KILL_GRACE_S - + probe.ABSENCE_GRACE_S - <= probe.ABSOLUTE_DEADLINE_S - ) - - -@pytest.mark.parametrize( - "code", - [ - _silent_child_code(), - _oversize_stdout_child_code(), - ], -) -def test_parent_frame_failures_return_stable_reason_and_leave_attempt_empty( - tmp_path, - monkeypatch, - code: str, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - monkeypatch.setattr(probe, "_spawn_child", _spawn_code(code, {})) - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert outcome["reason"] == probe_contract.REASON_INTERNAL_ERROR - _assert_attempt_empty(attempt) - - -def test_parent_timeout_reaps_process_group(tmp_path, monkeypatch) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - captured: dict[str, Any] = {} - monkeypatch.setattr( - probe, "_spawn_child", _spawn_code(_sleep_child_code(), captured) - ) - monkeypatch.setattr(probe, "ABSOLUTE_DEADLINE_S", 0.5) - monkeypatch.setattr(probe, "WORK_CUTOFF_S", 0.1) - monkeypatch.setattr(probe, "TERM_GRACE_S", 0.1) - monkeypatch.setattr(probe, "KILL_GRACE_S", 0.1) - monkeypatch.setattr(probe, "ABSENCE_GRACE_S", 0.1) - started = time.monotonic() - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert time.monotonic() - started < 1.5 - assert outcome["reason"] == probe_contract.REASON_DEADLINE_EXCEEDED - with pytest.raises(ProcessLookupError): - os.killpg(captured["pid"], 0) - assert list(attempt.iterdir()) == [] - - -def test_parent_concurrently_drains_stderr_without_deadlock( - tmp_path, monkeypatch -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - monkeypatch.setattr( - probe, "_spawn_child", _spawn_code(_stderr_flood_child_code(), {}) - ) - started = time.monotonic() - - outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert time.monotonic() - started < 5 - assert outcome["reason"] == probe_contract.REASON_INTERNAL_ERROR - assert list(attempt.iterdir()) == [] - - -def _child_frame(payload: dict[str, object]) -> bytes: - return probe.encode_frame(payload, cap=probe.STDOUT_FRAME_MAX_BYTES) - - -@pytest.mark.parametrize( - ("payload", "reason", "prefix"), - [ - ( - {"protocol_version": 1, "result": probe_contract.REASON_REMOTE_REJECTED}, - probe_contract.REASON_REMOTE_REJECTED, - probe.SCOUT_CHECKS[:0], - ), - ( - {"protocol_version": 1, "result": probe_contract.REASON_RESPONSE_INVALID}, - probe_contract.REASON_RESPONSE_INVALID, - probe.SCOUT_CHECKS[:0], - ), - ( - {"protocol_version": 1, "result": probe_contract.REASON_INTERNAL_ERROR}, - probe_contract.REASON_INTERNAL_ERROR, - probe.SCOUT_CHECKS[:0], - ), - ( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(b"other").hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": 1, "output_tokens": 1}, - }, - probe_contract.REASON_CONTENT_MISMATCH, - probe.SCOUT_CHECKS[:1], - ), - ( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(b"nonce").hexdigest(), - "finish_reason": "length", - "usage": {"input_tokens": 1, "output_tokens": 1}, - }, - probe_contract.REASON_RESPONSE_INVALID, - probe.SCOUT_CHECKS[:2], - ), - ( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(b"nonce").hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": 1, "output_tokens": 0}, - }, - probe_contract.REASON_USAGE_INVALID, - probe.SCOUT_CHECKS[:3], - ), - ( - { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(b"nonce").hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": True, "output_tokens": 1}, - }, - probe_contract.REASON_USAGE_INVALID, - probe.SCOUT_CHECKS[:3], - ), - ], -) -def test_state_machine_failed_reason_prefixes(payload, reason, prefix) -> None: - outcome, checks = probe._outcome_from_child_frame( - _child_frame(payload), - nonce="nonce", - start=time.monotonic(), - ) - legal = set(probe_contract.PROOF_SPECIFIC_REASONS["scout"]) | set( - probe_contract.FAILED_COMMON_REASONS - ) - - assert outcome.reason == reason - assert outcome.reason in legal - assert outcome.state == probe_contract.PROOF_STATE_FAILED - assert checks == tuple(prefix) - assert outcome.checks == tuple(prefix) - - -def test_state_machine_success_uses_contract_checks() -> None: - payload = { - "protocol_version": 1, - "result": "ok", - "nonce_sha256": hashlib.sha256(b"nonce").hexdigest(), - "finish_reason": "stop", - "usage": {"input_tokens": 1, "output_tokens": 1}, - } - - outcome, checks = probe._outcome_from_child_frame( - _child_frame(payload), - nonce="nonce", - start=time.monotonic(), - ) - - assert outcome.state == probe_contract.PROOF_STATE_PASSED - assert outcome.reason is None - assert checks == probe_contract.PROOF_CHECKS["scout"] - assert outcome.checks == probe_contract.PROOF_CHECKS["scout"] - - -def _child_env(tmp_path: Path) -> tuple[dict[str, str], Path]: - root = tmp_path / "child" - paths = { - "HOME": root / "home", - "TMPDIR": root / "tmp", - "XDG_CACHE_HOME": root / "cache", - "XDG_CONFIG_HOME": root / "config", - "XDG_DATA_HOME": root / "data", - "XDG_STATE_HOME": root / "state", - } - cwd = root / "cwd" - for path in (*paths.values(), cwd): - path.mkdir(mode=0o700, parents=True, exist_ok=True) - return ( - { - "LITELLM_MODE": "PRODUCTION", - "LITELLM_LOCAL_MODEL_COST_MAP": "True", - **{key: str(value) for key, value in paths.items()}, - }, - cwd, - ) - - -def _run_real_child(tmp_path: Path, data: bytes) -> dict[str, Any]: - env, cwd = _child_env(tmp_path) - root = cwd.parent - try: - proc = subprocess.Popen( - [ - sys.executable, - "-m", - "solstone.think.sandbox_profile.scout_provider_child", - ], - cwd=cwd, - env=env, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True, - pass_fds=(), - start_new_session=True, - ) - stdout, stderr = proc.communicate(data, timeout=10) - assert proc.returncode == 0, stderr - assert stderr == b"" - return probe.decode_frame(stdout, cap=probe.STDOUT_FRAME_MAX_BYTES) - finally: - shutil.rmtree(root, ignore_errors=True) - assert not root.exists() - - -def test_real_child_rejects_malformed_duplicate_and_oversize_frames(tmp_path) -> None: - malformed_payload = b"{" - malformed = len(malformed_payload).to_bytes(4, "big") + malformed_payload - truncated_payload = b"{}" - truncated = (len(truncated_payload) + 1).to_bytes(4, "big") + truncated_payload - duplicate_payload = b'{"protocol_version":1,"protocol_version":1}' - duplicate = len(duplicate_payload).to_bytes(4, "big") + duplicate_payload - oversize = b"x" * (probe.STDIN_FRAME_MAX_BYTES + 5) - - assert ( - _run_real_child(tmp_path, malformed)["result"] - == probe_contract.REASON_INTERNAL_ERROR - ) - assert ( - _run_real_child(tmp_path, truncated)["result"] - == probe_contract.REASON_INTERNAL_ERROR - ) - assert ( - _run_real_child(tmp_path, duplicate)["result"] - == probe_contract.REASON_INTERNAL_ERROR - ) - assert ( - _run_real_child(tmp_path, oversize)["result"] - == probe_contract.REASON_INTERNAL_ERROR - ) - - -def test_transport_enforces_single_official_request_without_leaking_header() -> None: - seen: list[httpx.Request] = [] - - def handler(request: httpx.Request) -> httpx.Response: - seen.append(request) - return httpx.Response(200, json={"ok": True}, request=request) - - transport = probe.GeminiSingleRequestTransport(httpx.MockTransport(handler)) - request = httpx.Request( - "POST", - "https://generativelanguage.googleapis.com/v1alpha/models/" - "gemini-3.5-flash:generateContent", - headers={"x-goog-api-key": "dummy-secret"}, - content=b"{}", - ) - - response = transport.handle_request(request) - assert response.status_code == 200 - assert transport.request_count == 1 - assert len(seen) == 1 - with pytest.raises(probe.ProbeInternalError) as exc: - transport.handle_request(request) - assert "dummy-secret" not in str(exc.value) - assert "x-goog-api-key" not in str(exc.value) - - -def test_transport_maps_response_caps_and_outbound_caps() -> None: - outbound = probe.GeminiSingleRequestTransport( - httpx.MockTransport(lambda request: httpx.Response(200, request=request)) - ) - request = httpx.Request( - "POST", - "https://generativelanguage.googleapis.com/v1alpha/models/" - "gemini-3.5-flash:generateContent", - content=b"x" * (probe.OUTBOUND_BODY_MAX_BYTES + 1), - ) - with pytest.raises(probe.ProbeInternalError): - outbound.handle_request(request) - - inbound = probe.GeminiSingleRequestTransport( - httpx.MockTransport( - lambda request: httpx.Response( - 200, - headers={"x-test": "x" * (probe.INBOUND_RAW_HEADER_MAX_BYTES + 1)}, - request=request, - ) - ) - ) - request = httpx.Request( - "POST", - "https://generativelanguage.googleapis.com/v1alpha/models/" - "gemini-3.5-flash:generateContent", - content=b"{}", - ) - with pytest.raises(probe.ProbeResponseInvalid): - inbound.handle_request(request) - - -# Isolated-subprocess proofs. -# -# Two claims can only be proved honestly in an interpreter this suite does not -# share. The absence claim needs a pristine module table as its precondition; the -# transport claim deliberately imports openhands and pins LiteLLM's import-time -# mode, which would then outlive the test in a shared worker. Both run in a child -# built from a constructed environment, with bounded output and a bounded -# deadline, and both leave the parent's module set, class identities, and -# environment untouched. Neither contacts a provider: the absence driver never -# imports one, and the transport driver answers itself from a mock transport. - -PROOF_SUBPROCESS_TIMEOUT_S = 300.0 -PROOF_SUBPROCESS_STDOUT_MAX_BYTES = 64 * 1024 -PROOF_SUBPROCESS_STDERR_REPORT_BYTES = 4096 - -_PROVIDER_ABSENCE_DRIVER = """ -import json -import os -import subprocess -import sys -from pathlib import Path - - -def provider_modules(): - return sorted( - name - for name in sys.modules - if name == "openhands" - or name.startswith("openhands.") - or name == "litellm" - or name.startswith("litellm.") - ) - - -request = json.loads(sys.stdin.read()) -verdict = {"before_import": provider_modules()} - -from solstone.think.sandbox_profile import scout_provider_probe as probe - -verdict["after_import"] = provider_modules() - -journal = Path(request["journal"]) -attempt = Path(request["attempt"]) -scenario = request["scenario"] -config_path = journal / "config" / "journal.json" -before_env = dict(os.environ) -before_config = config_path.read_bytes() - -if scenario == "success": - child_code = request["child_code"] - - def spawn(containment): - return subprocess.Popen( - [sys.executable, "-c", child_code], - cwd=containment.cwd, - env=containment.env, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True, - pass_fds=(), - start_new_session=True, - ) - - probe._spawn_child = spawn - probe._new_nonce = lambda: request["nonce"] - cancelled = False -else: - - def spawn(_containment): - raise AssertionError("Scout provider proof spawned a child") - - probe._spawn_child = spawn - cancelled = scenario == "cancel_before_contact" - -outcome = probe.prove_scout_provider( - journal, - attempt_dir=attempt, - cancel_requested=lambda: cancelled, -) - -verdict["after_proof"] = provider_modules() -verdict["state"] = outcome["state"] -verdict["reason"] = outcome["reason"] -verdict["checks"] = list(outcome["checks"]) -verdict["env_unchanged"] = dict(os.environ) == before_env -verdict["config_unchanged"] = config_path.read_bytes() == before_config -verdict["attempt_empty"] = list(attempt.iterdir()) == [] -sys.stdout.write(json.dumps(verdict)) -sys.stdout.flush() -""" - -_OPENHANDS_TRANSPORT_DRIVER = """ -import json -import os -import sys - -import httpx - -from solstone.think.providers import openhands -from solstone.think.sandbox_profile import scout_provider_child as child -from solstone.think.sandbox_profile import scout_provider_probe as probe - -request_payload = json.loads(sys.stdin.read()) -nonce = request_payload["nonce"] - -captured = {} -real_build = openhands._build_generate_llm -real_call_kwargs = openhands._generate_call_kwargs - - -def capture_build(*args, **kwargs): - captured["build_kwargs"] = dict(kwargs) - return real_build(*args, **kwargs) - - -def capture_call_kwargs(*args, **kwargs): - result = real_call_kwargs(*args, **kwargs) - captured["call_kwargs"] = dict(result) - return result - - -openhands._build_generate_llm = capture_build -openhands._generate_call_kwargs = capture_call_kwargs - - -def handler(request): - captured["request"] = request - captured["body"] = json.loads(request.content.decode("utf-8")) - return httpx.Response( - 200, - json={ - "candidates": [ - { - "content": { - "role": "model", - "parts": [{"text": json.dumps({"nonce": nonce})}], - }, - "finishReason": "STOP", - } - ], - "usageMetadata": { - "promptTokenCount": 3, - "candidatesTokenCount": 2, - "totalTokenCount": 5, - }, - "modelVersion": "gemini-3.5-flash", - }, - request=request, - ) - - -transport = probe.GeminiSingleRequestTransport(httpx.MockTransport(handler)) -result = child._run_completion( - api_key=request_payload["api_key"], - nonce=nonce, - timeout_s=request_payload["timeout_s"], - transport=transport, -) - -sent = captured["request"] -body = captured["body"] -build_kwargs = captured["build_kwargs"] -call_kwargs = captured["call_kwargs"] -generation_config = body["generationConfig"] -verdict = { - "result": result["result"], - "request_count": transport.request_count, - "method": sent.method, - "host": sent.url.host, - "path": sent.url.path, - "api_key_header": sent.headers.get("x-goog-api-key"), - "build_max_output_tokens": build_kwargs["max_output_tokens"], - "build_thinking_budget": build_kwargs["thinking_budget"], - "build_num_retries": build_kwargs["num_retries"], - "build_timeout_s": build_kwargs["timeout_s"], - "call_temperature": call_kwargs["temperature"], - "call_thinking": call_kwargs["thinking"], - "body_temperature": generation_config["temperature"], - "body_max_output_tokens": generation_config["max_output_tokens"], - "body_schema_properties": generation_config["response_json_schema"]["properties"], - "body_prompt": body["contents"][0]["parts"][0]["text"], - "litellm_mode": os.environ.get("LITELLM_MODE"), -} -sys.stdout.write(json.dumps(verdict)) -sys.stdout.flush() -""" - - -def _provider_module_table() -> dict[str, Any]: - """Live provider modules by name, kept as objects so identity is comparable.""" - return { - name: module - for name, module in list(sys.modules.items()) - if name == "openhands" - or name.startswith("openhands.") - or name == "litellm" - or name.startswith("litellm.") - } - - -def _isolated_env( - tmp_path: Path, extra: dict[str, str] | None = None -) -> dict[str, str]: - """Build a child environment from nothing, rooted entirely inside tmp_path.""" - root = tmp_path / "driver-env" - paths = { - "HOME": root / "home", - "TMPDIR": root / "tmp", - "XDG_CACHE_HOME": root / "cache", - "XDG_CONFIG_HOME": root / "config", - "XDG_DATA_HOME": root / "data", - "XDG_STATE_HOME": root / "state", - } - for path in paths.values(): - path.mkdir(mode=0o700, parents=True, exist_ok=True) - env = {key: str(value) for key, value in paths.items()} - env.update(extra or {}) - return env - - -def _run_proof_subprocess( - driver: str, - payload: dict[str, Any], - *, - env: dict[str, str], -) -> tuple[dict[str, Any], str]: - before = _provider_module_table() - before_env = dict(os.environ) - proc = subprocess.run( - [sys.executable, "-c", driver], - input=json.dumps(payload).encode("utf-8"), - capture_output=True, - timeout=PROOF_SUBPROCESS_TIMEOUT_S, - env=env, - check=False, - ) - stderr_tail = proc.stderr[-PROOF_SUBPROCESS_STDERR_REPORT_BYTES:].decode( - "utf-8", "replace" - ) - assert proc.returncode == 0, stderr_tail - assert len(proc.stdout) <= PROOF_SUBPROCESS_STDOUT_MAX_BYTES - # The parent must come back exactly as it went in: same module objects, not - # merely the same names, and the same environment. - assert _provider_module_table() == before - assert dict(os.environ) == before_env - return json.loads(proc.stdout.decode("utf-8")), stderr_tail - - -@pytest.mark.timeout(600) -@pytest.mark.parametrize( - ("scenario", "expected_state", "expected_reason"), - [ - ("success", probe_contract.PROOF_STATE_PASSED, None), - ( - "refuse", - probe_contract.PROOF_STATE_FAILED, - probe_contract.REASON_CAPABILITY_NOT_READY, - ), - ( - "cancel_before_contact", - probe_contract.PROOF_STATE_FAILED, - probe_contract.REASON_CANCELLED, - ), - ], -) -def test_pristine_parent_imports_no_provider_modules( - tmp_path, - monkeypatch, - scenario: str, - expected_state: str, - expected_reason: str | None, -) -> None: - journal, attempt = _ready_scout_journal(tmp_path, monkeypatch) - if scenario == "refuse": - config_path = journal / "config" / "journal.json" - config = read_json(config_path) - config["services"]["scout"]["account_id"] = "acct-other" - _write_json(config_path, config) - - verdict, stderr_tail = _run_proof_subprocess( - _PROVIDER_ABSENCE_DRIVER, - { - "journal": str(journal), - "attempt": str(attempt), - "scenario": scenario, - "nonce": CANARY_NONCE, - "child_code": _ok_child_code(), - }, - env=_isolated_env(tmp_path), - ) - - # The precondition a shared worker cannot offer, asserted rather than assumed. - assert verdict["before_import"] == [] - # Full strength: nothing present, not merely nothing new. - assert verdict["after_import"] == [] - assert verdict["after_proof"] == [] - assert verdict["state"] == expected_state - assert verdict["reason"] == expected_reason - assert verdict["env_unchanged"] is True - assert verdict["config_unchanged"] is True - assert verdict["attempt_empty"] is True - if scenario == "success": - assert verdict["checks"] == list(probe_contract.PROOF_CHECKS["scout"]) - else: - assert verdict["checks"] == [] - _assert_canaries_absent(stderr_tail) - - -@pytest.mark.timeout(600) -def test_real_openhands_completion_uses_injected_gemini_transport(tmp_path) -> None: - nonce = "nonce-for-provider" - api_key = "dummy-google-key" - verdict, _stderr = _run_proof_subprocess( - _OPENHANDS_TRANSPORT_DRIVER, - {"nonce": nonce, "api_key": api_key, "timeout_s": 30}, - # Set in the child's environment rather than the parent's: LiteLLM reads - # both at import time, and PRODUCTION plus the bundled cost map are what - # keep this offline (no GEMINI_API_BASE redirect, no cost-map fetch). - env=_isolated_env( - tmp_path, - {"LITELLM_MODE": "PRODUCTION", "LITELLM_LOCAL_MODEL_COST_MAP": "True"}, - ), - ) - - assert verdict["result"] == "ok" - assert verdict["request_count"] == 1 - assert verdict["method"] == "POST" - assert verdict["host"] == "generativelanguage.googleapis.com" - assert verdict["path"] == "/v1alpha/models/gemini-3.5-flash:generateContent" - assert verdict["api_key_header"] == api_key - assert verdict["litellm_mode"] == "PRODUCTION" - assert verdict["build_max_output_tokens"] == 512 - assert verdict["build_thinking_budget"] == 0 - assert verdict["build_num_retries"] == 0 - assert verdict["build_timeout_s"] == 30 - assert verdict["call_temperature"] == 0 - assert verdict["call_thinking"] == {"type": "disabled", "budget_tokens": 0} - assert verdict["body_temperature"] == 0 - assert verdict["body_max_output_tokens"] == 512 - assert verdict["body_schema_properties"] == {"nonce": {"type": "string"}} - assert verdict["body_prompt"] == probe.scout_prompt(nonce) - - -@pytest.mark.timeout(600) -def test_scout_proof_leaves_openhands_shape_tests_passing(tmp_path) -> None: - """Deterministic guard for the isolation defect these subprocesses fixed. - - Deleting provider modules from the shared interpreter used to pass this - file's own tests while reddening whichever worker inherited the corrupted - module table. Ordering is the whole point, so pin it: run this entire file - first, then the suites that broke. Scoping the run to the whole file rather - than a couple of tests is deliberate — it is what catches an in-process - ``sys.modules`` deletion reintroduced anywhere in it. Only this test is - deselected, since it is what spawns the nested run. - """ - repo_root = Path(__file__).resolve().parents[2] - scout = "tests/sandbox_profile/test_scout_provider_probe.py" - proc = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - scout, - "--deselect", - f"{scout}::{test_scout_proof_leaves_openhands_shape_tests_passing.__name__}", - "tests/test_openhands_sdk_shape.py", - "tests/test_cogitate_local_condenser.py", - "-q", - "-p", - "no:randomly", - ], - cwd=repo_root, - capture_output=True, - timeout=PROOF_SUBPROCESS_TIMEOUT_S, - env=_isolated_env( - tmp_path, - {"SOLSTONE_JOURNAL": str(repo_root / "tests" / "fixtures" / "journal")}, - ), - check=False, - ) - tail = proc.stdout[-PROOF_SUBPROCESS_STDERR_REPORT_BYTES:].decode( - "utf-8", "replace" - ) - assert proc.returncode == 0, tail diff --git a/tests/sandbox_profile/test_spb_backup_probe.py b/tests/sandbox_profile/test_spb_backup_probe.py deleted file mode 100644 index 27ee26d0d..000000000 --- a/tests/sandbox_profile/test_spb_backup_probe.py +++ /dev/null @@ -1,1911 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import logging -import os -from collections.abc import Callable -from dataclasses import dataclass -from datetime import UTC, datetime, timedelta -from pathlib import Path -from typing import Any - -import pytest - -from solstone.think.backup import runner as backup_runner -from solstone.think.backup.hosted import ( - HostedBinding, - HostedCredentials, - HostedCredsUnavailable, -) -from solstone.think.sandbox_profile import probe_contract -from solstone.think.sandbox_profile import spb_backup_probe as probe -from tests.sandbox_profile import ( - ATTEMPT_ID, - invoke, - prepare_ok, - sandbox_journal, - spb_payload, - write_attempt_dir, -) - -SPB_REASON_VOCABULARY = { - probe_contract.REASON_CAPABILITY_NOT_READY, - probe_contract.REASON_CONTENT_MISMATCH, - probe_contract.REASON_DEADLINE_EXCEEDED, - probe_contract.REASON_REMOTE_REJECTED, - probe_contract.REASON_RESPONSE_INVALID, - probe_contract.REASON_CLEANUP_UNVERIFIED, - probe_contract.REASON_INTERNAL_ERROR, -} - -SNAPSHOT_ID = "5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d5a1d" -CANARIES = ( - "broker-token", - "ACCESS-SECRET", - "SECRET-SECRET", - "SESSION-SECRET", - "https://storage.example.invalid", - "sandbox-bucket", - "sandbox-prefix", - "acct-backup", - SNAPSHOT_ID, -) -HUMAN_INIT_STDOUT = """created restic repository 00000000 at local:/tmp/spb-restic-proof - -Please note that knowledge of your password is required to access -the repository. Losing your password means that your data is -irrecoverably lost. -""" - - -class FakeClock: - def __init__(self) -> None: - self.now = datetime(2026, 1, 1, tzinfo=UTC) - self.mono = 1000.0 - - def utcnow(self) -> datetime: - return self.now - - def monotonic(self) -> float: - self.mono += 0.01 - return self.mono - - -class SequencedClock(FakeClock): - def __init__(self, values: list[float]) -> None: - super().__init__() - self._values = list(values) - - def monotonic(self) -> float: - if self._values: - return self._values.pop(0) - return super().monotonic() - - -def _assert_canaries_absent(text: str) -> None: - for canary in CANARIES: - assert canary not in text - - -def _assert_surviving_attempt_files_canary_clean(attempt_dir: Path) -> None: - for path in attempt_dir.rglob("*"): - _assert_canaries_absent(str(path)) - if path.is_file(): - _assert_canaries_absent(path.read_text(encoding="utf-8", errors="ignore")) - - -def _assert_outcome_contract(outcome: dict[str, Any]) -> None: - assert set(outcome) == { - probe_contract.FIELD_STATE, - probe_contract.FIELD_CHECKS, - probe_contract.FIELD_REASON, - probe_contract.FIELD_DURATION_MS, - } - assert isinstance(outcome[probe_contract.FIELD_DURATION_MS], int) - assert outcome[probe_contract.FIELD_DURATION_MS] >= 0 - checks = tuple(outcome[probe_contract.FIELD_CHECKS]) - assert checks == probe.PRIMITIVE_CHECKS[: len(checks)] - if outcome[probe_contract.FIELD_STATE] == probe_contract.PROOF_STATE_FAILED: - assert outcome[probe_contract.FIELD_REASON] in SPB_REASON_VOCABULARY - else: - assert outcome[probe_contract.FIELD_STATE] == probe_contract.PROOF_STATE_PASSED - assert outcome[probe_contract.FIELD_REASON] is None - assert checks == probe.PRIMITIVE_CHECKS - - -def _assert_failed( - outcome: dict[str, Any], - reason: str, - *, - checks: tuple[str, ...] = (), -) -> None: - _assert_outcome_contract(outcome) - assert outcome[probe_contract.FIELD_STATE] == probe_contract.PROOF_STATE_FAILED - assert tuple(outcome[probe_contract.FIELD_CHECKS]) == checks - assert outcome[probe_contract.FIELD_REASON] == reason - - -def _ready_journal( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> tuple[Path, Path]: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - assert result.exit_code == 0, result.output - attempt_dir = write_attempt_dir(journal, ATTEMPT_ID) - return journal, attempt_dir - - -def _install_ready_tools(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - probe.readiness, - "inspect_restic_ready", - lambda **_kwargs: (Path("/tool/restic"), None), - ) - monkeypatch.setattr( - probe.rclone_install, - "check_rclone_ready", - lambda **_kwargs: Path("/tool/rclone"), - ) - - -def _install_clock(monkeypatch: pytest.MonkeyPatch) -> FakeClock: - clock = FakeClock() - monkeypatch.setattr(probe, "_clock", clock) - return clock - - -def _creds(clock: FakeClock) -> HostedCredentials: - return HostedCredentials( - access_key_id="ACCESS-SECRET", - secret_access_key="SECRET-SECRET", - session_token="SESSION-SECRET", - endpoint="https://storage.example.invalid", - expires_at=(clock.now + timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ"), - ) - - -def _creds_with_expiry(clock: FakeClock, expires_at: str) -> HostedCredentials: - base = _creds(clock) - return HostedCredentials( - access_key_id=base.access_key_id, - secret_access_key=base.secret_access_key, - session_token=base.session_token, - endpoint=base.endpoint, - expires_at=expires_at, - ) - - -def _records(*records: object) -> str: - return "\n".join(json.dumps(record) for record in records) + "\n" - - -def _scrub_text(value: object, secrets: tuple[str, ...]) -> str: - text = str(value) - for secret in secrets: - if secret: - text = "[redacted]".join(text.split(secret)) - return text - - -def _captured_command(capture: dict[str, object]) -> str: - argv = capture["argv"] - assert isinstance(argv, tuple) - return next(token for token in ("init", "backup", "ls", "restore") if token in argv) - - -def _summary_record( - snapshot_id: object = SNAPSHOT_ID, - **overrides: object, -) -> dict[str, object]: - record = { - "message_type": "summary", - "total_files_processed": 1, - "total_bytes_processed": probe.FIXTURE_LENGTH, - "snapshot_id": snapshot_id, - } - record.update(overrides) - return record - - -def _restore_summary_record(**overrides: object) -> dict[str, object]: - record = { - "message_type": "summary", - "total_files": 2, - "files_restored": 2, - "total_bytes": probe.FIXTURE_LENGTH, - "bytes_restored": probe.FIXTURE_LENGTH, - } - record.update(overrides) - return record - - -def _ls_records( - *, - snapshot_id: str = SNAPSHOT_ID, - paths: object | None = None, - file_node: dict[str, object] | None = None, - extra: tuple[dict[str, object], ...] = (), -) -> tuple[dict[str, object], ...]: - if paths is None: - paths = [probe.LOGICAL_SOURCE_PATH] - if file_node is None: - file_node = { - "message_type": "node", - "struct_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - } - return ( - { - "message_type": "snapshot", - "struct_type": "snapshot", - "id": snapshot_id, - "paths": paths, - }, - {"message_type": "node", "struct_type": "node", "path": "/spb", "type": "dir"}, - file_node, - *extra, - ) - - -def _restic_command(args: list[str]) -> str: - return next(token for token in ("init", "backup", "ls", "restore") if token in args) - - -@dataclass(frozen=True) -class _PhaseOutput: - returncode: int = 0 - stdout: str | bytes = b"" - stderr: str | bytes = b"" - mutate_before_return: Callable[[], None] | None = None - restore_mutator: Callable[[Path], None] | None = None - - -def _bytes(value: str | bytes) -> bytes: - return value.encode() if isinstance(value, str) else value - - -def _assert_child_contract( - args: list[str], - kwargs: dict[str, Any], - *, - command: str, - input_bytes: bytes | None, - timeout: float | None, -) -> None: - assert "--no-cache" in args - assert kwargs["start_new_session"] is True - assert kwargs["close_fds"] is True - assert kwargs["pass_fds"] == () - assert timeout == probe.RESTIC_CHILD_TIMEOUT_S - env = kwargs["env"] - assert env["RESTIC_PASSWORD"] - if command == "init": - assert "--json" not in args - else: - assert "--json" in args - if command == "backup": - assert input_bytes == probe.SPB_SYNTHETIC_FIXTURE_BYTES - - -def _default_phase_output(command: str) -> _PhaseOutput: - if command == "init": - return _PhaseOutput(stdout=HUMAN_INIT_STDOUT) - if command == "backup": - return _PhaseOutput(stdout=_records(_summary_record())) - if command == "ls": - return _PhaseOutput(stdout=_records(*_ls_records())) - return _PhaseOutput(stdout=_records(_restore_summary_record())) - - -def _capture_child_surface( - args: list[str], - kwargs: dict[str, Any], - *, - input_bytes: bytes | None, - timeout: float | None, -) -> dict[str, object]: - env = kwargs["env"] - assert isinstance(env, dict) - child_secret_values = tuple( - str(value) - for value in ( - *env.values(), - *CANARIES, - SNAPSHOT_ID, - probe.LOGICAL_SOURCE_PATH, - ) - if value - ) - return { - "argv": tuple(_scrub_text(token, child_secret_values) for token in args), - "env": { - key: _scrub_text(value, child_secret_values) for key, value in env.items() - }, - "json": "--json" in args, - "stdin_bytes": input_bytes or b"", - "timeout": timeout, - } - - -def _install_popen_harness( - monkeypatch: pytest.MonkeyPatch, - events: list[str], - *, - phase_outputs: dict[str, _PhaseOutput] | None = None, - restore_mutator: Callable[[Path], None] | None = None, - captures: list[dict[str, object]] | None = None, - passwords: list[str] | None = None, -) -> None: - outputs = dict(phase_outputs or {}) - - class FakePopen: - pid = 12345 - - def __init__(self, args: list[str], **kwargs: Any) -> None: - self.args = args - self.kwargs = kwargs - self.returncode = 0 - - def communicate( - self, - input: bytes | None = None, - timeout: float | None = None, - ) -> tuple[bytes, bytes]: - command = _restic_command(self.args) - output = outputs.get(command, _default_phase_output(command)) - self.returncode = output.returncode - if passwords is not None: - passwords.append(self.kwargs["env"]["RESTIC_PASSWORD"]) - if captures is not None: - captures.append( - _capture_child_surface( - self.args, - self.kwargs, - input_bytes=input, - timeout=timeout, - ) - ) - events.append(command) - _assert_child_contract( - self.args, - self.kwargs, - command=command, - input_bytes=input, - timeout=timeout, - ) - if output.mutate_before_return is not None: - output.mutate_before_return() - if command == "restore" and output.returncode == 0: - target = Path(self.args[self.args.index("--target") + 1]) - (target / "spb").mkdir(parents=True) - (target / "spb" / "source.bin").write_bytes( - probe.SPB_SYNTHETIC_FIXTURE_BYTES - ) - mutator = output.restore_mutator or restore_mutator - if mutator is not None: - mutator(target) - return _bytes(output.stdout), _bytes(output.stderr) - - monkeypatch.setattr(backup_runner.subprocess, "Popen", FakePopen) - - -def _install_sequenced_creds( - monkeypatch: pytest.MonkeyPatch, - clock: FakeClock, - events: list[str], - expires_at_values: list[str], -) -> None: - values = list(expires_at_values) - - def fetch(_binding, *, scope: str) -> HostedCredentials: - assert scope == "operated" - events.append("fetch") - if not values: - return _creds(clock) - return _creds_with_expiry(clock, values.pop(0)) - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - - -def _install_empty_listing( - monkeypatch: pytest.MonkeyPatch, - events: list[str], -) -> None: - def list_prefix_contents(**_kwargs: Any): - events.append("list") - return (), () - - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", list_prefix_contents) - - -def _install_phase_restic( - monkeypatch: pytest.MonkeyPatch, - events: list[str], - *, - phase: str, - result: _PhaseOutput, - mutate_before_return: Callable[[], None] | None = None, - captures: list[dict[str, object]] | None = None, -) -> None: - output = result - if mutate_before_return is not None: - output = _PhaseOutput( - returncode=result.returncode, - stdout=result.stdout, - stderr=result.stderr, - mutate_before_return=mutate_before_return, - restore_mutator=result.restore_mutator, - ) - _install_popen_harness( - monkeypatch, - events, - phase_outputs={phase: output}, - captures=captures, - ) - - -def _install_ls_records( - monkeypatch: pytest.MonkeyPatch, - events: list[str], - *, - stdout: str, -) -> None: - _install_popen_harness( - monkeypatch, - events, - phase_outputs={"ls": _PhaseOutput(stdout=stdout)}, - ) - - -def _install_success_fakes( - monkeypatch: pytest.MonkeyPatch, - clock: FakeClock, - events: list[str], - *, - restore_mutator: Callable[[Path], None] | None = None, - captures: list[dict[str, object]] | None = None, - passwords: list[str] | None = None, -) -> None: - def fetch(_binding, *, scope: str) -> HostedCredentials: - assert scope == "operated" - events.append("fetch") - return _creds(clock) - - def list_prefix_contents(**_kwargs: Any): - events.append("list") - return (), () - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", list_prefix_contents) - _install_popen_harness( - monkeypatch, - events, - restore_mutator=restore_mutator, - captures=captures, - passwords=passwords, - ) - - -def _forbid_contact(*_args: Any, **_kwargs: Any): - raise AssertionError("SPB proof contacted remote state") - - -def test_spb_success_uses_five_fresh_fetches_and_four_checks( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - captures: list[dict[str, object]] = [] - _install_success_fakes(monkeypatch, clock, events, captures=captures) - caplog.set_level(logging.DEBUG) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - _assert_outcome_contract(outcome) - assert tuple(outcome["checks"]) == probe.PRIMITIVE_CHECKS - assert "spb.local_cleanup" not in outcome["checks"] - assert events == [ - "fetch", - "list", - "fetch", - "init", - "fetch", - "backup", - "fetch", - "ls", - "fetch", - "restore", - ] - assert len(captures) == 4 - captures_by_command = {_captured_command(capture): capture for capture in captures} - assert captures_by_command["init"]["json"] is False - assert captures_by_command["backup"]["json"] is True - assert captures_by_command["ls"]["json"] is True - assert captures_by_command["restore"]["json"] is True - for capture in captures: - argv = capture["argv"] - assert isinstance(argv, tuple) - assert "--no-cache" in argv - assert (attempt_dir / "spb" / "source.bin").exists() - _assert_canaries_absent(repr(outcome)) - _assert_canaries_absent(caplog.text) - _assert_canaries_absent(repr(captures)) - _assert_surviving_attempt_files_canary_clean(attempt_dir) - - -def test_spb_json_phase_scrub_set_contains_every_production_identifier( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - observed: dict[str, set[str]] = {} - original = probe.run_restic_json_records - - def spy_run_restic_json_records(args: list[str], **kwargs: Any): - command = _restic_command(args) - observed[command] = {str(value) for value in kwargs["scrub_values"] if value} - return original(args, **kwargs) - - monkeypatch.setattr( - probe, - "run_restic_json_records", - spy_run_restic_json_records, - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - binding = probe._load_binding(journal) - proof_binding = probe._proof_binding(binding, attempt_dir.name) - spb_root = attempt_dir / probe.SPB_DIR_NAME - restore_target = spb_root / probe.RESTORE_DIR_NAME - repository = f"rclone:spb:{proof_binding.bucket}/{proof_binding.prefix}" - expected_common = { - binding.broker_endpoint, - binding.account_id, - binding.instance_id, - binding.bucket, - binding.prefix, - binding.broker_token, - proof_binding.prefix, - str(attempt_dir), - str(spb_root), - str(restore_target), - probe.LOGICAL_SOURCE_PATH, - repository, - } - assert set(observed) == {"backup", "ls", "restore"} - for command, scrub_values in observed.items(): - assert expected_common <= scrub_values - if command in {"ls", "restore"}: - assert SNAPSHOT_ID in scrub_values - else: - assert SNAPSHOT_ID not in scrub_values - - -def test_nonempty_prefix_refuses_before_init( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - - def fetch(_binding, *, scope: str) -> HostedCredentials: - events.append("fetch") - return _creds(clock) - - def list_prefix_contents(**_kwargs: Any): - events.append("list") - return ("sandbox-prefix/proofs/x/config",), () - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", list_prefix_contents) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch", "list"] - - -def test_local_refusal_has_no_broker_or_spawn( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - attempt_dir = write_attempt_dir(journal, ATTEMPT_ID) - _install_ready_tools(monkeypatch) - monkeypatch.setattr(probe, "fetch_hosted_credentials", _forbid_contact) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_CAPABILITY_NOT_READY) - - -def test_partial_fixture_write_is_internal_error_before_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - - def partial_write(_fd: int, data: bytes) -> int: - assert len(data) == probe.FIXTURE_LENGTH - return probe.FIXTURE_LENGTH - 1 - - monkeypatch.setattr(probe.os, "write", partial_write) - monkeypatch.setattr(probe, "fetch_hosted_credentials", _forbid_contact) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_INTERNAL_ERROR) - - -def test_recovery_key_path_is_structurally_unreachable( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - captures: list[dict[str, object]] = [] - _install_success_fakes(monkeypatch, clock, events, captures=captures) - - import solstone.think.backup.repo as repo - - monkeypatch.setattr(repo, "init_repository", _forbid_contact) - monkeypatch.setattr(repo, "_add_recovery_key", _forbid_contact) - monkeypatch.setattr(repo, "_verify_recovery_key", _forbid_contact) - monkeypatch.setattr(repo, "_capture_current_key_id", _forbid_contact) - monkeypatch.setattr(probe.state, "get_keys", _forbid_contact, raising=False) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - _assert_outcome_contract(outcome) - - -def test_restore_tree_must_be_exact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - - def mutate(target: Path) -> None: - (target / "extra").write_text("not expected", encoding="utf-8") - - _install_success_fakes(monkeypatch, clock, events, restore_mutator=mutate) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed( - outcome, - probe_contract.REASON_CONTENT_MISMATCH, - checks=probe.PRIMITIVE_CHECKS[:3], - ) - - -def test_cleanup_helper_removes_only_spb_subtree( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - attempt_dir = write_attempt_dir(journal, ATTEMPT_ID) - (attempt_dir / "spb").mkdir() - (attempt_dir / "spb" / "source.bin").write_text("synthetic", encoding="utf-8") - - outcome = probe.cleanup_spb_attempt_local(journal, attempt_dir=attempt_dir) - - assert outcome == {"state": "verified", "reason": None, "duration_ms": 0} - assert attempt_dir.exists() - assert not (attempt_dir / "spb").exists() - - -def test_inspector_surface_is_exact(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): - monkeypatch.setattr( - probe.readiness, - "inspect_restic_ready", - lambda **_kwargs: (None, "restic_missing"), - ) - assert probe.inspect_sandbox_spb_prerequisites(tmp_path) == { - "state": "unavailable", - "reason": "restic_missing", - } - - -@pytest.mark.parametrize("prefix", ["users/acct/inst", "users/acct/inst/"]) -def test_proof_binding_normalizes_optional_trailing_slash(prefix: str) -> None: - binding = HostedBinding( - broker_endpoint="https://broker.example.invalid", - account_id="acct", - instance_id="inst", - bucket="bucket", - prefix=prefix, - broker_token="broker-token", - ) - - proof_binding = probe._proof_binding(binding, ATTEMPT_ID) - - assert proof_binding.prefix == f"users/acct/inst/proofs/{ATTEMPT_ID}/" - - -def test_spb_capability_mismatch_refuses_before_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - payload_path = journal / "backup" / "hosted" / "binding.json" - payload = json.loads(payload_path.read_text("utf-8")) - payload["instance_id"] = "99999999-9999-4999-8999-999999999999" - payload_path.write_text(json.dumps(payload), encoding="utf-8") - _install_ready_tools(monkeypatch) - monkeypatch.setattr(probe, "fetch_hosted_credentials", _forbid_contact) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_CAPABILITY_NOT_READY) - - -def test_daily_key_comes_from_passed_journal_not_ambient_journal( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - daily_key = probe.state.get_daily_key(journal) - assert daily_key is not None - ambient = tmp_path / "ambient-journal" - (ambient / "config").mkdir(parents=True) - (ambient / "config" / "journal.json").write_text( - json.dumps( - { - "backup": { - "enabled": True, - "mode": "operated", - "daily_key": "ambient-daily-key", - } - } - ), - encoding="utf-8", - ) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(ambient)) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - passwords: list[str] = [] - _install_success_fakes(monkeypatch, clock, events, passwords=passwords) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - assert set(passwords) == {daily_key} - assert "ambient-daily-key" not in passwords - - -@pytest.mark.parametrize( - "reason_code", - ["broker_unreachable", "hosted_entitlement_inactive"], -) -def test_broker_rejection_maps_to_remote_rejected_before_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - reason_code: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - _install_clock(monkeypatch) - - def fetch(_binding, *, scope: str) -> HostedCredentials: - assert scope == "operated" - raise HostedCredsUnavailable(reason_code) - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_REMOTE_REJECTED) - - -@pytest.mark.parametrize( - "expires_at", - [ - "2026-01-01T00:10:00.000Z", - "2026-01-01T00:10:00+00:00", - "2026-01-01T00:10:00z", - " 2026-01-01T00:10:00Z", - "2026-02-31T00:10:00Z", - ], -) -def test_malformed_expires_at_is_response_invalid_before_listing( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - expires_at: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_sequenced_creds(monkeypatch, clock, events, [expires_at]) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch"] - - -@pytest.mark.parametrize( - "expires_at", - [ - "2025-12-31T23:59:59Z", - "2026-01-01T00:01:01Z", - ], -) -def test_expired_or_insufficient_listing_lease_is_response_invalid( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - expires_at: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_sequenced_creds(monkeypatch, clock, events, [expires_at]) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch"] - - -def test_lease_remaining_exactly_seventy_five_rejects_before_spawn( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_sequenced_creds( - monkeypatch, - clock, - events, - [ - "2026-01-01T00:10:00Z", - "2026-01-01T00:01:15Z", - ], - ) - _install_empty_listing(monkeypatch, events) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch", "list", "fetch"] - - -def test_lease_remaining_above_seventy_five_proceeds_to_spawn( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_sequenced_creds( - monkeypatch, - clock, - events, - [ - "2026-01-01T00:10:00Z", - "2026-01-01T00:01:16Z", - "2026-01-01T00:10:00Z", - "2026-01-01T00:10:00Z", - "2026-01-01T00:10:00Z", - ], - ) - _install_empty_listing(monkeypatch, events) - _install_popen_harness(monkeypatch, events) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - _assert_outcome_contract(outcome) - assert "init" in events - - -def test_elapsed_monotonic_time_reduces_lease_before_spawn( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = SequencedClock( - [ - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1002.0, - 1002.0, - ] - ) - monkeypatch.setattr(probe, "_clock", clock) - events: list[str] = [] - _install_sequenced_creds( - monkeypatch, - clock, - events, - [ - "2026-01-01T00:10:00Z", - "2026-01-01T00:01:16Z", - ], - ) - _install_empty_listing(monkeypatch, events) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch", "list", "fetch"] - - -@pytest.mark.parametrize( - ("keys", "uploads"), - [ - ((), (("sandbox-prefix/proofs/x/multipart", "upload-1"),)), - ( - ( - "sandbox-prefix/proofs/x/page-1-object", - "sandbox-prefix/proofs/x/page-2-object", - ), - (), - ), - ( - (), - ( - ("sandbox-prefix/proofs/x/page-1-upload", "upload-1"), - ("sandbox-prefix/proofs/x/page-2-upload", "upload-2"), - ), - ), - (("sandbox-prefix/proofs/x/config",), ()), - (("sandbox-prefix/proofs/x/keys/key",), ()), - (("sandbox-prefix/proofs/x/data/aa/blob",), ()), - (("sandbox-prefix/proofs/x/index/index",), ()), - (("sandbox-prefix/proofs/x/snapshots/snapshot",), ()), - (("sandbox-prefix/proofs/x/locks/lock",), ()), - ], -) -def test_storage_contents_refuse_before_init( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - keys: tuple[str, ...], - uploads: tuple[tuple[str, str], ...], -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - - def fetch(_binding, *, scope: str) -> HostedCredentials: - events.append("fetch") - return _creds(clock) - - def list_prefix_contents(**_kwargs: Any): - events.append("list") - return keys, uploads - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", list_prefix_contents) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch", "list"] - - -@pytest.mark.parametrize( - "failure", - [ - "object-pagination-missing-token", - "multipart-pagination-missing-markers", - ], -) -def test_ambiguous_storage_listing_refuses_before_init( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - failure: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - - def fetch(_binding, *, scope: str) -> HostedCredentials: - events.append("fetch") - return _creds(clock) - - def list_prefix_contents(**_kwargs: Any): - events.append(f"list:{failure}") - raise RuntimeError(failure) - - monkeypatch.setattr(probe, "fetch_hosted_credentials", fetch) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", list_prefix_contents) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_RESPONSE_INVALID) - assert events == ["fetch", f"list:{failure}"] - - -@pytest.mark.parametrize( - ( - "phase", - "result", - "reason", - "checks", - "expected_parse_calls", - "expected_consume_calls", - ), - [ - ( - "init", - _PhaseOutput( - returncode=1, - stderr=backup_runner._PROCESS_GROUP_CLEANUP_UNVERIFIED, - ), - probe_contract.REASON_CLEANUP_UNVERIFIED, - (), - 0, - 0, - ), - ( - "init", - _PhaseOutput(returncode=124, stderr="timeout"), - probe_contract.REASON_DEADLINE_EXCEEDED, - (), - 0, - 0, - ), - ( - "backup", - _PhaseOutput(returncode=1, stdout=_records(_summary_record())), - probe_contract.REASON_REMOTE_REJECTED, - probe.PRIMITIVE_CHECKS[:1], - 0, - 0, - ), - ( - "backup", - _PhaseOutput(stdout=""), - probe_contract.REASON_RESPONSE_INVALID, - probe.PRIMITIVE_CHECKS[:1], - 1, - 0, - ), - ( - "backup", - _PhaseOutput(stdout='{"message_type":'), - probe_contract.REASON_RESPONSE_INVALID, - probe.PRIMITIVE_CHECKS[:1], - 1, - 0, - ), - ( - "backup", - _PhaseOutput( - stdout=( - '{"message_type":"summary","message_type":"summary",' - '"total_files_processed":1,"total_bytes_processed":4096,' - f'"snapshot_id":"{SNAPSHOT_ID}"}}\n' - ) - ), - probe_contract.REASON_RESPONSE_INVALID, - probe.PRIMITIVE_CHECKS[:1], - 1, - 0, - ), - ( - "backup", - _PhaseOutput( - stdout=_records( - _summary_record(total_bytes_processed=probe.FIXTURE_LENGTH + 1) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - probe.PRIMITIVE_CHECKS[:1], - 1, - 1, - ), - ], - ids=[ - "cleanup_unverified", - "timeout", - "nonzero", - "empty_parse_rejection", - "malformed_parse_rejection", - "duplicate_key_parse_rejection", - "semantic_rejection", - ], -) -def test_spb_precedence_table_and_parser_consume_counts( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - phase: str, - result: _PhaseOutput, - reason: str, - checks: tuple[str, ...], - expected_parse_calls: int, - expected_consume_calls: int, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - _install_phase_restic(monkeypatch, events, phase=phase, result=result) - parse_calls = 0 - consume_calls = 0 - original_parse = backup_runner._parse_json_records - original_consume = backup_runner.ResticJsonRecordsResult.consume_records - - def counting_parse(raw_stdout: bytes | None) -> tuple[object, ...] | None: - nonlocal parse_calls - parse_calls += 1 - return original_parse(raw_stdout) - - def counting_consume( - self: backup_runner.ResticJsonRecordsResult, - ) -> tuple[object, ...]: - nonlocal consume_calls - consume_calls += 1 - return original_consume(self) - - monkeypatch.setattr(backup_runner, "_parse_json_records", counting_parse) - monkeypatch.setattr( - backup_runner.ResticJsonRecordsResult, - "consume_records", - counting_consume, - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, reason, checks=checks) - assert parse_calls == expected_parse_calls - assert consume_calls == expected_consume_calls - - -@pytest.mark.parametrize( - "result", - [ - _PhaseOutput(returncode=1, stderr="denied"), - _PhaseOutput(returncode=124, stderr="timeout"), - ], -) -def test_init_nonzero_and_timeout_map_to_expected_reason( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - result: _PhaseOutput, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - captures: list[dict[str, object]] = [] - _install_success_fakes(monkeypatch, clock, events, captures=captures) - _install_phase_restic(monkeypatch, events, phase="init", result=result) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - reason = ( - probe_contract.REASON_DEADLINE_EXCEEDED - if result.returncode == 124 - else probe_contract.REASON_REMOTE_REJECTED - ) - _assert_failed(outcome, reason) - - -@pytest.mark.parametrize( - ("stdout", "reason"), - [ - ("", probe_contract.REASON_RESPONSE_INVALID), - (_records([]), probe_contract.REASON_RESPONSE_INVALID), - (_records({"message_type": "summary"}), probe_contract.REASON_RESPONSE_INVALID), - (_records(_summary_record("")), probe_contract.REASON_RESPONSE_INVALID), - ( - _records(_summary_record(total_files_processed=True)), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_summary_record(total_bytes_processed=probe.FIXTURE_LENGTH + 1)), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records(_summary_record(SNAPSHOT_ID.upper())), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records({"message_type": "verbose_status"}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records({"message_type": "status", "percent_done": 0.5}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_summary_record(), _summary_record()), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_summary_record(), {"message_type": "status"}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_summary_record(SNAPSHOT_ID)), - probe_contract.REASON_REMOTE_REJECTED, - ), - ], - ids=[ - "empty_stdout", - "non_object_record", - "summary_missing_required_fields", - "empty_snapshot_id", - "bool_total_files_processed", - "wrong_total_bytes_processed", - "uppercase_snapshot_id", - "verbose_status_record", - "status_without_summary", - "duplicate_summary", - "record_after_summary", - "remote_rejected_preempts_stdout_validation", - ], -) -def test_backup_failure_shapes_map_to_expected_reason( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - stdout: str, - reason: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - returncode = 1 if reason == probe_contract.REASON_REMOTE_REJECTED else 0 - _install_phase_restic( - monkeypatch, - events, - phase="backup", - result=_PhaseOutput(returncode=returncode, stdout=stdout, stderr="denied"), - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, reason, checks=probe.PRIMITIVE_CHECKS[:1]) - - -def test_fixture_mutation_between_backup_checks_is_content_mismatch( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - - def mutate_fixture() -> None: - (attempt_dir / "spb" / "source.bin").write_bytes(b"mutated") - - _install_phase_restic( - monkeypatch, - events, - phase="backup", - result=_PhaseOutput(stdout=_records(_summary_record(SNAPSHOT_ID))), - mutate_before_return=mutate_fixture, - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed( - outcome, - probe_contract.REASON_CONTENT_MISMATCH, - checks=probe.PRIMITIVE_CHECKS[:1], - ) - - -def test_backup_and_restore_accept_status_records_before_terminal_summary() -> None: - status_records = [ - {"message_type": "status", "percent_done": 0.25}, - {"message_type": "status", "percent_done": 0.75}, - ] - - assert ( - probe._validate_backup_records([*status_records, _summary_record()]) - == SNAPSHOT_ID - ) - probe._validate_restore_records([*status_records, _restore_summary_record()]) - - -@pytest.mark.parametrize( - ("stdout_source", "reason"), - [ - (_records([]), probe_contract.REASON_RESPONSE_INVALID), - ( - _records(*_ls_records(snapshot_id="wrong-id")), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - { - "message_type": "snapshot", - "struct_type": "node", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - {"message_type": "node", "path": "/spb", "type": "dir"}, - { - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - }, - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records( - *_ls_records(), - { - "message_type": "snapshot", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(*_ls_records(paths=["/wrong"])), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - file_node={ - "message_type": "node", - "struct_type": "snapshot", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - } - ) - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records( - *_ls_records( - file_node={ - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": True, - } - ) - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records( - *_ls_records( - file_node={ - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - } - ) - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records( - *_ls_records( - file_node={ - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH + 1, - } - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - { - "message_type": "snapshot", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - {"message_type": "node", "path": "/spb", "type": "dir"}, - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - { - "message_type": "snapshot", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - { - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - }, - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - extra=({"message_type": "node", "path": "/spb", "type": "dir"},) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - extra=( - { - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - }, - ) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - extra=( - { - "message_type": "node", - "path": "/spb/link", - "type": "symlink", - }, - ) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - extra=( - { - "message_type": "node", - "path": "/spb/fifo", - "type": "fifo", - }, - ) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - *_ls_records( - extra=( - {"message_type": "node", "path": "/spb/extra", "type": "dir"}, - ) - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - lambda attempt_dir: _records( - *_ls_records( - file_node={ - "message_type": "node", - "path": str(attempt_dir / "spb" / "source.bin"), - "type": "file", - "size": probe.FIXTURE_LENGTH, - } - ) - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records( - { - "message_type": "snapshot", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - {"message_type": "unknown"}, - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records( - *_ls_records( - extra=( - { - "message_type": "error", - "message": "unexpected restic record", - }, - ) - ) - ), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - '{"message_type":"snapshot","message_type":"snapshot",' - f'"id":"{SNAPSHOT_ID}","paths":["/spb/source.bin"]}}\n', - probe_contract.REASON_RESPONSE_INVALID, - ), - ], - ids=[ - "non_object_record", - "wrong_snapshot_id", - "snapshot_struct_type_mismatch", - "duplicate_snapshot_record", - "paths_mismatch", - "node_struct_type_mismatch", - "bool_file_size", - "missing_file_size", - "wrong_file_size", - "missing_file_node", - "missing_directory_node", - "duplicate_directory_node", - "duplicate_file_node", - "link_node", - "special_node", - "extra_dir_node", - "physical_source_path_node", - "unknown_record_kind", - "error_record_kind", - "duplicate_message_type_key", - ], -) -def test_ls_strictness_failures( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - stdout_source: str | Callable[[Path], str], - reason: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - stdout = stdout_source(attempt_dir) if callable(stdout_source) else stdout_source - _install_success_fakes(monkeypatch, clock, events) - _install_ls_records(monkeypatch, events, stdout=stdout) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, reason, checks=probe.PRIMITIVE_CHECKS[:2]) - - -def test_ls_accepts_permuted_records_without_struct_type( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - stdout = _records( - { - "message_type": "node", - "path": probe.LOGICAL_SOURCE_PATH, - "type": "file", - "size": probe.FIXTURE_LENGTH, - }, - { - "message_type": "snapshot", - "id": SNAPSHOT_ID, - "paths": [probe.LOGICAL_SOURCE_PATH], - }, - {"message_type": "node", "path": "/spb", "type": "dir"}, - ) - _install_success_fakes(monkeypatch, clock, events) - _install_ls_records(monkeypatch, events, stdout=stdout) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - _assert_outcome_contract(outcome) - - -@pytest.mark.parametrize( - ("returncode", "mutator", "reason"), - [ - (1, None, probe_contract.REASON_REMOTE_REJECTED), - ( - 0, - lambda target: (target / "extra").write_text( - "not expected", - encoding="utf-8", - ), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - 0, - lambda target: (target / "spb" / "linked").symlink_to("source.bin"), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - 0, - lambda target: os.mkfifo(target / "spb" / "fifo"), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ], - ids=[ - "remote_rejected", - "extra_restored_file", - "restored_symlink", - "restored_fifo", - ], -) -def test_restore_failures( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - returncode: int, - mutator: Callable[[Path], None] | None, - reason: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - if returncode: - _install_phase_restic( - monkeypatch, - events, - phase="restore", - result=_PhaseOutput(returncode=returncode, stderr="denied"), - ) - elif mutator is not None: - _install_popen_harness( - monkeypatch, - events, - restore_mutator=mutator, - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, reason, checks=probe.PRIMITIVE_CHECKS[:3]) - - -@pytest.mark.parametrize( - ("stdout", "reason"), - [ - ("", probe_contract.REASON_RESPONSE_INVALID), - (_records({"message_type": "summary"}), probe_contract.REASON_RESPONSE_INVALID), - ( - _records(_restore_summary_record(total_files=True)), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_restore_summary_record(total_files=1)), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records(_restore_summary_record(bytes_restored=probe.FIXTURE_LENGTH + 1)), - probe_contract.REASON_CONTENT_MISMATCH, - ), - ( - _records({"message_type": "verbose_status"}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records({"message_type": "status", "percent_done": 0.5}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_restore_summary_record(), _restore_summary_record()), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - _records(_restore_summary_record(), {"message_type": "status"}), - probe_contract.REASON_RESPONSE_INVALID, - ), - ( - '{"message_type":"summary","message_type":"summary",' - '"total_files":2,"files_restored":2,' - '"total_bytes":1024,"bytes_restored":1024}\n', - probe_contract.REASON_RESPONSE_INVALID, - ), - ], - ids=[ - "empty_stdout", - "summary_missing_required_fields", - "bool_total_files", - "wrong_total_files", - "wrong_bytes_restored", - "verbose_status_record", - "status_without_summary", - "duplicate_summary", - "record_after_summary", - "duplicate_message_type_key", - ], -) -def test_restore_json_strictness_failures( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - stdout: str, - reason: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - _install_phase_restic( - monkeypatch, - events, - phase="restore", - result=_PhaseOutput(stdout=stdout), - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, reason, checks=probe.PRIMITIVE_CHECKS[:3]) - - -def test_process_group_survivor_ambiguity_maps_to_cleanup_unverified( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - _install_success_fakes(monkeypatch, clock, events) - _install_phase_restic( - monkeypatch, - events, - phase="init", - result=_PhaseOutput( - returncode=1, - stderr=backup_runner._PROCESS_GROUP_CLEANUP_UNVERIFIED, - ), - ) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_CLEANUP_UNVERIFIED) - - -def test_pre_mint_budget_refusal_has_no_mint_or_spawn( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = SequencedClock([1000.0, 1268.0, 1268.0]) - monkeypatch.setattr(probe, "_clock", clock) - monkeypatch.setattr(probe, "fetch_hosted_credentials", _forbid_contact) - monkeypatch.setattr(probe.s3_wipe, "list_prefix_contents", _forbid_contact) - monkeypatch.setattr(probe, "run_restic", _forbid_contact) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_failed(outcome, probe_contract.REASON_DEADLINE_EXCEEDED) - - -def test_cleanup_helper_unverified_and_independent_budget( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - attempt_dir = write_attempt_dir(journal, ATTEMPT_ID) - (attempt_dir / "spb").mkdir() - clock = SequencedClock([10.0, 10.5, 10.5]) - monkeypatch.setattr(probe, "_clock", clock) - captured: dict[str, float] = {} - - def cleanup_path_absent(_path: Path, deadline: float) -> bool: - captured["deadline"] = deadline - return False - - monkeypatch.setattr(probe, "_cleanup_path_absent", cleanup_path_absent) - - outcome = probe.cleanup_spb_attempt_local(journal, attempt_dir=attempt_dir) - - assert outcome["state"] == probe_contract.CLEANUP_STATE_UNVERIFIED - assert outcome["reason"] == probe_contract.REASON_CLEANUP_UNVERIFIED - assert captured["deadline"] == 40.0 - - -@pytest.mark.parametrize( - "mode", - ["success", "timeout", "error"], -) -def test_canaries_absent_across_success_timeout_and_error_paths( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, - mode: str, -) -> None: - journal, attempt_dir = _ready_journal(tmp_path, monkeypatch) - _install_ready_tools(monkeypatch) - clock = _install_clock(monkeypatch) - events: list[str] = [] - captures: list[dict[str, object]] = [] - _install_success_fakes(monkeypatch, clock, events, captures=captures) - if mode == "timeout": - _install_phase_restic( - monkeypatch, - events, - phase="init", - result=_PhaseOutput(returncode=124, stderr="timeout"), - captures=captures, - ) - elif mode == "error": - - def list_prefix_contents(**_kwargs: Any): - events.append("list") - return ("sandbox-prefix/proofs/x/config",), () - - monkeypatch.setattr( - probe.s3_wipe, - "list_prefix_contents", - list_prefix_contents, - ) - caplog.set_level(logging.DEBUG) - - outcome = probe.prove_spb_backup(journal, attempt_dir=attempt_dir) - - _assert_outcome_contract(outcome) - _assert_canaries_absent(repr(outcome)) - _assert_canaries_absent(caplog.text) - _assert_canaries_absent(repr(captures)) - _assert_surviving_attempt_files_canary_clean(attempt_dir) diff --git a/tests/sandbox_profile/test_spb_backup_probe_integration.py b/tests/sandbox_profile/test_spb_backup_probe_integration.py deleted file mode 100644 index 05b12132c..000000000 --- a/tests/sandbox_profile/test_spb_backup_probe_integration.py +++ /dev/null @@ -1,173 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Local restic boundary proof for the SPB probe contract. - -This test drives the real restic subprocess boundary against pinned restic -0.19.0 and a tmp_path local repository, including parser-mode JSON phases. No -broker, rclone, network backend, or production state is contacted. -""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path - -import pytest - -from solstone.think.backup.hosted import HostedBinding -from solstone.think.backup.runner import ( - ResticJsonRecordsResult, - ResticResult, - run_restic, - run_restic_json_records, -) -from solstone.think.sandbox_profile import spb_backup_probe as probe - -pytestmark = pytest.mark.integration - -RESTIC_BIN = shutil.which("restic") - - -@pytest.mark.skipif(RESTIC_BIN is None, reason="restic is not installed") -def test_run_restic_boundary_accepts_real_local_restic_output( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - assert RESTIC_BIN is not None - version = subprocess.run( - [RESTIC_BIN, "version"], - check=True, - capture_output=True, - text=True, - timeout=5, - ) - assert version.stderr == "" - assert version.stdout.split()[:2] == ["restic", "0.19.0"] - - home = tmp_path / "home" - tmp_dir = tmp_path / "tmp" - for path in (home, tmp_dir): - path.mkdir() - monkeypatch.setenv("HOME", str(home)) - monkeypatch.setenv("TMPDIR", str(tmp_dir)) - - attempt_dir = tmp_path / "attempt" - spb_root = attempt_dir / probe.SPB_DIR_NAME - fixture_path = spb_root / probe.SOURCE_FILE_NAME - restore_target = spb_root / probe.RESTORE_DIR_NAME - spb_root.mkdir(parents=True) - fixture_path.write_bytes(probe.SPB_SYNTHETIC_FIXTURE_BYTES) - - binding = HostedBinding( - broker_endpoint="https://unused.invalid", - account_id="unused-account", - instance_id="unused-instance", - bucket="unused-bucket", - prefix="unused-prefix", - broker_token="unused-token", - ) - # Hosted/rclone fields are unused by this direct local-repository path. - preflight = probe._Preflight( - attempt_dir=attempt_dir, - spb_root=spb_root, - restore_target=restore_target, - fixture_path=fixture_path, - fixture=probe._fixture_identity(fixture_path), - binding=binding, - proof_binding=binding, - daily_key="synthetic-local-restic-password", - restic_path=Path(RESTIC_BIN), - rclone_path=tmp_path / "unused-rclone", - scrub_values=probe._scrub_values( - binding=binding, - proof_binding=binding, - attempt_dir=attempt_dir, - spb_root=spb_root, - restore_target=restore_target, - ), - ) - - repository = f"local:{tmp_path / 'repo'}" - - def assert_visible_surface_safe( - result: ResticResult | ResticJsonRecordsResult, - secrets: tuple[str, ...], - ) -> None: - rendered = f"{result!r} {result.stdout} {result.stderr} {' '.join(result.argv)}" - for secret in secrets: - assert secret not in rendered - - def run_init_phase( - args: list[str], - ) -> ResticResult: - result = run_restic( - ["--no-cache", *args], - repository=repository, - password=preflight.daily_key, - restic_path=preflight.restic_path, - json=False, - timeout=probe.RESTIC_CHILD_TIMEOUT_S, - process_group=True, - scrub_values=(*preflight.scrub_values, repository), - terminate_grace_s=probe.TERM_GRACE_S, - kill_grace_s=probe.KILL_GRACE_S, - ) - assert "--no-cache" in result.argv - assert_visible_surface_safe(result, (*preflight.scrub_values, repository)) - return result - - def run_records_phase( - args: list[str], - *, - stdin_bytes: bytes | None = None, - extra_scrub_values: tuple[str, ...] = (), - ) -> tuple[list[object], ResticJsonRecordsResult]: - scrub_values = (*preflight.scrub_values, repository, *extra_scrub_values) - result = run_restic_json_records( - ["--no-cache", *args], - repository=repository, - password=preflight.daily_key, - restic_path=preflight.restic_path, - timeout=probe.RESTIC_CHILD_TIMEOUT_S, - stdin_bytes=stdin_bytes, - scrub_values=scrub_values, - terminate_grace_s=probe.TERM_GRACE_S, - kill_grace_s=probe.KILL_GRACE_S, - ) - assert "--no-cache" in result.argv - assert result.stdout - assert_visible_surface_safe(result, scrub_values) - probe._check_restic_result(result) - assert result.has_records - records = list(result.consume_records()) - assert not result.has_records - with pytest.raises(TypeError): - result.consume_records() - return records, result - - init_result = run_init_phase(["init"]) - probe._check_restic_result(init_result) - - backup_records, backup_result = run_records_phase( - ["backup", "--stdin", "--stdin-filename", probe.LOGICAL_SOURCE_PATH], - stdin_bytes=probe.SPB_SYNTHETIC_FIXTURE_BYTES, - ) - snapshot_id = probe._validate_backup_records(backup_records) - assert snapshot_id not in backup_result.stdout - - ls_records, _ls_result = run_records_phase( - ["ls", "--long", snapshot_id], - extra_scrub_values=(snapshot_id,), - ) - probe._validate_ls_records(ls_records, snapshot_id, preflight) - - restore_records, _restore_result = run_records_phase( - ["restore", snapshot_id, "--target", str(preflight.restore_target)], - extra_scrub_values=(snapshot_id,), - ) - probe._validate_restore_records(restore_records) - probe._verify_restore_tree(preflight) - - assert not (home / ".cache" / "restic").exists() diff --git a/tests/sandbox_profile/test_spl_readiness.py b/tests/sandbox_profile/test_spl_readiness.py deleted file mode 100644 index 541762806..000000000 --- a/tests/sandbox_profile/test_spl_readiness.py +++ /dev/null @@ -1,389 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import dataclasses -import json -import time -from pathlib import Path -from typing import Any - -import pytest - -from solstone.think.link import client as link_client -from solstone.think.sandbox_profile import probe_contract, spl_readiness -from solstone.think.sandbox_profile import spl_relay_tunnel as probe -from tests.sandbox_profile import RUN_ID, sandbox_journal, write_attempt_dir - - -def test_snapshot_field_set_is_exact() -> None: - assert tuple( - field.name for field in dataclasses.fields(spl_readiness.SplReadinessSnapshot) - ) == ( - "supervisor_ref", - "spl_pid", - "spl_ref", - "convey_pid", - "convey_ref", - "spl_connection_state", - "listen_generation", - "link_health_observed_at_monotonic", - "observed_relay_origin", - "secure_listener_bound_accepting", - "supervisor_observed_at_monotonic", - ) - - -def test_snapshot_and_reverify_use_same_connection( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = _journal(tmp_path) - fake = _install_fake_connection(monkeypatch) - _install_process_and_socket(monkeypatch) - - with spl_readiness.open_spl_readiness_observer(journal) as observer: - fake.emit(_supervisor_status()) - fake.emit(_link_health(generation=7)) - snapshot = observer.wait_snapshot(deadline=time.monotonic() + 1) - - assert snapshot.supervisor_ref == "supervisor-ref" - assert snapshot.spl_pid == 101 - assert snapshot.spl_ref == "spl-ref" - assert snapshot.convey_pid == 202 - assert snapshot.convey_ref == "convey-ref" - assert snapshot.spl_connection_state == "connected" - assert snapshot.listen_generation == 7 - assert snapshot.observed_relay_origin == "https://link.solstone.app" - assert snapshot.secure_listener_bound_accepting is True - - fake.emit(_supervisor_status()) - observer.reverify_before_authorization(snapshot) - - assert fake.stopped is True - - -def test_reverify_refuses_generation_change( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = _journal(tmp_path) - fake = _install_fake_connection(monkeypatch) - _install_process_and_socket(monkeypatch) - - with spl_readiness.open_spl_readiness_observer(journal) as observer: - fake.emit(_supervisor_status()) - fake.emit(_link_health(generation=7)) - snapshot = observer.wait_snapshot(deadline=time.monotonic() + 1) - fake.emit(_link_health(generation=8)) - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - observer.reverify_before_authorization(snapshot) - - assert excinfo.value.code == "link_generation_changed" - - -def test_reverify_refuses_process_replacement( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = _journal(tmp_path) - fake = _install_fake_connection(monkeypatch) - create_times = {101: 10.0, 202: 20.0} - _install_process_and_socket(monkeypatch, create_times=create_times) - - with spl_readiness.open_spl_readiness_observer(journal) as observer: - fake.emit(_supervisor_status()) - fake.emit(_link_health(generation=7)) - snapshot = observer.wait_snapshot(deadline=time.monotonic() + 1) - create_times[101] = 11.0 - fake.emit(_supervisor_status()) - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - observer.reverify_before_authorization(snapshot) - - assert excinfo.value.code == "process_replaced" - - -def test_reverify_refuses_stale_supervisor_status( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = _journal(tmp_path) - fake = _install_fake_connection(monkeypatch) - _install_process_and_socket(monkeypatch) - now = [100.0] - monkeypatch.setattr(spl_readiness.time, "monotonic", lambda: now[0]) - - with spl_readiness.open_spl_readiness_observer(journal) as observer: - fake.emit(_supervisor_status()) - fake.emit(_link_health(generation=7)) - snapshot = observer.wait_snapshot(deadline=101.0) - now[0] = 100.0 + spl_readiness.STATUS_MAX_AGE_SECONDS + 0.01 - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - observer.reverify_before_authorization(snapshot) - - assert excinfo.value.code == "supervisor_status_stale" - - -@pytest.mark.parametrize( - ("case", "code"), - [ - ("missing_spl", "spl_service_missing"), - ("missing_convey", "convey_service_missing"), - ("crashed_spl", "service_crashed"), - ("missing_link", "link_health_missing"), - ("link_not_connected", "link_state_not_connected"), - ("listener_unavailable", "secure_listener_unavailable"), - ], -) -def test_snapshot_refuses_unready_inputs( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - case: str, - code: str, -) -> None: - journal = _journal(tmp_path) - supervisor, link, socket_ok = _snapshot_case(case) - _install_process_and_socket(monkeypatch, socket_ok=socket_ok) - observer = spl_readiness.SplReadinessObserver(journal) - observer._latest_supervisor = (time.monotonic(), supervisor) - if link is not None: - observer._latest_link = (time.monotonic(), link) - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - observer._build_snapshot() - - assert excinfo.value.code == code - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "code", - [ - "supervisor_status_stale", - "spl_service_missing", - "convey_service_missing", - "service_crashed", - "link_health_missing", - "link_state_not_connected", - "secure_listener_unavailable", - ], -) -async def test_readiness_refusals_map_capability_not_ready_without_write_or_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - code: str, -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch, run_id=RUN_ID) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) - (journal / "config").mkdir(parents=True, exist_ok=True) - (journal / "config" / "journal.json").write_text("{}\n", encoding="utf-8") - attempt = write_attempt_dir(journal) - calls: list[str] = [] - - class RefusingObserver: - def __enter__(self) -> RefusingObserver: - return self - - def __exit__(self, *_args: object) -> None: - return None - - def wait_snapshot(self, *, deadline: float | None = None) -> object: - raise spl_readiness.SplReadinessError(code) - - async def enroll(*_args: object, **_kwargs: object) -> object: - calls.append("enroll") - raise AssertionError("enrollment must not be reached") - - async def dial(*_args: object, **_kwargs: object) -> object: - calls.append("dial") - raise AssertionError("dial must not be reached") - - monkeypatch.setattr( - probe.spl_readiness, - "open_spl_readiness_observer", - lambda _journal: RefusingObserver(), - ) - monkeypatch.setattr(link_client.Client, "enroll_device_async", enroll) - monkeypatch.setattr(probe, "_dial_with_deadline", dial) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == () - assert calls == [] - assert not (journal / "link" / "authorized_clients.json").exists() - - -def test_config_relay_url_refuses_even_when_default(tmp_path: Path) -> None: - journal = _journal(tmp_path) - (journal / "config" / "journal.json").write_text( - json.dumps({"link": {"relay_url": "https://link.solstone.app"}}, indent=2) - + "\n", - encoding="utf-8", - ) - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - spl_readiness.observed_relay_origin(journal) - - assert excinfo.value.code == "relay_config_override" - - -def test_env_relay_url_refuses( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal = _journal(tmp_path) - monkeypatch.setenv("SOL_LINK_RELAY_URL", "https://elsewhere.test") - - with pytest.raises(spl_readiness.SplReadinessError) as excinfo: - spl_readiness.observed_relay_origin(journal) - - assert excinfo.value.code == "relay_env_override" - - -def _journal(tmp_path: Path) -> Path: - journal = tmp_path / "journal" - (journal / "health").mkdir(parents=True) - (journal / "health" / "callosum.sock").write_text("", encoding="utf-8") - (journal / "config").mkdir() - (journal / "config" / "journal.json").write_text("{}\n", encoding="utf-8") - return journal - - -class _FakeCallosumConnection: - instance: _FakeCallosumConnection | None = None - - def __init__(self, socket_path: Path) -> None: - self.socket_path = socket_path - self.callback = None - self.stopped = False - _FakeCallosumConnection.instance = self - - def start(self, callback) -> None: - self.callback = callback - - def stop(self) -> None: - self.stopped = True - - def emit(self, message: dict[str, Any]) -> None: - assert self.callback is not None - self.callback(message) - - -def _install_fake_connection(monkeypatch: pytest.MonkeyPatch) -> _FakeCallosumProxy: - _FakeCallosumConnection.instance = None - monkeypatch.setattr(spl_readiness, "CallosumConnection", _FakeCallosumConnection) - return _FakeCallosumProxy() - - -class _FakeCallosumProxy: - @property - def stopped(self) -> bool: - assert _FakeCallosumConnection.instance is not None - return _FakeCallosumConnection.instance.stopped - - def emit(self, message: dict[str, Any]) -> None: - assert _FakeCallosumConnection.instance is not None - _FakeCallosumConnection.instance.emit(message) - - -def _install_process_and_socket( - monkeypatch: pytest.MonkeyPatch, - *, - create_times: dict[int, float] | None = None, - socket_ok: bool = True, -) -> None: - create_times = create_times if create_times is not None else {101: 10.0, 202: 20.0} - - class FakeProcess: - def __init__(self, pid: int) -> None: - self.pid = pid - - def create_time(self) -> float: - return create_times[self.pid] - - class FakeSocket: - def __enter__(self) -> FakeSocket: - return self - - def __exit__(self, *_args: object) -> None: - return None - - monkeypatch.setattr(spl_readiness.psutil, "Process", FakeProcess) - monkeypatch.setattr(spl_readiness.os, "kill", lambda _pid, _sig: None) - if socket_ok: - monkeypatch.setattr( - spl_readiness.socket, - "create_connection", - lambda _addr, timeout: FakeSocket(), - ) - else: - monkeypatch.setattr( - spl_readiness.socket, - "create_connection", - lambda _addr, timeout: (_ for _ in ()).throw(OSError("refused")), - ) - - -def _snapshot_case( - case: str, -) -> tuple[dict[str, object], dict[str, object] | None, bool]: - if case == "missing_spl": - return _supervisor_status(omit=("spl",)), _link_health(generation=7), True - if case == "missing_convey": - return _supervisor_status(omit=("convey",)), _link_health(generation=7), True - if case == "crashed_spl": - return ( - _supervisor_status(omit=("spl",), crashed=("spl",)), - _link_health(generation=7), - True, - ) - if case == "missing_link": - return _supervisor_status(), None, True - if case == "link_not_connected": - return ( - _supervisor_status(), - _link_health(generation=7, state="connecting"), - True, - ) - if case == "listener_unavailable": - return _supervisor_status(), _link_health(generation=7), False - raise AssertionError(f"unknown case {case}") - - -def _supervisor_status( - *, - omit: tuple[str, ...] = (), - crashed: tuple[str, ...] = (), -) -> dict[str, object]: - services = [ - {"name": "supervisor", "ref": "supervisor-ref", "pid": 1}, - {"name": "spl", "ref": "spl-ref", "pid": 101}, - {"name": "convey", "ref": "convey-ref", "pid": 202}, - ] - return { - "tract": "supervisor", - "event": "status", - "services": [ - service for service in services if str(service["name"]) not in omit - ], - "crashed": [{"name": name, "restart_attempts": 1} for name in crashed], - } - - -def _link_health(*, generation: int, state: str = "connected") -> dict[str, object]: - return { - "tract": "link", - "event": "health", - "state": state, - "listen_generation": generation, - } diff --git a/tests/sandbox_profile/test_spl_relay_tunnel.py b/tests/sandbox_profile/test_spl_relay_tunnel.py deleted file mode 100644 index 2f32d48b3..000000000 --- a/tests/sandbox_profile/test_spl_relay_tunnel.py +++ /dev/null @@ -1,1235 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import asyncio -import contextlib -import copy -import dataclasses -import json -import logging -import pickle -import sys -import threading -from pathlib import Path -from typing import Any - -import pytest -from websockets.exceptions import ConnectionClosedOK - -from solstone.think.journal_io import write_json -from solstone.think.link import client as link_client -from solstone.think.link.ca import cert_fingerprint, generate_ca -from solstone.think.sandbox_profile import probe_contract, spl_readiness -from solstone.think.sandbox_profile import spl_relay_tunnel as probe -from tests.link.secure_listener_harness import SecureListenerHarness -from tests.sandbox_profile import RUN_ID, sandbox_journal, write_attempt_dir - -CANARY_TOKEN = "device-token-canary" -CANARY_PRIVATE = "private-key-canary" -CANARIES = (CANARY_TOKEN, CANARY_PRIVATE) - - -@pytest.mark.asyncio -async def test_pass_path_binds_fingerprint_preserves_store_and_closes( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - unrelated = _seed_unrelated_authorizations(journal) - captured: dict[str, Any] = {} - _install_fake_readiness(monkeypatch) - _install_unreachable_path_canaries(monkeypatch) - real_build_identity = probe._build_ephemeral_identity - - def build_identity_with_private_canary( - journal_arg: Path, - attempt_arg: Path, - ) -> link_client.ClientIdentity: - identity = real_build_identity(journal_arg, attempt_arg) - return dataclasses.replace(identity, private_key_pem=CANARY_PRIVATE) - - async def fake_enroll( - _relay_url: str, - identity: link_client.ClientIdentity, - *, - timeout: float = 30.0, - ) -> link_client.EnrolledDevice: - captured["identity"] = identity - captured["timeout"] = timeout - return link_client.EnrolledDevice(device_token=CANARY_TOKEN, identity=identity) - - async def fake_dial( - enrolled: link_client.EnrolledDevice, - _deadline: float, - ) -> FakeSession: - captured["enrolled"] = enrolled - return FakeSession(identity=enrolled.identity) - - monkeypatch.setattr( - probe, "_build_ephemeral_identity", build_identity_with_private_canary - ) - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - monkeypatch.setattr(probe, "_dial_with_deadline", fake_dial) - caplog.set_level("DEBUG") - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is not None - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - assert outcome["checks"] == probe.SPL_CHECKS - assert outcome["reason"] is None - assert isinstance(outcome["duration_ms"], int) and outcome["duration_ms"] >= 0 - identity = captured["identity"] - assert cert_fingerprint(identity.client_cert_pem) == identity.fingerprint - assert ( - probe._attestation_device_fp(identity.home_attestation) == identity.fingerprint - ) - assert identity.private_key_pem == CANARY_PRIVATE - secrets = (*CANARIES, identity.fingerprint) - auth_payload = _load_auth(journal) - attempt_entry = next( - item for item in auth_payload if item["device_label"].startswith("sandbox-spl-") - ) - assert attempt_entry["fingerprint"] == identity.fingerprint - assert _canonical(_without_attempt(auth_payload)) == _canonical(unrelated) - outcome_json = json.dumps(outcome, sort_keys=True) - for secret in secrets: - assert secret not in outcome_json - assert secret not in repr(lease) - assert secret not in caplog.text - assert secret not in " ".join(sys.argv) - - for operation in (copy.copy, copy.deepcopy, pickle.dumps): - with pytest.raises(TypeError) as excinfo: - operation(lease) - assert excinfo.value.__cause__ is None - receipt = lease._receipt - assert receipt is not None - for secret in secrets: - assert secret not in repr(receipt) - for operation in (copy.copy, copy.deepcopy, pickle.dumps): - with pytest.raises(TypeError) as excinfo: - operation(receipt) - assert excinfo.value.__cause__ is None - - await lease.close() - assert lease.is_closed - assert _canonical(_load_auth(journal)) == _canonical(unrelated) - names = " ".join( - [task.get_name() for task in asyncio.all_tasks()] - + [thread.name for thread in threading.enumerate()] - ) - for secret in secrets: - assert secret not in names - _assert_canaries_absent_from_journal(journal, secrets) - - -@pytest.mark.asyncio -async def test_lease_close_succeeds_after_touch_last_seen_normalizes_unrelated_entries( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - _seed_unrelated_authorizations(journal) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "a" * 64, - device_label="sandbox-spl-normalized", - instance_id="instance", - ) - lease = probe.RelayTunnelLease( - session=FakeSession(), - store=store, - receipt=receipt, - ) - - assert store.touch_last_seen(receipt.fingerprint) - normalized_before_close = _without_attempt(_load_auth(journal)) - assert all( - "unmodeled" not in item and "future" not in item - for item in normalized_before_close - ) - - await lease.close() - - after_close = _load_auth(journal) - assert _canonical(after_close) == _canonical(normalized_before_close) - assert {item["fingerprint"] for item in after_close} == {"sha256:one", "sha256:two"} - assert lease.is_closed - - -@pytest.mark.asyncio -async def test_pin_override_during_readiness_refuses_before_write_or_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - calls: list[str] = [] - - class MutatingObserver: - def __enter__(self) -> MutatingObserver: - return self - - def __exit__(self, *_args: object) -> None: - return None - - def wait_snapshot(self, *, deadline: float | None = None): - monkeypatch.setenv("SOL_LINK_RELAY_URL", "https://late.example") - return spl_readiness.SplReadinessSnapshot( - supervisor_ref="supervisor", - spl_pid=1, - spl_ref="spl", - convey_pid=2, - convey_ref="convey", - spl_connection_state="connected", - listen_generation=1, - link_health_observed_at_monotonic=1.0, - observed_relay_origin="https://link.solstone.app", - secure_listener_bound_accepting=True, - supervisor_observed_at_monotonic=1.0, - ) - - def reverify_before_authorization(self, _snapshot) -> None: - return None - - async def enroll(*_args: object, **_kwargs: object) -> object: - calls.append("enroll") - raise AssertionError("enrollment must not be reached") - - async def dial(*_args: object, **_kwargs: object) -> object: - calls.append("dial") - raise AssertionError("dial must not be reached") - - monkeypatch.setattr( - probe.spl_readiness, - "open_spl_readiness_observer", - lambda _journal: MutatingObserver(), - ) - monkeypatch.setattr(link_client.Client, "enroll_device_async", enroll) - monkeypatch.setattr(probe, "_dial_with_deadline", dial) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == () - assert calls == [] - assert not (journal / "link" / "authorized_clients.json").exists() - - -@pytest.mark.asyncio -async def test_env_pin_refuses_before_write_or_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - monkeypatch.setenv("SOL_LINK_RELAY_URL", "https://elsewhere.test") - calls: list[str] = [] - monkeypatch.setattr( - link_client.Client, - "enroll_device_async", - lambda *args, **kwargs: calls.append("enroll"), - ) - monkeypatch.setattr( - probe, "_dial_with_deadline", lambda *args, **kwargs: calls.append("dial") - ) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["state"] == probe_contract.PROOF_STATE_FAILED - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == () - assert calls == [] - assert not (journal / "link" / "authorized_clients.json").exists() - - -@pytest.mark.asyncio -async def test_config_pin_refuses_before_write_or_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - write_json( - journal / "config" / "journal.json", - {"link": {"relay_url": "https://link.solstone.app"}}, - ) - calls: list[str] = [] - monkeypatch.setattr( - link_client.Client, - "enroll_device_async", - lambda *args, **kwargs: calls.append("enroll"), - ) - monkeypatch.setattr( - probe, "_dial_with_deadline", lambda *args, **kwargs: calls.append("dial") - ) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == () - assert calls == [] - assert not (journal / "link" / "authorized_clients.json").exists() - - -@pytest.mark.asyncio -async def test_strict_store_rejection_at_create_maps_capability_not_ready( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - auth_path = journal / "link" / "authorized_clients.json" - write_json( - auth_path, - [ - {"fingerprint": "sha256:dup", "device_label": "one"}, - {"fingerprint": "sha256:dup", "device_label": "two"}, - ], - ) - before = auth_path.read_text("utf-8") - _install_fake_readiness(monkeypatch) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert outcome["checks"] == () - assert auth_path.read_text("utf-8") == before - - -@pytest.mark.asyncio -async def test_work_deadline_is_exactly_sixty_seconds( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - deadlines: list[float | None] = [] - monkeypatch.setattr(probe.time, "monotonic", lambda: 100.0) - - class RefusingObserver: - def __enter__(self) -> RefusingObserver: - return self - - def __exit__(self, *_args: object) -> None: - return None - - def wait_snapshot(self, *, deadline: float | None = None) -> object: - deadlines.append(deadline) - raise spl_readiness.SplReadinessError("readiness_window_timeout") - - monkeypatch.setattr( - probe.spl_readiness, - "open_spl_readiness_observer", - lambda _journal: RefusingObserver(), - ) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CAPABILITY_NOT_READY - assert deadlines == [100.0 + probe.WORK_DEADLINE_SECONDS] - - -@pytest.mark.asyncio -async def test_enrollment_deadline_cap_clamp_and_boundary_expiry( - monkeypatch: pytest.MonkeyPatch, -) -> None: - identity = _dummy_identity() - waits: list[float] = [] - enroll_timeouts: list[float] = [] - now = [100.0] - monkeypatch.setattr(probe.time, "monotonic", lambda: now[0]) - - async def fake_wait_for(coro, *, timeout: float): - waits.append(timeout) - return await coro - - async def fake_enroll( - _relay_url: str, - identity_arg: link_client.ClientIdentity, - *, - timeout: float = 30.0, - ) -> link_client.EnrolledDevice: - enroll_timeouts.append(timeout) - return link_client.EnrolledDevice(device_token="token", identity=identity_arg) - - monkeypatch.setattr(probe.asyncio, "wait_for", fake_wait_for) - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - - await probe._enroll_with_deadline(identity, now[0] + 120.0) - await probe._enroll_with_deadline(identity, now[0] + 12.5) - now[0] = 130.0 - with pytest.raises(probe.SplRelayTunnelError) as excinfo: - await probe._enroll_with_deadline(identity, now[0]) - - assert waits == [probe.ENROLLMENT_DEADLINE_SECONDS, 12.5] - assert enroll_timeouts == [probe.ENROLLMENT_DEADLINE_SECONDS, 12.5] - assert excinfo.value.reason == probe_contract.REASON_DEADLINE_EXCEEDED - - -@pytest.mark.asyncio -async def test_cleanup_shield_is_independent_after_work_budget_expired( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "c" * 64, - device_label="sandbox-spl-cleanup", - instance_id="instance", - ) - session = FakeSession() - waits: list[float] = [] - monkeypatch.setattr(probe.time, "monotonic", lambda: 10_000.0) - - async def fake_wait_for(coro, *, timeout: float): - waits.append(timeout) - return await coro - - monkeypatch.setattr(probe.asyncio, "wait_for", fake_wait_for) - - assert await probe._cleanup_with_shield( - session=session, - store=store, - receipt=receipt, - ) - - assert waits == [probe.CLEANUP_SHIELD_SECONDS] - assert session.closed is True - assert _load_auth(journal) == [] - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("name", "setup", "reason", "checks"), - [ - ( - "enrollment_http_error", - lambda monkeypatch, tracker: _patch_enroll_error( - monkeypatch, - RuntimeError("POST https://link.solstone.app failed: HTTP 403"), - ), - probe_contract.REASON_REMOTE_REJECTED, - tuple(), - ), - ( - "malformed_enrollment_response", - lambda monkeypatch, tracker: _patch_enroll_error( - monkeypatch, RuntimeError("missing string field device_token") - ), - probe_contract.REASON_RESPONSE_INVALID, - tuple(), - ), - ( - "relay_upgrade_refusal", - lambda monkeypatch, tracker: _patch_enroll_success_and_connect_error( - monkeypatch, tracker, RuntimeError("HTTP 403 upgrade refused") - ), - probe_contract.REASON_REMOTE_REJECTED, - tuple(probe.SPL_CHECKS[:1]), - ), - ( - "malformed_relay_upgrade", - lambda monkeypatch, tracker: _patch_enroll_success_and_connect_error( - monkeypatch, tracker, RuntimeError("malformed relay upgrade") - ), - probe_contract.REASON_RESPONSE_INVALID, - tuple(probe.SPL_CHECKS[:1]), - ), - ( - "inner_tls_authorization_refusal", - lambda monkeypatch, tracker: _patch_open_session_error( - monkeypatch, tracker, RuntimeError("certificate rejected") - ), - probe_contract.REASON_REMOTE_REJECTED, - tuple(probe.SPL_CHECKS[:2]), - ), - ( - "malformed_mux_success", - lambda monkeypatch, tracker: _patch_open_session_result( - monkeypatch, tracker, FakeSession(is_alive=False) - ), - probe_contract.REASON_RESPONSE_INVALID, - tuple(probe.SPL_CHECKS[:2]), - ), - ( - "unexpected_local_failure", - lambda monkeypatch, tracker: monkeypatch.setattr( - probe, - "_build_ephemeral_identity", - lambda *_args: (_ for _ in ()).throw(RuntimeError("boom")), - ), - probe_contract.REASON_INTERNAL_ERROR, - tuple(), - ), - ], -) -async def test_failure_table_rows_and_non_transferred_cleanup( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - name: str, - setup, - reason: str, - checks: tuple[str, ...], -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - _install_fake_readiness(monkeypatch) - tracker: dict[str, Any] = {"websockets": []} - setup(monkeypatch, tracker) - - lease, outcome = await _assert_no_task_or_thread_leak( - probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - - assert lease is None, name - assert outcome["state"] == probe_contract.PROOF_STATE_FAILED - assert outcome["reason"] == reason - assert outcome["checks"] == checks - assert _load_auth(journal) == [] - for ws in tracker["websockets"]: - assert ws.closed is True - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("name", "setup", "checks"), - [ - ( - "enrollment_timeout", - lambda monkeypatch, tracker: _patch_enroll_error( - monkeypatch, _NamedTimeoutException("request timed out") - ), - tuple(), - ), - ( - "relay_connect_timeout", - lambda monkeypatch, tracker: _patch_enroll_success_and_connect_error( - monkeypatch, tracker, asyncio.TimeoutError() - ), - tuple(probe.SPL_CHECKS[:1]), - ), - ( - "inner_tls_timeout", - lambda monkeypatch, tracker: _patch_open_session_error( - monkeypatch, tracker, asyncio.TimeoutError() - ), - tuple(probe.SPL_CHECKS[:2]), - ), - ], -) -async def test_deadline_exceeded_failure_rows( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - name: str, - setup, - checks: tuple[str, ...], -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - _install_fake_readiness(monkeypatch) - tracker: dict[str, Any] = {"websockets": []} - setup(monkeypatch, tracker) - - lease, outcome = await _assert_no_task_or_thread_leak( - probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - - assert lease is None, name - assert outcome["reason"] == probe_contract.REASON_DEADLINE_EXCEEDED - assert outcome["checks"] == checks - assert _load_auth(journal) == [] - for ws in tracker["websockets"]: - assert ws.closed is True - - -@pytest.mark.asyncio -async def test_cancel_after_authorization_cleans_then_reraises( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - _install_fake_readiness(monkeypatch) - - async def cancel_enroll( - *_args: object, **_kwargs: object - ) -> link_client.EnrolledDevice: - raise asyncio.CancelledError - - monkeypatch.setattr(link_client.Client, "enroll_device_async", cancel_enroll) - - with pytest.raises(asyncio.CancelledError): - await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert _load_auth(journal) == [] - - -@pytest.mark.asyncio -async def test_cancel_during_cleanup_shield_reports_ambiguous_cleanup( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - _install_fake_readiness(monkeypatch) - cleanup_started = asyncio.Event() - release_cleanup = asyncio.Event() - - async def cancel_enroll( - *_args: object, **_kwargs: object - ) -> link_client.EnrolledDevice: - raise asyncio.CancelledError - - async def ambiguous_cleanup(**_kwargs: object) -> bool: - cleanup_started.set() - await release_cleanup.wait() - return False - - monkeypatch.setattr(link_client.Client, "enroll_device_async", cancel_enroll) - monkeypatch.setattr( - probe, "_cleanup_transport_and_authorization", ambiguous_cleanup - ) - - task = asyncio.create_task( - probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - ) - await cleanup_started.wait() - task.cancel() - release_cleanup.set() - - lease, outcome = await task - - assert lease is None - assert outcome["reason"] == probe_contract.REASON_CLEANUP_UNVERIFIED - assert outcome["checks"] == () - - -@pytest.mark.asyncio -async def test_cancel_before_authorization_raises_without_cleanup_or_store_change( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - cleanup_calls: list[str] = [] - monkeypatch.setattr( - probe, - "_cleanup_with_shield", - lambda **_kwargs: cleanup_calls.append("cleanup"), - ) - - with pytest.raises(asyncio.CancelledError): - await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: True, - ) - - assert cleanup_calls == [] - assert not (journal / "link" / "authorized_clients.json").exists() - - -@pytest.mark.asyncio -async def test_lease_close_failure_is_retryable( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "b" * 64, - device_label="sandbox-spl-retry", - instance_id="instance", - ) - session = FakeSession() - lease = probe.RelayTunnelLease(session=session, store=store, receipt=receipt) - calls = 0 - real_remove = store.remove_attempt_client_strict - - def flaky_remove(receipt_arg) -> None: - nonlocal calls - calls += 1 - if calls == 1: - raise probe.StrictAuthorizationError("forced") - real_remove(receipt_arg) - - monkeypatch.setattr(store, "remove_attempt_client_strict", flaky_remove) - - with pytest.raises(probe.RelayTunnelCloseError) as excinfo: - await lease.close() - - assert excinfo.value.reason == probe_contract.REASON_CLEANUP_UNVERIFIED - assert not lease.is_closed - await lease.close() - assert lease.is_closed - await lease.close() - - -@pytest.mark.asyncio -async def test_lease_lifecycle_consumer_not_called_closes_cleanly( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "f" * 64, - device_label="sandbox-spl-not-called", - instance_id="instance", - ) - session = FakeSession() - lease = probe.RelayTunnelLease(session=session, store=store, receipt=receipt) - - await lease.close() - - assert lease.is_closed - assert session.closed is True - assert _load_auth(journal) == [] - - -@pytest.mark.asyncio -async def test_lease_lifecycle_consumer_raised_still_closes( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "d" * 64, - device_label="sandbox-spl-raised", - instance_id="instance", - ) - session = FakeSession() - lease = probe.RelayTunnelLease(session=session, store=store, receipt=receipt) - - with pytest.raises(ValueError, match="consumer failed"): - async with lease: - raise ValueError("consumer failed") - - assert lease.is_closed - assert session.closed is True - assert _load_auth(journal) == [] - - -@pytest.mark.asyncio -async def test_lease_double_close_after_success_is_noop( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - journal, _attempt = _prepared_journal(tmp_path, monkeypatch) - store = probe.AuthorizedClients(journal / "link" / "authorized_clients.json") - receipt = store.add_attempt_client_strict( - fingerprint="sha256:" + "e" * 64, - device_label="sandbox-spl-double-close", - instance_id="instance", - ) - session = FakeSession() - lease = probe.RelayTunnelLease(session=session, store=store, receipt=receipt) - - await lease.close() - await lease.close() - - assert lease.is_closed - assert session.close_calls == 1 - assert _load_auth(journal) == [] - - -@pytest.mark.asyncio -@pytest.mark.parametrize("mode", ["success", "exception", "cancellation"]) -async def test_proof_log_filter_attaches_only_specific_logger_and_detaches( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, - mode: str, -) -> None: - journal, attempt = _prepared_journal(tmp_path, monkeypatch) - _install_fake_readiness(monkeypatch) - logger = logging.getLogger(link_client.LOG.name) - probe_logger = logging.getLogger(probe.__name__) - before_filters = list(logger.filters) - seen_filters: list[list[logging.Filter]] = [] - - async def fake_enroll( - _relay_url: str, - identity: link_client.ClientIdentity, - *, - timeout: float = 30.0, - ) -> link_client.EnrolledDevice: - seen_filters.append(list(logger.filters)) - assert probe_logger.filters == [] - if mode == "exception": - raise RuntimeError("POST failed: HTTP 403") - if mode == "cancellation": - raise asyncio.CancelledError - return link_client.EnrolledDevice(device_token=CANARY_TOKEN, identity=identity) - - async def fake_dial( - _enrolled: link_client.EnrolledDevice, - _deadline: float, - ) -> FakeSession: - return FakeSession() - - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - monkeypatch.setattr(probe, "_dial_with_deadline", fake_dial) - - if mode == "cancellation": - with pytest.raises(asyncio.CancelledError): - await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - else: - lease, _outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - if lease is not None: - await lease.close() - - assert seen_filters and len(seen_filters[0]) == len(before_filters) + 1 - assert logger.filters == before_filters - assert probe_logger.filters == [] - - caplog.set_level(logging.INFO, logger=link_client.LOG.name) - link_client.LOG.info("client %s: enrolling device token", "sha256:normal") - assert "sha256:normal" in caplog.text - - -@pytest.mark.asyncio -async def test_offline_real_inner_tls_and_mux_with_relay_boundary_doubled( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - harness = await SecureListenerHarness.start(tmp_path, monkeypatch) - try: - import solstone.convey.root as convey_root - import solstone.convey.secure_listener.runtime as secure_runtime - - monkeypatch.setattr( - secure_runtime, - "get_authorized_clients", - lambda: harness.authorized, - ) - monkeypatch.setattr( - convey_root, - "get_authorized_clients", - lambda: harness.authorized, - ) - journal = harness.journal - _write_state_and_marker(journal, monkeypatch) - attempt = write_attempt_dir(journal) - _install_fake_readiness(monkeypatch) - - async def fake_enroll( - _relay_url: str, - identity: link_client.ClientIdentity, - *, - timeout: float = 30.0, - ) -> link_client.EnrolledDevice: - return link_client.EnrolledDevice( - device_token="offline-token", identity=identity - ) - - class TcpBackedWebSocket: - def __init__( - self, - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - ) -> None: - self._reader = reader - self._writer = writer - self._inbound: asyncio.Queue[bytes | None] = asyncio.Queue() - self._pump = asyncio.create_task( - self._pump_reader(), - name="spl-proof-test-ws-pump", - ) - self.closed = False - - async def _pump_reader(self) -> None: - try: - while True: - data = await self._reader.read(65536) - if not data: - break - self._inbound.put_nowait(data) - finally: - self._inbound.put_nowait(None) - - async def send(self, data: bytes) -> None: - self._writer.write(data) - await self._writer.drain() - - async def recv(self) -> bytes: - data = await self._inbound.get() - if data is None: - raise ConnectionClosedOK(None, None) - return data - - async def close(self) -> None: - self.closed = True - self._writer.close() - with contextlib.suppress(Exception): - await self._writer.wait_closed() - self._pump.cancel() - with contextlib.suppress(asyncio.CancelledError): - await self._pump - - connect_calls: list[str] = [] - - async def fake_connect( - url: str, *, max_size: int | None = None - ) -> TcpBackedWebSocket: - connect_calls.append(url) - assert max_size is None - reader, writer = await asyncio.open_connection(harness.host, harness.port) - return TcpBackedWebSocket(reader, writer) - - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - monkeypatch.setattr(probe.websockets, "connect", fake_connect) - - lease, outcome = await probe.prove_spl_relay_tunnel( - journal, - attempt_dir=attempt, - cancel_requested=lambda: False, - ) - - assert lease is not None - assert outcome["state"] == probe_contract.PROOF_STATE_PASSED - assert connect_calls == [ - "wss://link.solstone.app/session/dial?instance=instance&token=offline-token" - ] - status, _headers, body = await lease.request("GET", "/app/network/api/status") - assert status == 200 - assert json.loads(body.decode("utf-8"))["posture"] == "spl" - await lease.close() - finally: - await harness.close() - - -def _prepared_journal( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> tuple[Path, Path]: - journal = sandbox_journal(tmp_path, monkeypatch, run_id=RUN_ID) - _write_state_and_marker(journal, monkeypatch) - generate_ca(journal / "link" / "ca") - return journal, write_attempt_dir(journal) - - -def _write_state_and_marker(journal: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) - write_json(journal / "config" / "journal.json", {"link": {"posture": "spl"}}) - write_json( - journal / "link" / "state.json", - {"instance_id": "instance", "home_label": "home"}, - ) - - -def _install_fake_readiness(monkeypatch: pytest.MonkeyPatch) -> None: - class FakeObserver: - def __enter__(self) -> FakeObserver: - return self - - def __exit__(self, *_args: object) -> None: - return None - - def wait_snapshot(self, *, deadline: float | None = None): - return spl_readiness.SplReadinessSnapshot( - supervisor_ref="supervisor", - spl_pid=1, - spl_ref="spl", - convey_pid=2, - convey_ref="convey", - spl_connection_state="connected", - listen_generation=1, - link_health_observed_at_monotonic=1.0, - observed_relay_origin="https://link.solstone.app", - secure_listener_bound_accepting=True, - supervisor_observed_at_monotonic=1.0, - ) - - def reverify_before_authorization(self, _snapshot) -> None: - return None - - monkeypatch.setattr( - probe.spl_readiness, - "open_spl_readiness_observer", - lambda _journal: FakeObserver(), - ) - - -def _install_unreachable_path_canaries(monkeypatch: pytest.MonkeyPatch) -> None: - from solstone.think.link import dialer, interface_watcher - - def fail(*_args: object, **_kwargs: object) -> None: - raise AssertionError("unreachable path entered") - - monkeypatch.setattr(link_client.Client, "dial_direct", fail) - monkeypatch.setattr(dialer, "open_tunnel", fail) - monkeypatch.setattr(dialer, "_dial_relay", fail) - monkeypatch.setattr(dialer, "TunnelClient", fail) - monkeypatch.setattr(interface_watcher.InterfaceWatcher, "start", fail) - - -class FakeSession: - def __init__( - self, - *, - is_alive: bool = True, - identity: link_client.ClientIdentity | None = None, - ) -> None: - self.is_alive = is_alive - self.closed = False - self.close_calls = 0 - self.websocket: FakeWebSocket | None = None - self._identity = identity - - async def request( - self, - _method: str, - _path: str, - *, - headers: dict[str, str] | None = None, - body: bytes | link_client.BodySource = b"", - ) -> tuple[int, dict[str, str], bytes]: - return 200, {}, b"{}" - - async def stream_request( - self, - _method: str, - _path: str, - *, - headers: dict[str, str] | None = None, - body: bytes | link_client.BodySource = b"", - ) -> tuple[int, dict[str, str], bytes, object]: - return 200, {}, b"{}", object() - - async def close(self) -> None: - self.close_calls += 1 - self.closed = True - if self.websocket is not None: - await self.websocket.close() - - -class FakeWebSocket: - def __init__(self) -> None: - self.closed = False - - async def close(self) -> None: - self.closed = True - - -class _NamedTimeoutException(Exception): - pass - - -_NamedTimeoutException.__name__ = "TimeoutException" - - -def _dummy_identity() -> link_client.ClientIdentity: - return link_client.ClientIdentity( - private_key_pem="private", - client_cert_pem="cert", - ca_chain_pem="ca", - fingerprint="sha256:dummy", - home_instance_id="instance", - home_label="home", - home_attestation="attestation", - local_endpoints=(), - ) - - -async def _assert_no_task_or_thread_leak(coro): - current = asyncio.current_task() - before_tasks = { - task for task in asyncio.all_tasks() if task is not current and not task.done() - } - before_threads = {thread.ident for thread in threading.enumerate()} - result = await coro - await asyncio.sleep(0) - after_tasks = { - task for task in asyncio.all_tasks() if task is not current and not task.done() - } - after_threads = {thread.ident for thread in threading.enumerate()} - assert after_tasks <= before_tasks - assert after_threads == before_threads - return result - - -def _patch_enroll_error( - monkeypatch: pytest.MonkeyPatch, - exc: Exception, -) -> None: - async def fake_enroll(*_args: object, **_kwargs: object) -> object: - raise exc - - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - - -def _patch_enroll_success_and_connect_error( - monkeypatch: pytest.MonkeyPatch, - tracker: dict[str, Any], - exc: Exception, -) -> None: - _patch_enroll_success(monkeypatch) - - async def fake_connect(*_args: object, **_kwargs: object) -> object: - tracker.setdefault("connect_attempts", 0) - tracker["connect_attempts"] += 1 - raise exc - - monkeypatch.setattr(probe.websockets, "connect", fake_connect) - - -def _patch_open_session_error( - monkeypatch: pytest.MonkeyPatch, - tracker: dict[str, Any], - exc: Exception, -) -> None: - _patch_enroll_success(monkeypatch) - ws = FakeWebSocket() - tracker.setdefault("websockets", []).append(ws) - - async def fake_connect(*_args: object, **_kwargs: object) -> FakeWebSocket: - return ws - - async def fake_open(*_args: object, **_kwargs: object) -> object: - raise exc - - monkeypatch.setattr(probe.websockets, "connect", fake_connect) - monkeypatch.setattr(link_client, "_open_tunnel_session", fake_open) - - -def _patch_open_session_result( - monkeypatch: pytest.MonkeyPatch, - tracker: dict[str, Any], - session: FakeSession, -) -> None: - _patch_enroll_success(monkeypatch) - ws = FakeWebSocket() - tracker.setdefault("websockets", []).append(ws) - session.websocket = ws - tracker["session"] = session - - async def fake_connect(*_args: object, **_kwargs: object) -> FakeWebSocket: - return ws - - async def fake_open(*_args: object, **_kwargs: object) -> FakeSession: - return session - - monkeypatch.setattr(probe.websockets, "connect", fake_connect) - monkeypatch.setattr(link_client, "_open_tunnel_session", fake_open) - - -def _patch_enroll_success(monkeypatch: pytest.MonkeyPatch) -> None: - async def fake_enroll( - _relay_url: str, - identity: link_client.ClientIdentity, - *, - timeout: float = 30.0, - ) -> link_client.EnrolledDevice: - return link_client.EnrolledDevice(device_token="token", identity=identity) - - monkeypatch.setattr(link_client.Client, "enroll_device_async", fake_enroll) - - -def _seed_unrelated_authorizations(journal: Path) -> list[dict[str, object]]: - payload = [ - { - "fingerprint": "sha256:one", - "device_label": "one", - "paired_at": "2026-01-01T00:00:00Z", - "instance_id": "instance", - "role": "", - "unmodeled": {"keep": True}, - }, - { - "fingerprint": "sha256:two", - "device_label": "two", - "paired_at": "2026-01-01T00:00:00Z", - "instance_id": "instance", - "role": "", - "future": "value", - }, - ] - write_json(journal / "link" / "authorized_clients.json", payload) - return payload - - -def _load_auth(journal: Path) -> list[dict[str, object]]: - path = journal / "link" / "authorized_clients.json" - if not path.exists(): - return [] - payload = json.loads(path.read_text("utf-8")) - assert isinstance(payload, list) - return payload - - -def _without_attempt(items: list[dict[str, object]]) -> list[dict[str, object]]: - return [ - item - for item in items - if not str(item.get("device_label", "")).startswith("sandbox-spl-") - ] - - -def _canonical(items: list[dict[str, object]]) -> tuple[str, ...]: - return tuple( - json.dumps(item, ensure_ascii=False, separators=(",", ":")) for item in items - ) - - -def _assert_canaries_absent_from_journal( - journal: Path, - canaries: tuple[str, ...] = CANARIES, -) -> None: - for path in journal.rglob("*"): - for canary in canaries: - assert canary not in path.name - if path.is_file(): - data = path.read_text("utf-8", errors="ignore") - for canary in canaries: - assert canary not in data diff --git a/tests/sandbox_profile/test_status.py b/tests/sandbox_profile/test_status.py deleted file mode 100644 index 2fd46a9c4..000000000 --- a/tests/sandbox_profile/test_status.py +++ /dev/null @@ -1,194 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json - -from tests.sandbox_profile import ( - invoke, - output_json, - prepare_ok, - read_json, - sandbox_journal, - scout_payload, - spb_payload, - spl_payload, - spp_payload, -) - - -def test_status_reports_intent_observed_split_without_network( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "scout", "--json"], input_text=json.dumps(scout_payload()) - ) - assert result.exit_code == 0 - config_path = journal / "config" / "journal.json" - config = json.loads(config_path.read_text("utf-8")) - config["env"].pop("GOOGLE_API_KEY") - config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") - - monkeypatch.setattr( - "solstone.think.services.spl.enroll_home", - lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("network")), - ) - monkeypatch.setattr( - "solstone.think.services.portal_client.build_consent_url", - lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("portal")), - ) - monkeypatch.setattr( - "solstone.think.backup.hosted.fetch_hosted_credentials", - lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("broker")), - ) - - status = invoke(["status", "--json"]) - body = output_json(status) - scout = next(cap for cap in body["capabilities"] if cap["name"] == "scout") - - assert status.exit_code == 3 - assert body["state"] == "degraded" - assert scout["state"] == "degraded" - assert "scout_block_missing" in scout["residuals"] - - -def test_status_refuses_intent_run_mismatch_without_mutating( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - intent_path = journal / "health" / "sandbox-profile" / "intent.json" - payload = json.loads(intent_path.read_text("utf-8")) - payload["run_id"] = "11111111-1111-4111-8111-111111111111" - intent_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - - result = invoke(["status", "--json"]) - body = output_json(result) - - assert result.exit_code == 2 - assert body["run_id"] == "86d9eb6c-d64e-4ae5-b29e-524ddf57a013" - assert body["error"]["code"] == "intent_run_mismatch" - - -def _capability(body: dict[str, object], name: str) -> dict[str, object]: - capabilities = body["capabilities"] - assert isinstance(capabilities, list) - return next(cap for cap in capabilities if cap["name"] == name) - - -def _assert_degraded_residual(result, name: str, residual: str) -> None: - body = output_json(result) - cap = _capability(body, name) - assert result.exit_code == 3 - assert body["state"] == "degraded" - assert cap["state"] == "degraded" - assert residual in cap["residuals"] - - -def test_status_reconciles_scout_key_fingerprint(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "scout", "--json"], input_text=json.dumps(scout_payload()) - ) - assert result.exit_code == 0 - config_path = journal / "config" / "journal.json" - config = read_json(config_path) - config["env"]["GOOGLE_API_KEY"] = "different-google-key" - config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "scout", "scout_key_fingerprint_mismatch") - - -def test_status_reconciles_scout_account_id(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "scout", "--json"], input_text=json.dumps(scout_payload()) - ) - assert result.exit_code == 0 - config_path = journal / "config" / "journal.json" - config = read_json(config_path) - config["services"]["scout"]["account_id"] = "acct-mismatch" - config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "scout", "scout_account_id_mismatch") - - -def test_status_refuses_legacy_scout_intent_without_account_id( - tmp_path, monkeypatch -) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "scout", "--json"], input_text=json.dumps(scout_payload()) - ) - assert result.exit_code == 0 - intent_path = journal / "health" / "sandbox-profile" / "intent.json" - payload = read_json(intent_path) - payload["observed_at_apply"]["scout"].pop("account_id") - intent_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "scout", "scout_account_id_mismatch") - - -def test_status_reconciles_spl_identity(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - monkeypatch.setattr( - "solstone.think.services.spl.enroll_home", - lambda *args, **kwargs: "service-token", - ) - result = invoke(["apply", "spl", "--json"], input_text=json.dumps(spl_payload())) - assert result.exit_code == 0 - state_path = journal / "link" / "state.json" - state = read_json(state_path) - state["instance_id"] = "did:key:mismatched" - state_path.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "spl", "spl_identity_missing") - - -def test_status_reconciles_spb_binding_instance(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke( - ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) - ) - assert result.exit_code == 0 - binding_path = journal / "backup" / "hosted" / "binding.json" - binding = read_json(binding_path) - binding["instance_id"] = "did:key:mismatched" - binding_path.write_text(json.dumps(binding, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "spb", "spb_instance_mismatch") - - -def test_status_reconciles_spp_credential_fingerprint(tmp_path, monkeypatch) -> None: - journal = sandbox_journal(tmp_path, monkeypatch) - prepare_ok(journal) - result = invoke(["apply", "spp", "--json"], input_text=json.dumps(spp_payload())) - assert result.exit_code == 0 - config_path = journal / "config" / "journal.json" - config = read_json(config_path) - config["services"]["confidential"]["credential_fingerprint_sha256"] = ( - "mismatched-fingerprint" - ) - config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") - - status = invoke(["status", "--json"]) - - _assert_degraded_residual(status, "spp", "spp_credential_fingerprint_mismatch") diff --git a/tests/test_native_sol_journal_host_commands.py b/tests/test_native_sol_journal_host_commands.py index 726a63d9d..cc71bafa8 100644 --- a/tests/test_native_sol_journal_host_commands.py +++ b/tests/test_native_sol_journal_host_commands.py @@ -8,9 +8,18 @@ import pytest import scripts.build_native_sol_journal_host_commands as journal_host -SERVICE_COMMANDS = ("setup", "think") + tuple(f"svc{i:02d}" for i in range(41)) +SERVICE_SENTINELS = ("setup", "think") +SERVICE_COMMANDS = SERVICE_SENTINELS + tuple( + f"svc{i:02d}" + for i in range( + journal_host.EXPECTED_SERVICE_COMMANDS_COUNT - len(SERVICE_SENTINELS) + ) +) UNIVERSAL_COMMANDS = ("check", "contract", "doctor", "link") SERVICE_ALIASES = ("down", "up") +SERVICE_SURFACE_COUNT = journal_host.EXPECTED_SERVICE_COMMANDS_COUNT + len( + SERVICE_ALIASES +) def _raw_source(lines: list[str]) -> str: @@ -96,7 +105,7 @@ def _raw_service_command_literal_count(source: str) -> int: def test_extract_keeps_service_commands_and_aliases_as_sorted_moved_list() -> None: moved = journal_host.extract(_source(_base_commands(), _base_aliases())) - assert len(moved) == 45 + assert len(moved) == SERVICE_SURFACE_COUNT assert moved == sorted((*SERVICE_COMMANDS, *SERVICE_ALIASES)) @@ -169,7 +178,6 @@ def test_duplicate_service_command_preserves_old_count_but_is_rejected() -> None ' "svc36": Command("module.svc36", "service"),', ' "svc37": Command("module.svc37", "service"),', ' "svc38": Command("module.svc38", "service"),', - ' "svc39": Command("module.svc39", "service"),', ' "check": Command("module.check", "universal"),', ' "contract": Command("module.contract", "universal"),', ' "doctor": Command("module.doctor", "universal"),', @@ -183,7 +191,10 @@ def test_duplicate_service_command_preserves_old_count_but_is_rejected() -> None ] ) - assert _raw_service_command_literal_count(source) == 43 + assert ( + _raw_service_command_literal_count(source) + == journal_host.EXPECTED_SERVICE_COMMANDS_COUNT + ) message = _raw_error(source) assert ( @@ -360,14 +371,13 @@ def test_spread_and_non_literal_keys_are_skipped_on_a_valid_source() -> None: moved = journal_host.extract(source) - assert len(moved) == 45 + assert len(moved) == SERVICE_SURFACE_COUNT assert moved == sorted((*SERVICE_COMMANDS, *SERVICE_ALIASES)) def test_production_registry_extracts_expected_partitions() -> None: partitions = journal_host.extract_partitions() - assert len(partitions.service_commands) == 43 assert set(partitions.service_aliases) == {"up", "down"} assert set(partitions.universal_commands) == { "doctor", @@ -376,32 +386,45 @@ def test_production_registry_extracts_expected_partitions() -> None: "link", } assert partitions.universal_aliases == () - assert len(journal_host.extract()) == 45 -def test_service_command_count_rejects_44_service_commands_and_1_alias() -> None: +def test_service_command_count_rejects_one_extra_service_command_despite_missing_alias() -> ( + None +): commands = _base_commands() commands["audio"] = "service" aliases = _base_aliases() del aliases["down"] - assert _old_combined_service_surface_count(commands, aliases) == 45 + assert ( + _old_combined_service_surface_count(commands, aliases) == SERVICE_SURFACE_COUNT + ) message = _error(commands, aliases) + expected = journal_host.EXPECTED_SERVICE_COMMANDS_COUNT - assert message.startswith("journal-host service COMMANDS count 44 != 43: ") + assert message.startswith( + f"journal-host service COMMANDS count {expected + 1} != {expected}: " + ) assert "'audio'" in message -def test_service_command_count_rejects_42_service_commands_and_3_aliases() -> None: +def test_service_command_count_rejects_one_missing_service_command_despite_extra_alias() -> ( + None +): commands = _base_commands() del commands["svc00"] aliases = _base_aliases() aliases["extra"] = "service" - assert _old_combined_service_surface_count(commands, aliases) == 45 + assert ( + _old_combined_service_surface_count(commands, aliases) == SERVICE_SURFACE_COUNT + ) message = _error(commands, aliases) + expected = journal_host.EXPECTED_SERVICE_COMMANDS_COUNT - assert message.startswith("journal-host service COMMANDS count 42 != 43: ") + assert message.startswith( + f"journal-host service COMMANDS count {expected - 1} != {expected}: " + ) @pytest.mark.parametrize("alias", ["up", "down"])