generated from coulomb/repo-seed
feat: collect accountability root evidence
This commit is contained in:
@@ -27,6 +27,12 @@ Tenant/subfabric example:
|
|||||||
examples/discovery/accountability-root-manifest.yaml
|
examples/discovery/accountability-root-manifest.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Raw evidence run schema:
|
||||||
|
|
||||||
|
```text
|
||||||
|
schemas/accountability-root-evidence.schema.yaml
|
||||||
|
```
|
||||||
|
|
||||||
## Required Sections
|
## Required Sections
|
||||||
|
|
||||||
- `netkingdom`: root id, name, and king actor.
|
- `netkingdom`: root id, name, and king actor.
|
||||||
@@ -49,3 +55,26 @@ still rests on financial and operational accountability.
|
|||||||
Discovery roots should state `safe_discovery` explicitly. Secret and backup
|
Discovery roots should state `safe_discovery` explicitly. Secret and backup
|
||||||
roots should use `metadata_only` or `explicit_review`; adapters must never read
|
roots should use `metadata_only` or `explicit_review`; adapters must never read
|
||||||
secret values or operational telemetry while building Fabric graph evidence.
|
secret values or operational telemetry while building Fabric graph evidence.
|
||||||
|
|
||||||
|
## Collecting Root Evidence
|
||||||
|
|
||||||
|
The first adapter slice emits raw evidence without promoting it into accepted
|
||||||
|
graph snapshots:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
railiance-fabric discover-roots \
|
||||||
|
--manifest fabric/discovery/railiance-accountability-roots.yaml \
|
||||||
|
--max-items-per-root 200
|
||||||
|
```
|
||||||
|
|
||||||
|
The command covers manifest-backed repository inventory, repository checkout
|
||||||
|
identity, host-path evidence, deployment automation and infrastructure files,
|
||||||
|
State Hub/Gitea metadata roots, endpoint/service-config roots, and safe
|
||||||
|
metadata-only backup or secret roots. Remote HTTP reads are disabled by default;
|
||||||
|
pass `--include-remote` only when the operator intentionally wants configured
|
||||||
|
remote roots such as State Hub inventory endpoints to be fetched.
|
||||||
|
|
||||||
|
The output is an `AccountabilityRootEvidenceRun`. Every evidence item carries
|
||||||
|
provenance, source, fingerprint, `durable: true`, and
|
||||||
|
`live_telemetry: false`, preserving the boundary between Fabric evidence and
|
||||||
|
operational telemetry.
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ fabric/discovery/railiance-accountability-roots.yaml
|
|||||||
|
|
||||||
The manifest schema is documented in `docs/accountability-root-manifest.md`.
|
The manifest schema is documented in `docs/accountability-root-manifest.md`.
|
||||||
|
|
||||||
|
To collect raw evidence from those roots without promoting graph state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
railiance-fabric discover-roots --max-items-per-root 200
|
||||||
|
```
|
||||||
|
|
||||||
The financial export must satisfy these invariants:
|
The financial export must satisfy these invariants:
|
||||||
|
|
||||||
- every accepted node has resolvable ownership;
|
- every accepted node has resolvable ownership;
|
||||||
|
|||||||
385
railiance_fabric/accountability_roots.py
Normal file
385
railiance_fabric/accountability_roots.py
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .discovery import short_fingerprint
|
||||||
|
from .loader import load_yaml, repo_root
|
||||||
|
from .schema_validation import draft202012_validator
|
||||||
|
|
||||||
|
|
||||||
|
EXTRACTOR_VERSION = "0.1.0"
|
||||||
|
DEFAULT_ROOT_MANIFEST_PATH = repo_root() / "fabric" / "discovery" / "railiance-accountability-roots.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def load_accountability_root_manifest(path: Path | None = None, *, validate: bool = True) -> dict[str, Any]:
|
||||||
|
manifest_path = path or DEFAULT_ROOT_MANIFEST_PATH
|
||||||
|
manifest = load_yaml(manifest_path)
|
||||||
|
if not isinstance(manifest, dict):
|
||||||
|
raise ValueError(f"accountability root manifest must be a mapping: {manifest_path}")
|
||||||
|
if validate:
|
||||||
|
validator = draft202012_validator(repo_root() / "schemas" / "accountability-root-manifest.schema.yaml")
|
||||||
|
errors = sorted(validator.iter_errors(manifest), key=lambda error: list(error.path))
|
||||||
|
if errors:
|
||||||
|
location = ".".join(str(part) for part in errors[0].path) or "<root>"
|
||||||
|
raise ValueError(f"invalid accountability root manifest at {location}: {errors[0].message}")
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def collect_accountability_root_evidence(
|
||||||
|
manifest_path: Path | None = None,
|
||||||
|
*,
|
||||||
|
include_remote: bool = False,
|
||||||
|
max_items_per_root: int = 200,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
manifest_path = manifest_path or DEFAULT_ROOT_MANIFEST_PATH
|
||||||
|
manifest = load_accountability_root_manifest(manifest_path)
|
||||||
|
generated_at = _utc_now()
|
||||||
|
roots: list[dict[str, Any]] = []
|
||||||
|
review_artifacts: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for root in manifest.get("discovery_roots", []):
|
||||||
|
if not isinstance(root, dict):
|
||||||
|
continue
|
||||||
|
root_record = {
|
||||||
|
"root_id": root.get("id", ""),
|
||||||
|
"root_type": root.get("type", ""),
|
||||||
|
"status": root.get("status", "planned"),
|
||||||
|
"fabric_id": root.get("fabric_id", ""),
|
||||||
|
"owner_actor_id": root.get("owner_actor_id", ""),
|
||||||
|
"safe_discovery": _source(root).get("safe_discovery", "metadata_only"),
|
||||||
|
"evidence": [],
|
||||||
|
}
|
||||||
|
if root.get("subfabric_id"):
|
||||||
|
root_record["subfabric_id"] = root["subfabric_id"]
|
||||||
|
try:
|
||||||
|
root_record["evidence"] = _collect_root_evidence(
|
||||||
|
root,
|
||||||
|
include_remote=include_remote,
|
||||||
|
max_items=max_items_per_root,
|
||||||
|
)
|
||||||
|
except Exception as exc: # pragma: no cover - defensive boundary for operator runs
|
||||||
|
review_artifacts.append(
|
||||||
|
_review_artifact(
|
||||||
|
root,
|
||||||
|
"adapter_failed",
|
||||||
|
"error",
|
||||||
|
f"{type(exc).__name__}: {exc}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
roots.append(root_record)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"apiVersion": "railiance.fabric/v1alpha2",
|
||||||
|
"kind": "AccountabilityRootEvidenceRun",
|
||||||
|
"generated_at": generated_at,
|
||||||
|
"manifest": {
|
||||||
|
"id": manifest.get("metadata", {}).get("id", ""),
|
||||||
|
"path": _display_path(manifest_path),
|
||||||
|
"fingerprint": _file_sha256(manifest_path) or short_fingerprint(manifest),
|
||||||
|
},
|
||||||
|
"roots": roots,
|
||||||
|
"review_artifacts": review_artifacts,
|
||||||
|
}
|
||||||
|
validator = draft202012_validator(repo_root() / "schemas" / "accountability-root-evidence.schema.yaml")
|
||||||
|
errors = sorted(validator.iter_errors(result), key=lambda error: list(error.path))
|
||||||
|
if errors:
|
||||||
|
location = ".".join(str(part) for part in errors[0].path) or "<root>"
|
||||||
|
raise ValueError(f"invalid accountability root evidence at {location}: {errors[0].message}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_root_evidence(root: dict[str, Any], *, include_remote: bool, max_items: int) -> list[dict[str, Any]]:
|
||||||
|
root_type = str(root.get("type") or "")
|
||||||
|
if root.get("status") == "disabled":
|
||||||
|
return [_declared_evidence(root, "root_disabled", "skipped", "Discovery root is disabled.")]
|
||||||
|
if root_type == "registry_manifest":
|
||||||
|
return _registry_manifest_evidence(root, max_items=max_items)
|
||||||
|
if root_type == "repository_checkout":
|
||||||
|
return _repository_checkout_evidence(root)
|
||||||
|
if root_type == "host_path":
|
||||||
|
return _glob_root_evidence(root, "host_path_match", max_items=max_items)
|
||||||
|
if root_type in {"deployment_automation", "infrastructure_manifest", "service_config", "endpoint_contract"}:
|
||||||
|
return _glob_root_evidence(root, root_type, max_items=max_items)
|
||||||
|
if root_type == "state_hub_repo_inventory":
|
||||||
|
return _state_hub_evidence(root, include_remote=include_remote)
|
||||||
|
if root_type in {"gitea_organization", "gitea_repository"}:
|
||||||
|
return [_declared_evidence(root, root_type, "declared", f"{root_type} root declared.")]
|
||||||
|
if root_type in {"secret_root", "backup_recovery", "manual_review_queue"}:
|
||||||
|
return _metadata_root_evidence(root)
|
||||||
|
return [_declared_evidence(root, root_type or "unknown_root", "declared", "Discovery root declared.")]
|
||||||
|
|
||||||
|
|
||||||
|
def _registry_manifest_evidence(root: dict[str, Any], *, max_items: int) -> list[dict[str, Any]]:
|
||||||
|
source = _source(root)
|
||||||
|
manifest_path = _resolve_path(source.get("manifest_path") or source.get("path"))
|
||||||
|
if not manifest_path.exists():
|
||||||
|
return [_declared_evidence(root, "registry_manifest_missing", "unavailable", f"Manifest missing: {manifest_path}")]
|
||||||
|
manifest = load_yaml(manifest_path)
|
||||||
|
repositories = manifest.get("repositories") if isinstance(manifest, dict) else []
|
||||||
|
if not isinstance(repositories, list):
|
||||||
|
return [_declared_evidence(root, "registry_manifest_invalid", "unavailable", "Manifest has no repositories list.")]
|
||||||
|
|
||||||
|
evidence: list[dict[str, Any]] = [
|
||||||
|
_file_evidence(root, manifest_path, "registry_manifest", summary=f"Registry manifest with {len(repositories)} repositories.")
|
||||||
|
]
|
||||||
|
for index, repo in enumerate(repositories[:max_items]):
|
||||||
|
if not isinstance(repo, dict):
|
||||||
|
continue
|
||||||
|
repo_source = {
|
||||||
|
"manifest_path": _display_path(manifest_path),
|
||||||
|
"json_pointer": f"/repositories/{index}",
|
||||||
|
"repo_slug": repo.get("slug", ""),
|
||||||
|
"path": repo.get("path", ""),
|
||||||
|
"remote_url": repo.get("remote_url", ""),
|
||||||
|
}
|
||||||
|
attributes = {
|
||||||
|
"name": repo.get("name", ""),
|
||||||
|
"domain": repo.get("domain", ""),
|
||||||
|
"default_branch": repo.get("default_branch", ""),
|
||||||
|
"state_hub_repo_id": repo.get("state_hub_repo_id", ""),
|
||||||
|
"has_local_path": bool(repo.get("path")),
|
||||||
|
"has_remote_url": bool(repo.get("remote_url")),
|
||||||
|
}
|
||||||
|
evidence.append(
|
||||||
|
_evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type="registered_repository",
|
||||||
|
state="declared",
|
||||||
|
source=repo_source,
|
||||||
|
summary=f"Registered repository {repo.get('slug', '<unknown>')}.",
|
||||||
|
attributes={key: value for key, value in attributes.items() if value not in ("", None)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(repositories) > max_items:
|
||||||
|
evidence.append(_declared_evidence(root, "registry_manifest_truncated", "skipped", f"Skipped {len(repositories) - max_items} repositories beyond max_items_per_root."))
|
||||||
|
return evidence
|
||||||
|
|
||||||
|
|
||||||
|
def _repository_checkout_evidence(root: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
source = _source(root)
|
||||||
|
checkout = _resolve_path(source.get("path"))
|
||||||
|
if not checkout.exists():
|
||||||
|
return [_declared_evidence(root, "repository_checkout_missing", "unavailable", f"Checkout missing: {checkout}")]
|
||||||
|
attributes = {
|
||||||
|
"repo_slug": source.get("repo_slug", ""),
|
||||||
|
"path_exists": True,
|
||||||
|
"has_git_dir": (checkout / ".git").exists(),
|
||||||
|
"has_fabric_dir": (checkout / "fabric").exists(),
|
||||||
|
"remote_origin": _git_value(checkout, "config", "--get", "remote.origin.url") or source.get("remote_url", ""),
|
||||||
|
"head": _git_value(checkout, "rev-parse", "HEAD") or "",
|
||||||
|
"branch": _git_value(checkout, "rev-parse", "--abbrev-ref", "HEAD") or "",
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
_evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type="repository_checkout",
|
||||||
|
state="observed",
|
||||||
|
source={"path": _display_path(checkout), "repo_slug": source.get("repo_slug", "")},
|
||||||
|
summary=f"Repository checkout observed at {_display_path(checkout)}.",
|
||||||
|
attributes={key: value for key, value in attributes.items() if value not in ("", None)},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _glob_root_evidence(root: dict[str, Any], evidence_type: str, *, max_items: int) -> list[dict[str, Any]]:
|
||||||
|
source = _source(root)
|
||||||
|
base = _resolve_path(source.get("path") or ".")
|
||||||
|
patterns = source.get("patterns") if isinstance(source.get("patterns"), list) else ["*"]
|
||||||
|
if not base.exists():
|
||||||
|
return [_declared_evidence(root, f"{evidence_type}_missing", "unavailable", f"Root path missing: {base}")]
|
||||||
|
matches: list[Path] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
matches.extend(sorted(base.glob(str(pattern))))
|
||||||
|
if len(matches) >= max_items:
|
||||||
|
break
|
||||||
|
evidence = [
|
||||||
|
_evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type=evidence_type,
|
||||||
|
state="observed",
|
||||||
|
source={"path": _display_path(path)},
|
||||||
|
summary=f"Observed {evidence_type} at {_display_path(path)}.",
|
||||||
|
attributes=_file_attributes(path),
|
||||||
|
)
|
||||||
|
for path in matches[:max_items]
|
||||||
|
]
|
||||||
|
if not evidence:
|
||||||
|
evidence.append(_declared_evidence(root, f"{evidence_type}_empty", "unavailable", f"No files matched under {base}."))
|
||||||
|
if len(matches) > max_items:
|
||||||
|
evidence.append(_declared_evidence(root, f"{evidence_type}_truncated", "skipped", f"Skipped {len(matches) - max_items} matches beyond max_items_per_root."))
|
||||||
|
return evidence
|
||||||
|
|
||||||
|
|
||||||
|
def _state_hub_evidence(root: dict[str, Any], *, include_remote: bool) -> list[dict[str, Any]]:
|
||||||
|
source = _source(root)
|
||||||
|
if not include_remote:
|
||||||
|
return [_declared_evidence(root, "state_hub_repo_inventory", "declared", "State Hub repo inventory root declared; remote fetch disabled.")]
|
||||||
|
base_url = str(source.get("base_url") or "").rstrip("/")
|
||||||
|
evidence: list[dict[str, Any]] = []
|
||||||
|
for api_path in source.get("api_paths") or ["/managed-repos/"]:
|
||||||
|
url = f"{base_url}{api_path}"
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=5) as response:
|
||||||
|
payload = json.loads(response.read())
|
||||||
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
|
||||||
|
evidence.append(_declared_evidence(root, "state_hub_fetch_failed", "unavailable", f"{url}: {exc}"))
|
||||||
|
continue
|
||||||
|
count = len(payload) if isinstance(payload, list) else len(payload.get("items", [])) if isinstance(payload, dict) else 0
|
||||||
|
evidence.append(
|
||||||
|
_evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type="state_hub_repo_inventory",
|
||||||
|
state="observed",
|
||||||
|
source={"url": url},
|
||||||
|
summary=f"Fetched State Hub repository inventory from {url}.",
|
||||||
|
attributes={"item_count": count, "payload_fingerprint": short_fingerprint(payload)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return evidence
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_root_evidence(root: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
source = _source(root)
|
||||||
|
path = source.get("path")
|
||||||
|
if path:
|
||||||
|
resolved = _resolve_path(path)
|
||||||
|
if resolved.exists():
|
||||||
|
return [_file_evidence(root, resolved, str(root.get("type") or "metadata_root"))]
|
||||||
|
return [_declared_evidence(root, str(root.get("type") or "metadata_root"), "planned" if root.get("status") == "planned" else "declared", "Metadata-only root declared.")]
|
||||||
|
|
||||||
|
|
||||||
|
def _file_evidence(root: dict[str, Any], path: Path, evidence_type: str, *, summary: str | None = None) -> dict[str, Any]:
|
||||||
|
return _evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type=evidence_type,
|
||||||
|
state="observed",
|
||||||
|
source={"path": _display_path(path)},
|
||||||
|
summary=summary or f"Observed {evidence_type} file at {_display_path(path)}.",
|
||||||
|
attributes=_file_attributes(path),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_evidence(root: dict[str, Any], evidence_type: str, state: str, summary: str) -> dict[str, Any]:
|
||||||
|
source = _source(root)
|
||||||
|
return _evidence_item(
|
||||||
|
root,
|
||||||
|
evidence_type=evidence_type,
|
||||||
|
state=state,
|
||||||
|
source={key: value for key, value in source.items() if key != "safe_discovery"},
|
||||||
|
summary=summary,
|
||||||
|
attributes={"safe_discovery": source.get("safe_discovery", "metadata_only")},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_item(
|
||||||
|
root: dict[str, Any],
|
||||||
|
*,
|
||||||
|
evidence_type: str,
|
||||||
|
state: str,
|
||||||
|
source: dict[str, Any],
|
||||||
|
summary: str,
|
||||||
|
attributes: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"root_id": root.get("id", ""),
|
||||||
|
"evidence_type": evidence_type,
|
||||||
|
"state": state,
|
||||||
|
"source": source,
|
||||||
|
"summary": summary,
|
||||||
|
"attributes": attributes or {},
|
||||||
|
}
|
||||||
|
fingerprint = short_fingerprint(payload, length=16)
|
||||||
|
return {
|
||||||
|
"id": f"evidence:{root.get('id', 'root')}:{fingerprint}",
|
||||||
|
"root_id": root.get("id", ""),
|
||||||
|
"evidence_type": evidence_type,
|
||||||
|
"state": state,
|
||||||
|
"durable": True,
|
||||||
|
"live_telemetry": False,
|
||||||
|
"source": source,
|
||||||
|
"provenance": {
|
||||||
|
"extractor_id": "accountability-root-adapter",
|
||||||
|
"extractor_version": EXTRACTOR_VERSION,
|
||||||
|
"method": "deterministic",
|
||||||
|
"origin": "deterministic",
|
||||||
|
},
|
||||||
|
"fingerprint": fingerprint,
|
||||||
|
"summary": summary,
|
||||||
|
"attributes": attributes or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _review_artifact(root: dict[str, Any], artifact_type: str, severity: str, message: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"root_id": root.get("id", ""),
|
||||||
|
"artifact_type": artifact_type,
|
||||||
|
"severity": severity,
|
||||||
|
"message": message,
|
||||||
|
"source": _source(root),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _source(root: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
source = root.get("source")
|
||||||
|
return source if isinstance(source, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_path(value: object) -> Path:
|
||||||
|
path = Path(str(value or "."))
|
||||||
|
return path if path.is_absolute() else repo_root() / path
|
||||||
|
|
||||||
|
|
||||||
|
def _display_path(path: Path) -> str:
|
||||||
|
try:
|
||||||
|
return path.resolve().relative_to(repo_root()).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return str(path.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
def _file_attributes(path: Path) -> dict[str, Any]:
|
||||||
|
attributes: dict[str, Any] = {
|
||||||
|
"path_type": "directory" if path.is_dir() else "file",
|
||||||
|
"exists": path.exists(),
|
||||||
|
}
|
||||||
|
if path.is_file():
|
||||||
|
attributes["size_bytes"] = path.stat().st_size
|
||||||
|
attributes["sha256"] = _file_sha256(path)
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def _file_sha256(path: Path) -> str | None:
|
||||||
|
if not path.is_file():
|
||||||
|
return None
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _git_value(repo_path: Path, *args: str) -> str | None:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", *args],
|
||||||
|
cwd=repo_path,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
value = result.stdout.strip()
|
||||||
|
return value or None
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
@@ -14,6 +14,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from .accountability_roots import DEFAULT_ROOT_MANIFEST_PATH, collect_accountability_root_evidence
|
||||||
from .connectors import ConnectorConfig
|
from .connectors import ConnectorConfig
|
||||||
from .financial_baseline import financial_export_from_legacy
|
from .financial_baseline import financial_export_from_legacy
|
||||||
from .loader import declaration_files, load_yaml
|
from .loader import declaration_files, load_yaml
|
||||||
@@ -108,6 +109,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
help="Manifest path for the local-fabric-registry connector.",
|
help="Manifest path for the local-fabric-registry connector.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
discover_roots = sub.add_parser(
|
||||||
|
"discover-roots",
|
||||||
|
help="Collect raw evidence from accountability root manifest entries.",
|
||||||
|
)
|
||||||
|
discover_roots.add_argument("--manifest", type=Path, default=DEFAULT_ROOT_MANIFEST_PATH)
|
||||||
|
discover_roots.add_argument("--include-remote", action="store_true", help="Allow HTTP reads from configured remote roots.")
|
||||||
|
discover_roots.add_argument("--max-items-per-root", type=int, default=200)
|
||||||
|
|
||||||
registry = sub.add_parser("registry", help="Feed a running Railiance Fabric registry service.")
|
registry = sub.add_parser("registry", help="Feed a running Railiance Fabric registry service.")
|
||||||
registry_sub = registry.add_subparsers(dest="registry_command", required=True)
|
registry_sub = registry.add_subparsers(dest="registry_command", required=True)
|
||||||
|
|
||||||
@@ -320,6 +329,15 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.command == "scan":
|
if args.command == "scan":
|
||||||
return _scan_repo(args)
|
return _scan_repo(args)
|
||||||
|
|
||||||
|
if args.command == "discover-roots":
|
||||||
|
payload = collect_accountability_root_evidence(
|
||||||
|
args.manifest,
|
||||||
|
include_remote=args.include_remote,
|
||||||
|
max_items_per_root=args.max_items_per_root,
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
if args.command == "registry":
|
if args.command == "registry":
|
||||||
if args.registry_command == "sync":
|
if args.registry_command == "sync":
|
||||||
return _registry_sync(args)
|
return _registry_sync(args)
|
||||||
|
|||||||
187
schemas/accountability-root-evidence.schema.yaml
Normal file
187
schemas/accountability-root-evidence.schema.yaml
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||||
|
$id: "https://railiance.local/fabric/schemas/accountability-root-evidence.schema.yaml"
|
||||||
|
title: "AccountabilityRootEvidenceRun"
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- apiVersion
|
||||||
|
- kind
|
||||||
|
- generated_at
|
||||||
|
- manifest
|
||||||
|
- roots
|
||||||
|
- review_artifacts
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
type: string
|
||||||
|
const: "railiance.fabric/v1alpha2"
|
||||||
|
kind:
|
||||||
|
type: string
|
||||||
|
const: AccountabilityRootEvidenceRun
|
||||||
|
generated_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
manifest:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- path
|
||||||
|
- fingerprint
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
fingerprint:
|
||||||
|
type: string
|
||||||
|
minLength: 8
|
||||||
|
roots:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/$defs/rootEvidence"
|
||||||
|
review_artifacts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/$defs/reviewArtifact"
|
||||||
|
|
||||||
|
$defs:
|
||||||
|
rootEvidence:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- root_id
|
||||||
|
- root_type
|
||||||
|
- status
|
||||||
|
- fabric_id
|
||||||
|
- owner_actor_id
|
||||||
|
- safe_discovery
|
||||||
|
- evidence
|
||||||
|
properties:
|
||||||
|
root_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
root_type:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- active
|
||||||
|
- planned
|
||||||
|
- disabled
|
||||||
|
fabric_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
subfabric_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
owner_actor_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
safe_discovery:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
evidence:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/$defs/evidenceItem"
|
||||||
|
|
||||||
|
evidenceItem:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- root_id
|
||||||
|
- evidence_type
|
||||||
|
- state
|
||||||
|
- durable
|
||||||
|
- live_telemetry
|
||||||
|
- source
|
||||||
|
- provenance
|
||||||
|
- fingerprint
|
||||||
|
- summary
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
root_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
evidence_type:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- observed
|
||||||
|
- declared
|
||||||
|
- planned
|
||||||
|
- skipped
|
||||||
|
- unavailable
|
||||||
|
durable:
|
||||||
|
type: boolean
|
||||||
|
live_telemetry:
|
||||||
|
type: boolean
|
||||||
|
source:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
provenance:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- extractor_id
|
||||||
|
- extractor_version
|
||||||
|
- method
|
||||||
|
- origin
|
||||||
|
properties:
|
||||||
|
extractor_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
extractor_version:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
origin:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
fingerprint:
|
||||||
|
type: string
|
||||||
|
minLength: 8
|
||||||
|
summary:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
reviewArtifact:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- root_id
|
||||||
|
- artifact_type
|
||||||
|
- severity
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
root_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
artifact_type:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- info
|
||||||
|
- warning
|
||||||
|
- error
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
source:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
161
tests/test_accountability_root_adapters.py
Normal file
161
tests/test_accountability_root_adapters.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from railiance_fabric.accountability_roots import collect_accountability_root_evidence
|
||||||
|
from railiance_fabric.cli import main as cli_main
|
||||||
|
from railiance_fabric.schema_validation import draft202012_validator
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_accountability_root_evidence_from_manifest(tmp_path: Path) -> None:
|
||||||
|
manifest = _fixture_manifest(tmp_path)
|
||||||
|
evidence_run = collect_accountability_root_evidence(manifest, max_items_per_root=20)
|
||||||
|
|
||||||
|
validator = draft202012_validator(Path("schemas/accountability-root-evidence.schema.yaml"))
|
||||||
|
assert list(validator.iter_errors(evidence_run)) == []
|
||||||
|
assert evidence_run["kind"] == "AccountabilityRootEvidenceRun"
|
||||||
|
assert evidence_run["manifest"]["id"] == "fixture.accountability-roots"
|
||||||
|
|
||||||
|
evidence = [
|
||||||
|
item
|
||||||
|
for root in evidence_run["roots"]
|
||||||
|
for item in root["evidence"]
|
||||||
|
]
|
||||||
|
evidence_types = {item["evidence_type"] for item in evidence}
|
||||||
|
assert {"registered_repository", "repository_checkout", "deployment_automation", "secret_root"} <= evidence_types
|
||||||
|
assert all(item["durable"] is True for item in evidence)
|
||||||
|
assert all(item["live_telemetry"] is False for item in evidence)
|
||||||
|
|
||||||
|
registered_repo = next(item for item in evidence if item["evidence_type"] == "registered_repository")
|
||||||
|
assert registered_repo["attributes"]["state_hub_repo_id"] == "fixture-state-hub-id"
|
||||||
|
|
||||||
|
secret_root = next(item for item in evidence if item["evidence_type"] == "secret_root")
|
||||||
|
assert "secret-value" not in json.dumps(secret_root)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discover_roots_cli_prints_evidence_json(tmp_path: Path, capsys) -> None:
|
||||||
|
manifest = _fixture_manifest(tmp_path)
|
||||||
|
|
||||||
|
assert cli_main(["discover-roots", "--manifest", str(manifest), "--max-items-per-root", "20"]) == 0
|
||||||
|
|
||||||
|
payload = json.loads(capsys.readouterr().out)
|
||||||
|
assert payload["kind"] == "AccountabilityRootEvidenceRun"
|
||||||
|
assert payload["roots"]
|
||||||
|
|
||||||
|
|
||||||
|
def _fixture_manifest(tmp_path: Path) -> Path:
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
repo = workspace / "fixture-repo"
|
||||||
|
repo.mkdir(parents=True)
|
||||||
|
(repo / ".git").mkdir()
|
||||||
|
(repo / "fabric").mkdir()
|
||||||
|
(repo / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8")
|
||||||
|
(repo / "compose.yaml").write_text("services:\n api:\n image: fixture/api\n", encoding="utf-8")
|
||||||
|
secret_metadata = repo / "secret-root.txt"
|
||||||
|
secret_metadata.write_text("secret-value-never-promote\n", encoding="utf-8")
|
||||||
|
|
||||||
|
registry_manifest = tmp_path / "local-repos.yaml"
|
||||||
|
registry_manifest.write_text(
|
||||||
|
"""
|
||||||
|
apiVersion: railiance.fabric/v1alpha1
|
||||||
|
kind: RegistryOnboardingManifest
|
||||||
|
repositories:
|
||||||
|
- slug: fixture-repo
|
||||||
|
name: Fixture Repo
|
||||||
|
domain: testing
|
||||||
|
path: {repo}
|
||||||
|
default_branch: main
|
||||||
|
state_hub_repo_id: fixture-state-hub-id
|
||||||
|
remote_url: gitea-remote:coulomb/fixture-repo.git
|
||||||
|
""".format(repo=repo),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest = tmp_path / "accountability-roots.yaml"
|
||||||
|
manifest.write_text(
|
||||||
|
"""
|
||||||
|
apiVersion: railiance.fabric/v1alpha2
|
||||||
|
kind: AccountabilityRootManifest
|
||||||
|
metadata:
|
||||||
|
id: fixture.accountability-roots
|
||||||
|
name: Fixture Accountability Roots
|
||||||
|
netkingdom:
|
||||||
|
id: fixture.netkingdom
|
||||||
|
name: Fixture Netkingdom
|
||||||
|
king_actor_id: actor.fixture.king
|
||||||
|
actors:
|
||||||
|
- id: actor.fixture.king
|
||||||
|
role: king
|
||||||
|
name: Fixture King
|
||||||
|
- id: actor.fixture.lord
|
||||||
|
role: lord
|
||||||
|
name: Fixture Lord
|
||||||
|
fabrics:
|
||||||
|
- id: fabric.fixture.primary
|
||||||
|
kind: Fabric
|
||||||
|
name: Fixture Primary Fabric
|
||||||
|
netkingdom_id: fixture.netkingdom
|
||||||
|
lord_actor_id: actor.fixture.lord
|
||||||
|
parent_fabric_id: null
|
||||||
|
status: active
|
||||||
|
boundary:
|
||||||
|
boundary_type: fabric
|
||||||
|
criterion: financial_and_operational_accountability
|
||||||
|
discovery_roots:
|
||||||
|
- id: root.fixture.registry
|
||||||
|
type: registry_manifest
|
||||||
|
status: active
|
||||||
|
fabric_id: fabric.fixture.primary
|
||||||
|
owner_actor_id: actor.fixture.king
|
||||||
|
source:
|
||||||
|
manifest_path: {registry_manifest}
|
||||||
|
safe_discovery: local_files
|
||||||
|
evidence_scope:
|
||||||
|
- repo_inventory
|
||||||
|
- repository_identity
|
||||||
|
- id: root.fixture.checkout
|
||||||
|
type: repository_checkout
|
||||||
|
status: active
|
||||||
|
fabric_id: fabric.fixture.primary
|
||||||
|
owner_actor_id: actor.fixture.lord
|
||||||
|
source:
|
||||||
|
repo_slug: fixture-repo
|
||||||
|
path: {repo}
|
||||||
|
safe_discovery: local_files
|
||||||
|
evidence_scope:
|
||||||
|
- repository_identity
|
||||||
|
- local_checkout
|
||||||
|
- id: root.fixture.deployment
|
||||||
|
type: deployment_automation
|
||||||
|
status: active
|
||||||
|
fabric_id: fabric.fixture.primary
|
||||||
|
owner_actor_id: actor.fixture.lord
|
||||||
|
source:
|
||||||
|
path: {repo}
|
||||||
|
patterns:
|
||||||
|
- Dockerfile
|
||||||
|
- compose.yaml
|
||||||
|
safe_discovery: local_files
|
||||||
|
evidence_scope:
|
||||||
|
- deployment_topology
|
||||||
|
- id: root.fixture.secret
|
||||||
|
type: secret_root
|
||||||
|
status: planned
|
||||||
|
fabric_id: fabric.fixture.primary
|
||||||
|
owner_actor_id: actor.fixture.king
|
||||||
|
source:
|
||||||
|
path: {secret_metadata}
|
||||||
|
safe_discovery: metadata_only
|
||||||
|
evidence_scope:
|
||||||
|
- secret_metadata
|
||||||
|
refresh:
|
||||||
|
cadence: manual
|
||||||
|
triggers:
|
||||||
|
- operator_request
|
||||||
|
""".format(
|
||||||
|
registry_manifest=registry_manifest,
|
||||||
|
repo=repo,
|
||||||
|
secret_metadata=secret_metadata,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return manifest
|
||||||
@@ -86,7 +86,7 @@ Result:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0018-T02
|
id: RAIL-FAB-WP-0018-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "09246f06-10db-4c6c-9cb3-f2808fdbaa38"
|
state_hub_task_id: "09246f06-10db-4c6c-9cb3-f2808fdbaa38"
|
||||||
```
|
```
|
||||||
@@ -110,6 +110,27 @@ Done when:
|
|||||||
state;
|
state;
|
||||||
- adapters can run against the current local Railiance workspace.
|
- adapters can run against the current local Railiance workspace.
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
- Added `railiance_fabric/accountability_roots.py` to load and validate the
|
||||||
|
accountability root manifest and collect raw evidence from registered roots.
|
||||||
|
- Added `schemas/accountability-root-evidence.schema.yaml` for
|
||||||
|
`AccountabilityRootEvidenceRun` payloads.
|
||||||
|
- Added `railiance-fabric discover-roots` with `--manifest`,
|
||||||
|
`--max-items-per-root`, and opt-in `--include-remote`.
|
||||||
|
- Implemented initial adapters for registry manifests, repository checkouts,
|
||||||
|
host paths, deployment/infrastructure/service/endpoint file roots, State Hub
|
||||||
|
metadata roots, Gitea metadata roots, and metadata-only backup/secret roots.
|
||||||
|
- Raw evidence carries source, provenance, fingerprints, durable evidence
|
||||||
|
state, and `live_telemetry: false` without promoting candidates into accepted
|
||||||
|
graph state.
|
||||||
|
- Added adapter coverage in `tests/test_accountability_root_adapters.py` and
|
||||||
|
documented the evidence command in the manifest/operator docs.
|
||||||
|
- Verified with
|
||||||
|
`python3 -m pytest tests/test_accountability_roots.py tests/test_accountability_root_adapters.py -q`,
|
||||||
|
`python3 -m railiance_fabric.cli discover-roots --max-items-per-root 5`,
|
||||||
|
and full `python3 -m pytest`.
|
||||||
|
|
||||||
## T03 - Build Evidence Store And Identity Normalization
|
## T03 - Build Evidence Store And Identity Normalization
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|||||||
Reference in New Issue
Block a user