feat: finish register receiving and authority routing

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-21 23:15:48 +02:00
parent 6d134425df
commit d103955217
28 changed files with 970 additions and 48 deletions

View file

@ -0,0 +1,74 @@
"""Resolve exactly one authoritative owner for a State Hub record type."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
AUTHORITY_SCHEMA = "repo-manager.hub-record-authority.v1"
VALID_CLASSES = frozenset(
{"file-derived", "hub-native-append", "hub-native-control", "derived-cache", "retired"}
)
DEFAULT_CONTRACT = Path(__file__).resolve().parents[2] / "config" / "hub-record-authority.yaml"
class AuthorityError(ValueError):
"""The requested route is missing or conflicts with the authority contract."""
def load_authority_contract(path: Path = DEFAULT_CONTRACT) -> dict[str, Any]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict) or data.get("schema") != AUTHORITY_SCHEMA:
raise AuthorityError(f"unsupported authority contract: {path}")
records = data.get("records")
if not isinstance(records, dict) or not records:
raise AuthorityError("authority contract has no records")
for record_type, rule in records.items():
if not isinstance(rule, dict):
raise AuthorityError(f"invalid rule for {record_type}")
if rule.get("class") not in VALID_CLASSES or not rule.get("owner"):
raise AuthorityError(f"incomplete authority rule for {record_type}")
return data
def resolve_record_authority(
record_type: str,
*,
repo_slug: str | None = None,
domain_slug: str | None = None,
claimed_owner: str | None = None,
contract_path: Path = DEFAULT_CONTRACT,
) -> dict[str, str]:
"""Return the one authority route or reject an ambiguous/conflicting write."""
contract = load_authority_contract(contract_path)
rule = contract["records"].get(record_type)
if not isinstance(rule, dict):
raise AuthorityError(f"unknown record type: {record_type}")
record_class = str(rule["class"])
owner = str(rule["owner"])
if claimed_owner is not None and claimed_owner != owner:
raise AuthorityError(
f"authority mismatch for {record_type}: contract={owner}, claimed={claimed_owner}"
)
if record_class == "file-derived":
if not repo_slug or not domain_slug:
raise AuthorityError(
f"file-derived {record_type} requires repo_slug and domain_slug"
)
authority_key = f"repository:{domain_slug}/{repo_slug}"
elif record_class == "retired":
authority_key = f"archive:{record_type}"
else:
authority_key = f"hub:{owner}"
return {
"schema": AUTHORITY_SCHEMA,
"record_type": record_type,
"record_class": record_class,
"owner": owner,
"authority_key": authority_key,
}

View file

@ -295,6 +295,25 @@ def main(argv: list[str] | None = None) -> int:
p_id_plan.add_argument("--output", default=None, help="Write the provenance mapping as JSON")
p_id_plan.add_argument("--force", action="store_true", help="Replace an existing --output file")
p_sbom = sub.add_parser("sbom", help="Derive SBOM snapshots and licence reports from repository files")
sbom_sub = p_sbom.add_subparsers(dest="sbom_command")
p_sbom_scan = sbom_sub.add_parser("scan", help="Scan recognised lockfiles and tool manifests")
p_sbom_scan.add_argument("--path", default=".")
p_sbom_scan.add_argument("--slug", default=None)
p_sbom_scan.add_argument("--output", default=None, help="Write the derived snapshot as JSON")
p_sbom_scan.add_argument("--force", action="store_true", help="Replace an existing --output file")
p_sbom_report = sbom_sub.add_parser("licence-report", help="Report licences from a fresh file scan")
p_sbom_report.add_argument("--path", default=".")
p_sbom_report.add_argument("--slug", default=None)
p_authority = sub.add_parser("authority", help="Resolve the one authoritative record owner")
authority_sub = p_authority.add_subparsers(dest="authority_command")
p_authority_route = authority_sub.add_parser("route", help="Resolve or verify an authority route")
p_authority_route.add_argument("--record-type", required=True)
p_authority_route.add_argument("--repo-slug", default=None)
p_authority_route.add_argument("--domain-slug", default=None)
p_authority_route.add_argument("--claimed-owner", default=None)
args = parser.parse_args(argv)
if args.version or args.command in (None, "version"):
@ -618,6 +637,56 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1
if args.command == "sbom":
if not args.sbom_command:
p_sbom.print_help()
return 2
from repo_manager.sbom import scan_repository
snapshot = scan_repository(Path(args.path), slug=args.slug)
result = snapshot if args.sbom_command == "scan" else {
"ok": snapshot["ok"],
"repo_slug": snapshot["repo_slug"],
"source_revision": snapshot["source_revision"],
"generated_at": snapshot["generated_at"],
"entry_count": snapshot["entry_count"],
"licence_report": snapshot["licence_report"],
"errors": snapshot["errors"],
}
if args.sbom_command == "scan" and args.output:
output = Path(args.output)
if output.exists() and not args.force:
print(
json.dumps(
{"ok": False, "error": f"output exists: {output}; use --force to replace"},
indent=2,
)
)
return 1
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(snapshot, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1
if args.command == "authority":
if not args.authority_command:
p_authority.print_help()
return 2
from repo_manager.authority import AuthorityError, resolve_record_authority
try:
result = resolve_record_authority(
args.record_type,
repo_slug=args.repo_slug,
domain_slug=args.domain_slug,
claimed_owner=args.claimed_owner,
)
except AuthorityError as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
print(json.dumps({"ok": True, **result}, indent=2))
return 0
parser.print_help()
return 0

View file

@ -448,10 +448,20 @@ def place(path: Path, *, reef: str, family_root: Path | None = None) -> dict[str
if reef not in COMPUTE_REEFS and not (search / reef).is_dir():
return _refuse(f"reef {reef!r} is not a known compute reef")
text = declaration.read_text()
if re.search(r"^bound_reefs:\n - ", text, re.M):
text = re.sub(r"^bound_reefs:\n(?: - .+\n)+", f"bound_reefs:\n - {reef}\n", text, flags=re.M)
if re.search(r"^bound_reefs:\n - ", text, re.MULTILINE):
text = re.sub(
r"^bound_reefs:\n(?: - .+\n)+",
f"bound_reefs:\n - {reef}\n",
text,
flags=re.MULTILINE,
)
else:
text = re.sub(r"^bound_reefs:\s*\[\]\s*$", f"bound_reefs:\n - {reef}", text, flags=re.M)
text = re.sub(
r"^bound_reefs:\s*\[\]\s*$",
f"bound_reefs:\n - {reef}",
text,
flags=re.MULTILINE,
)
if "exposure:" in text and "posture: public" in text:
return _refuse("place does not grant public exposure; edit exposure separately")
declaration.write_text(text)

View file

@ -4,7 +4,6 @@ from __future__ import annotations
import re
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@ -16,6 +15,7 @@ from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.parse.record import iter_record_files, parse_record_file
from repo_manager.time import utc_now_text
_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_PROTECTED = frozenset(
@ -39,7 +39,7 @@ _OPERATIONS = {
def _now() -> str:
return datetime.now(UTC).isoformat()
return utc_now_text()
def _reject(command: str, correlation_id: str, code: str, message: str, **evidence: Any) -> CommandResult:

View file

@ -4,7 +4,6 @@ from __future__ import annotations
import re
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@ -16,6 +15,7 @@ from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.parse.register import REGISTER_SCHEMA, SUPPORTED_REGISTER_KINDS, register_path
from repo_manager.time import utc_now_text
_ENTRY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_PROTECTED_FIELDS = frozenset({"id", "title", "status", "notes", "created", "updated"})
@ -30,7 +30,7 @@ _REQUIRED_DATA = {
def _now() -> str:
return datetime.now(UTC).isoformat()
return utc_now_text()
def _reject(command: str, correlation_id: str, code: str, message: str, **evidence: Any) -> CommandResult:

View file

@ -4,12 +4,12 @@ from __future__ import annotations
import uuid
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Any
from repo_manager.gitops import GitError, commit_paths, is_git_repo
from repo_manager.standards import FLAVOR_MARKER_PREFIX, expected_workplan_prefix
from repo_manager.time import utc_today
DURABLE_FLAVORS = ("experimental", "research", "tooling", "product", "business")
FLAVORS = (*DURABLE_FLAVORS, "project")
@ -124,7 +124,7 @@ def scaffold_repository(
f"Workplan prefix: `{prefix}-`.\n",
)
if prj:
today = date.today().isoformat()
today = utc_today().isoformat()
put(
"GOAL.md",
"---\n"

View file

@ -6,7 +6,7 @@ import json
import re
import uuid
from dataclasses import dataclass
from datetime import UTC, date, datetime
from datetime import date
from pathlib import Path
from typing import Any
@ -14,6 +14,7 @@ from repo_manager import dual_run, idempotency
from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.time import utc_today
VALID_WORKPLAN_STATUSES = frozenset(
{"proposed", "ready", "active", "blocked", "backlog", "finished", "archived"}
@ -80,7 +81,7 @@ def _quoted(value: str) -> str:
def _today() -> date:
return datetime.now(UTC).date()
return utc_today()
def _patch_frontmatter(text: str, updates: dict[str, str]) -> str | None:

View file

@ -16,13 +16,14 @@ from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal
import yaml
from repo_manager.time import utc_now_text
Source = Literal["state-hub", "repo-manager"]
_TRUE = frozenset({"1", "true", "yes", "on"})
@ -157,7 +158,7 @@ def record_mutation(
path = meter_path()
path.parent.mkdir(parents=True, exist_ok=True)
row = {
"ts": datetime.now(timezone.utc).isoformat(),
"ts": utc_now_text(),
"source": source,
"kind": kind,
"repo_slug": repo_slug,

View file

@ -4,13 +4,14 @@ from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from repo_manager.time import utc_now_text
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
return utc_now_text()
@dataclass

326
src/repo_manager/sbom.py Normal file
View file

@ -0,0 +1,326 @@
"""Repository-derived SBOM scanning and licence reporting."""
from __future__ import annotations
import hashlib
import json
import os
import re
import tomllib
from collections import Counter
from collections.abc import Callable
from pathlib import Path
from typing import Any
import yaml
from repo_manager.gitops import head_sha
from repo_manager.time import utc_now_text
SBOM_SCHEMA = "repo-manager.sbom-snapshot.v1"
COPYLEFT_MARKERS = frozenset({"GPL", "AGPL", "LGPL", "EUPL", "CDDL", "MPL"})
VALID_ECOSYSTEMS = frozenset(
{"python", "node", "rust", "go", "java", "terraform", "ansible", "tool", "other"}
)
SKIP_DIRECTORIES = frozenset(
{
".git",
".hg",
".svn",
".venv",
"venv",
".env",
"node_modules",
"__pycache__",
".mypy_cache",
".pytest_cache",
".ruff_cache",
"dist",
"build",
".build",
"target",
".tox",
".nox",
}
)
Entry = dict[str, Any]
Parser = Callable[[Path], list[Entry]]
def _entry(
name: str,
version: str | None,
ecosystem: str,
*,
license_spdx: str | None = None,
is_direct: bool = False,
is_dev: bool = False,
) -> Entry:
return {
"package_name": name,
"package_version": version,
"ecosystem": ecosystem,
"license_spdx": license_spdx,
"is_direct": is_direct,
"is_dev": is_dev,
}
def _parse_toml_packages(path: Path, ecosystem: str) -> list[Entry]:
data = tomllib.loads(path.read_text(encoding="utf-8"))
return [
_entry(str(item["name"]), str(item["version"]) if item.get("version") else None, ecosystem)
for item in data.get("package", [])
if isinstance(item, dict) and item.get("name")
]
def parse_uv_lock(path: Path) -> list[Entry]:
return _parse_toml_packages(path, "python")
def parse_cargo_lock(path: Path) -> list[Entry]:
return _parse_toml_packages(path, "rust")
def parse_requirements(path: Path) -> list[Entry]:
entries: list[Entry] = []
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith(("#", "-")):
continue
match = re.match(r"^([A-Za-z0-9_.-]+)(?:[>=<!~^]+([^\s;]+))?", line)
if match:
entries.append(_entry(match.group(1), match.group(2), "python", is_direct=True))
return entries
def parse_package_lock(path: Path) -> list[Entry]:
data = json.loads(path.read_text(encoding="utf-8"))
entries: list[Entry] = []
for package_path, item in (data.get("packages") or {}).items():
if not package_path or not isinstance(item, dict):
continue
name = item.get("name") or package_path.rsplit("node_modules/", 1)[-1]
entries.append(
_entry(
str(name),
str(item["version"]) if item.get("version") else None,
"node",
license_spdx=str(item["license"]) if item.get("license") else None,
is_direct=not bool(item.get("indirect", False)),
is_dev=bool(item.get("dev", False)),
)
)
return entries
def parse_yarn_lock(path: Path) -> list[Entry]:
entries: list[Entry] = []
names: list[str] = []
for raw in [*path.read_text(encoding="utf-8").splitlines(), ""]:
stripped = raw.strip()
if raw and not raw.startswith((" ", "\t")) and stripped.endswith(":"):
names = []
for specifier in stripped.rstrip(":").split(","):
match = re.match(r'"?((?:@[^/" ]+/)?[^@" ]+)@', specifier.strip())
if match:
names.append(match.group(1))
elif stripped.startswith("version ") and names:
version_match = re.search(r'"([^"]+)"', stripped)
version = version_match.group(1) if version_match else None
entries.extend(_entry(name, version, "node") for name in names)
names = []
return entries
def _go_direct_modules(directory: Path) -> set[str]:
path = directory / "go.mod"
if not path.is_file():
return set()
direct: set[str] = set()
in_block = False
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if line == "require (":
in_block = True
continue
if in_block and line == ")":
in_block = False
continue
candidate = line if in_block else line.removeprefix("require ") if line.startswith("require ") else ""
if candidate and "// indirect" not in candidate:
direct.add(candidate.split()[0])
return direct
def parse_go_sum(path: Path) -> list[Entry]:
direct = _go_direct_modules(path.parent)
seen: set[tuple[str, str]] = set()
entries: list[Entry] = []
for raw in path.read_text(encoding="utf-8").splitlines():
parts = raw.split()
if len(parts) < 2 or parts[1].endswith("/go.mod"):
continue
key = (parts[0], parts[1])
if key in seen:
continue
seen.add(key)
entries.append(_entry(key[0], key[1], "go", is_direct=parts[0] in direct))
return entries
def parse_terraform_lock(path: Path) -> list[Entry]:
entries: list[Entry] = []
provider: str | None = None
version: str | None = None
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
match = re.match(r'^provider\s+"([^"]+)"\s*\{', line)
if match:
provider, version = match.group(1), None
elif provider:
version_match = re.match(r'version\s*=\s*"([^"]+)"', line)
if version_match:
version = version_match.group(1)
elif line == "}":
entries.append(_entry(provider, version, "terraform", is_direct=True))
provider, version = None, None
return entries
def parse_ansible_requirements(path: Path) -> list[Entry]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return []
entries: list[Entry] = []
for kind in ("collections", "roles"):
for item in data.get(kind, []) or []:
if isinstance(item, str):
name, version = item, None
elif isinstance(item, dict):
name = item.get("name") or item.get("src")
version = str(item["version"]) if item.get("version") else None
else:
continue
if name:
entries.append(_entry(str(name), version, "ansible", is_direct=True))
return entries
def parse_tools_manifest(path: Path) -> list[Entry]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return []
entries: list[Entry] = []
for item in data.get("tools", []) or []:
if not isinstance(item, dict) or not item.get("name"):
continue
ecosystem = str(item.get("ecosystem") or "tool")
if ecosystem not in VALID_ECOSYSTEMS:
ecosystem = "tool"
version = str(item["version"]) if item.get("version") not in {None, "unknown"} else None
entries.append(
_entry(
str(item["name"]),
version,
ecosystem,
license_spdx=str(item["license_spdx"]) if item.get("license_spdx") else None,
is_direct=bool(item.get("is_direct", True)),
is_dev=bool(item.get("is_dev", False)),
)
)
return entries
LOCKFILE_PARSERS: dict[str, Parser] = {
"uv.lock": parse_uv_lock,
"requirements.txt": parse_requirements,
"package-lock.json": parse_package_lock,
"yarn.lock": parse_yarn_lock,
"Cargo.lock": parse_cargo_lock,
".terraform.lock.hcl": parse_terraform_lock,
"go.sum": parse_go_sum,
}
def detect_sources(repo_root: Path) -> list[tuple[Path, Parser]]:
found: list[tuple[Path, Parser]] = []
seen: set[Path] = set()
for directory, directories, filenames in os.walk(repo_root):
directories[:] = sorted(name for name in directories if name not in SKIP_DIRECTORIES)
current = Path(directory)
for filename, parser in LOCKFILE_PARSERS.items():
if filename in filenames:
path = current / filename
found.append((path, parser))
seen.add(path)
if current.name == "ansible":
for filename in ("requirements.yml", "requirements.yaml"):
if filename in filenames:
path = current / filename
found.append((path, parse_ansible_requirements))
seen.add(path)
tools = repo_root / "sbom-tools.yaml"
if tools.is_file() and tools not in seen:
found.append((tools, parse_tools_manifest))
return sorted(found, key=lambda item: str(item[0]))
def is_copyleft(spdx: str | None) -> bool:
upper = (spdx or "").upper()
return any(marker in upper for marker in COPYLEFT_MARKERS)
def licence_report(entries: list[Entry]) -> dict[str, Any]:
counts = Counter(entry.get("license_spdx") for entry in entries)
groups = [
{"license_spdx": license_id, "count": count, "is_copyleft": is_copyleft(license_id)}
for license_id, count in sorted(counts.items(), key=lambda item: (-item[1], item[0] or ""))
]
risks = [
{
"package_name": entry["package_name"],
"package_version": entry.get("package_version"),
"license_spdx": entry.get("license_spdx"),
"source_path": entry["source_path"],
}
for entry in entries
if is_copyleft(entry.get("license_spdx"))
and entry.get("is_direct")
and not entry.get("is_dev")
]
return {"groups": groups, "copyleft_direct_prod": risks, "copyleft_direct_count": len(risks)}
def scan_repository(repo_root: Path, *, slug: str | None = None) -> dict[str, Any]:
repo_root = repo_root.resolve()
sources: list[dict[str, Any]] = []
entries: list[Entry] = []
errors: list[dict[str, str]] = []
for path, parser in detect_sources(repo_root):
relative = str(path.relative_to(repo_root))
try:
parsed = parser(path)
except (OSError, ValueError, TypeError, json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as exc:
errors.append({"source_path": relative, "error": str(exc)})
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
sources.append({"path": relative, "sha256": digest, "entry_count": len(parsed)})
for item in parsed:
entries.append({**item, "source_path": relative})
return {
"schema": SBOM_SCHEMA,
"ok": not errors,
"repo_slug": slug or repo_root.name,
"repo_path": str(repo_root),
"source_revision": head_sha(repo_root),
"generated_at": utc_now_text(),
"authority": "detected lockfiles and reviewed sbom-tools.yaml",
"sources": sources,
"entry_count": len(entries),
"entries": entries,
"licence_report": licence_report(entries),
"errors": errors,
}

View file

@ -16,8 +16,6 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal
import yaml
from repo_manager.observe import load_classification
from repo_manager.parse.workplan import parse_frontmatter

26
src/repo_manager/time.py Normal file
View file

@ -0,0 +1,26 @@
"""Canonical UTC time helpers (RMGR-ADR-002)."""
from __future__ import annotations
from datetime import UTC, date, datetime
def utc_now() -> datetime:
"""Return the current timezone-aware UTC instant."""
return datetime.now(UTC)
def format_utc(value: datetime) -> str:
"""Serialize an aware instant as canonical RFC 3339 UTC with ``Z``."""
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("timestamp must be timezone-aware")
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
def utc_now_text() -> str:
return format_utc(utc_now())
def utc_today() -> date:
"""Return the calendar date derived from the current UTC instant."""
return utc_now().date()