diff --git a/docs/integration/mutation-evidence-accountability-devtools.md b/docs/integration/mutation-evidence-accountability-devtools.md new file mode 100644 index 0000000..40e699a --- /dev/null +++ b/docs/integration/mutation-evidence-accountability-devtools.md @@ -0,0 +1,83 @@ +# Mutation and Evidence Accountability Devtools Integration + +Status: Proposed +Canonical spec: https://github.com/SourceOS-Linux/sourceos-spec/pull/96 +Tracking issue: https://github.com/SourceOS-Linux/sourceos-devtools/issues/25 + +## Purpose + +This document defines the first `sourceos-devtools` integration slice for SourceOS Mutation and Evidence Accountability. + +The canonical schemas, examples, anti-pattern fixtures, and validator live in `SourceOS-Linux/sourceos-spec`. This repository provides the operator/developer CLI wrapper so implementation repos can validate against one shared contract instead of copying schema logic. + +## CLI surface + +```bash +python3 bin/sourceosctl mutation-evidence plan +python3 bin/sourceosctl mutation-evidence inspect --spec-root ../sourceos-spec +python3 bin/sourceosctl mutation-evidence validate --spec-root ../sourceos-spec +python3 bin/sourceosctl mutation-evidence fixture-plan +``` + +## Commands + +### `mutation-evidence plan` + +Prints the integration plan, required canonical spec files, downstream implementation repos, and semantic guardrails. + +### `mutation-evidence inspect` + +Checks whether a local `sourceos-spec` checkout contains the required Mutation and Evidence Accountability files: + +- base schema bundle; +- umbrella primitive schema bundle; +- valid example fixtures; +- invalid anti-pattern fixtures; +- canonical validator. + +### `mutation-evidence validate` + +Runs the canonical validator from the `sourceos-spec` checkout. The validator checks: + +- base examples and anti-patterns; +- umbrella primitive examples and anti-patterns; +- no-visible-extension browser attribution guardrails; +- delegated I/O actor-chain completeness; +- diagnostic observer-effect clearance blocking; +- archive extraction path-boundary and cleanup accounting; +- blind/degraded/missing sensor compromise-clearance blocking. + +### `mutation-evidence fixture-plan` + +Prints the valid and invalid fixture classes that implementation repos should reproduce or consume. + +## Guardrails + +The devtools wrapper keeps five guardrails visible to all downstream implementation repos: + +1. No clearance when sensors are blind, degraded, or missing. +2. No extension-primary browser attribution when extension inventory is `none_visible`. +3. No delegated mutation without a complete actor chain. +4. No archive extraction without path-boundary and cleanup accounting. +5. No diagnostic observer-effect clearance with partial or redacted evidence. + +## Stack consumers + +This integration slice prepares implementation work in: + +- `SourceOS-Linux/BearBrowser` +- `SourceOS-Linux/TurtleTerm` +- `SourceOS-Linux/sourceos-shell` +- `SourceOS-Linux/sourceos-syncd` +- `SourceOS-Linux/sourceos-boot` +- `SocioProphet/prophet-platform` +- `SocioProphet/ontogenesis` +- `SocioProphet/exodus` + +## Acceptance criteria + +- `sourceosctl mutation-evidence plan` returns 0. +- `sourceosctl mutation-evidence inspect --spec-root ` fails when required spec files are missing. +- `sourceosctl mutation-evidence inspect --spec-root ` passes when required spec files exist. +- `sourceosctl mutation-evidence validate --spec-root ` returns the canonical validator exit code. +- Tests cover the command surface and validator propagation behavior. diff --git a/sourceosctl/cli.py b/sourceosctl/cli.py index 309c38f..81c6c01 100644 --- a/sourceosctl/cli.py +++ b/sourceosctl/cli.py @@ -15,6 +15,7 @@ local_model, agent_machine, office, + mutation_evidence, ) @@ -42,6 +43,46 @@ def build_parser() -> argparse.ArgumentParser: profiles_list_p = profiles_sub.add_parser("list", help="List available profiles") profiles_list_p.set_defaults(func=profiles.list_profiles) + # --- mutation-evidence --- + mutation_p = sub.add_parser( + "mutation-evidence", + help="Mutation and Evidence Accountability developer helpers", + ) + mutation_sub = mutation_p.add_subparsers( + dest="mutation_evidence_command", metavar="" + ) + mutation_sub.required = True + + def add_spec_root(p): + p.add_argument( + "--spec-root", + default="../sourceos-spec", + help="Path to a sourceos-spec checkout containing the canonical schemas and fixtures", + ) + + mutation_plan_p = mutation_sub.add_parser( + "plan", help="Render the mutation/evidence validation integration plan" + ) + add_spec_root(mutation_plan_p) + mutation_plan_p.set_defaults(func=mutation_evidence.plan) + + mutation_inspect_p = mutation_sub.add_parser( + "inspect", help="Inspect a sourceos-spec checkout for mutation/evidence contract files" + ) + add_spec_root(mutation_inspect_p) + mutation_inspect_p.set_defaults(func=mutation_evidence.inspect) + + mutation_validate_p = mutation_sub.add_parser( + "validate", help="Run the canonical sourceos-spec mutation/evidence validator" + ) + add_spec_root(mutation_validate_p) + mutation_validate_p.set_defaults(func=mutation_evidence.validate) + + mutation_fixture_p = mutation_sub.add_parser( + "fixture-plan", help="List required valid and invalid fixture classes" + ) + mutation_fixture_p.set_defaults(func=mutation_evidence.fixture_plan) + # --- nlboot --- nlboot_p = sub.add_parser("nlboot", help="NLBoot operator helpers") nlboot_sub = nlboot_p.add_subparsers(dest="nlboot_command", metavar="") diff --git a/sourceosctl/commands/mutation_evidence.py b/sourceosctl/commands/mutation_evidence.py new file mode 100644 index 0000000..67932bb --- /dev/null +++ b/sourceosctl/commands/mutation_evidence.py @@ -0,0 +1,139 @@ +"""Mutation and Evidence Accountability developer helpers. + +This module intentionally treats sourceos-spec as the canonical schema home. +Devtools provides the operator/developer CLI wrapper and posture checks so every +implementation repo can validate against the same contract instead of copying +schema logic. +""" +from __future__ import annotations + +import json +import pathlib +import subprocess +import sys +from dataclasses import dataclass + + +REQUIRED_SPEC_PATHS = [ + "schemas/MutationEvidenceAccountability.schema.json", + "schemas/MutationEvidenceUmbrellaPrimitives.schema.json", + "examples/mutation-evidence-accountability.examples.json", + "examples/mutation-evidence-umbrella.examples.json", + "fixtures/anti-patterns/mutation-evidence-accountability.invalid.json", + "fixtures/anti-patterns/mutation-evidence-umbrella.invalid.json", + "tools/validate_mutation_evidence_accountability.py", +] + +IMPLEMENTATION_REPOS = [ + "SourceOS-Linux/BearBrowser", + "SourceOS-Linux/TurtleTerm", + "SourceOS-Linux/sourceos-shell", + "SourceOS-Linux/sourceos-syncd", + "SourceOS-Linux/sourceos-devtools", + "SourceOS-Linux/sourceos-boot", + "SocioProphet/prophet-platform", + "SocioProphet/ontogenesis", + "SocioProphet/exodus", +] + +GUARDRAILS = [ + "no clearance when sensors are blind, degraded, or missing", + "no extension-primary browser attribution when extension inventory is none_visible", + "no delegated mutation without a complete actor chain", + "no archive extraction without path-boundary and cleanup accounting", + "no diagnostic observer-effect clearance with partial or redacted evidence", +] + + +@dataclass(frozen=True) +class Check: + name: str + status: str + detail: str + + +def _spec_root(args) -> pathlib.Path: + return pathlib.Path(args.spec_root).expanduser().resolve() + + +def _print_checks(checks: list[Check]) -> int: + rc = 0 + for check in checks: + marker = {"pass": "ok", "fail": "fail", "warn": "warn"}.get(check.status, check.status) + print(f"[{marker}] {check.name}: {check.detail}") + if check.status == "fail": + rc = 1 + return rc + + +def _collect_spec_checks(spec_root: pathlib.Path) -> list[Check]: + checks = [ + Check( + "spec-root", + "pass" if spec_root.exists() and spec_root.is_dir() else "fail", + str(spec_root), + ) + ] + for rel in REQUIRED_SPEC_PATHS: + path = spec_root / rel + checks.append(Check(rel, "pass" if path.exists() else "fail", str(path))) + return checks + + +def plan(args) -> int: + """Render the mutation/evidence devtools integration posture.""" + spec_root = _spec_root(args) + print("SourceOS Mutation and Evidence Accountability devtools plan") + print(f"spec_root: {spec_root}") + print("canonical_spec_pr: https://github.com/SourceOS-Linux/sourceos-spec/pull/96") + print("\nrequired spec files:") + for rel in REQUIRED_SPEC_PATHS: + print(f"- {rel}") + print("\nimplementation repos:") + for repo in IMPLEMENTATION_REPOS: + print(f"- {repo}") + print("\nsemantic guardrails:") + for guardrail in GUARDRAILS: + print(f"- {guardrail}") + return 0 + + +def inspect(args) -> int: + """Inspect whether a sourceos-spec checkout has the mutation/evidence contract.""" + return _print_checks(_collect_spec_checks(_spec_root(args))) + + +def validate(args) -> int: + """Run the canonical sourceos-spec mutation/evidence validator.""" + spec_root = _spec_root(args) + checks = _collect_spec_checks(spec_root) + check_rc = _print_checks(checks) + if check_rc != 0: + print("mutation/evidence validation skipped: required spec files are missing") + return check_rc + + validator = spec_root / "tools" / "validate_mutation_evidence_accountability.py" + cmd = [sys.executable, str(validator)] + print(f"running: {' '.join(cmd)}") + result = subprocess.run(cmd, cwd=str(spec_root), check=False, text=True) + return int(result.returncode) + + +def fixture_plan(args) -> int: + """Print the fixture classes expected by downstream implementation repos.""" + fixture_classes = { + "valid": [ + "browser write with no visible extensions", + "delegated sync chain with origin/requesting/execution/storage actors", + "terminal/archive extraction with path-boundary and cleanup accounting", + "diagnostic self-noise with clearance disabled when evidence is partial", + ], + "invalid": [ + "extension-primary browser write with extension_inventory_state=none_visible", + "delegated mutation without complete actor chain", + "diagnostic observer-effect receipt that allows clearance despite redaction", + "archive extraction with unknown path class or missing cleanup policy", + ], + } + print(json.dumps(fixture_classes, indent=2, sort_keys=True)) + return 0 diff --git a/tests/test_mutation_evidence_cli.py b/tests/test_mutation_evidence_cli.py new file mode 100644 index 0000000..9032bde --- /dev/null +++ b/tests/test_mutation_evidence_cli.py @@ -0,0 +1,74 @@ +"""Tests for SourceOS Mutation and Evidence Accountability devtools CLI.""" + +from __future__ import annotations + +import pathlib +import stat +import sys +import tempfile +import unittest + +_REPO_ROOT = pathlib.Path(__file__).parent.parent +sys.path.insert(0, str(_REPO_ROOT)) + +from sourceosctl.cli import main +from sourceosctl.commands import mutation_evidence + + +REQUIRED_SPEC_PATHS = mutation_evidence.REQUIRED_SPEC_PATHS + + +def _make_spec_root(tmpdir: pathlib.Path, validator_exit: int = 0) -> pathlib.Path: + """Create a minimal sourceos-spec-like tree for CLI tests.""" + spec_root = tmpdir / "sourceos-spec" + spec_root.mkdir() + for rel in REQUIRED_SPEC_PATHS: + path = spec_root / rel + path.parent.mkdir(parents=True, exist_ok=True) + if rel.endswith("validate_mutation_evidence_accountability.py"): + path.write_text( + "#!/usr/bin/env python3\n" + "import sys\n" + f"print('validator fixture exit={validator_exit}')\n" + f"raise SystemExit({validator_exit})\n" + ) + path.chmod(path.stat().st_mode | stat.S_IXUSR) + else: + path.write_text("{}\n") + return spec_root + + +class TestMutationEvidenceCli(unittest.TestCase): + def test_plan_returns_zero_for_default_spec_root(self): + rc = main(["mutation-evidence", "plan"]) + self.assertEqual(rc, 0) + + def test_fixture_plan_returns_zero(self): + rc = main(["mutation-evidence", "fixture-plan"]) + self.assertEqual(rc, 0) + + def test_inspect_missing_spec_root_fails(self): + rc = main(["mutation-evidence", "inspect", "--spec-root", "/nonexistent/sourceos-spec"]) + self.assertEqual(rc, 1) + + def test_inspect_complete_spec_root_passes(self): + with tempfile.TemporaryDirectory() as td: + spec_root = _make_spec_root(pathlib.Path(td)) + rc = main(["mutation-evidence", "inspect", "--spec-root", str(spec_root)]) + self.assertEqual(rc, 0) + + def test_validate_complete_spec_root_passes_when_validator_passes(self): + with tempfile.TemporaryDirectory() as td: + spec_root = _make_spec_root(pathlib.Path(td), validator_exit=0) + rc = main(["mutation-evidence", "validate", "--spec-root", str(spec_root)]) + self.assertEqual(rc, 0) + + def test_validate_complete_spec_root_fails_when_validator_fails(self): + with tempfile.TemporaryDirectory() as td: + spec_root = _make_spec_root(pathlib.Path(td), validator_exit=7) + rc = main(["mutation-evidence", "validate", "--spec-root", str(spec_root)]) + self.assertEqual(rc, 7) + + +if __name__ == "__main__": + unittest.main()