Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/integration/mutation-evidence-accountability-devtools.md
Original file line number Diff line number Diff line change
@@ -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 <path>` fails when required spec files are missing.
- `sourceosctl mutation-evidence inspect --spec-root <path>` passes when required spec files exist.
- `sourceosctl mutation-evidence validate --spec-root <path>` returns the canonical validator exit code.
- Tests cover the command surface and validator propagation behavior.
41 changes: 41 additions & 0 deletions sourceosctl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
local_model,
agent_machine,
office,
mutation_evidence,
)


Expand Down Expand Up @@ -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="<subcommand>"
)
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="<subcommand>")
Expand Down
139 changes: 139 additions & 0 deletions sourceosctl/commands/mutation_evidence.py
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions tests/test_mutation_evidence_cli.py
Original file line number Diff line number Diff line change
@@ -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()
Loading