* docs: consolidate governance anchors and clean up test registries * refactor(cli): decompose cli into dedicated modules * test: fix broken test baselines and formatting * docs: add domain boundary READMEs for governance anchors * test: update baseline for determination lane * test: fix capability_pass expectation * test: fix CORE_SHOWCASE_SKIP_BUDGET enforcement * chore: cleanup CLI extraction and unreachable code
409 lines
14 KiB
Python
409 lines
14 KiB
Python
"""Ethics-pack loader.
|
|
|
|
ADR-0033. Ethics packs sit alongside identity (ADR-0027) and safety
|
|
(ADR-0029) but occupy a distinct architectural niche:
|
|
|
|
* **Identity packs** carry *who* CORE is — value axes, directions, weights.
|
|
* **Safety packs** carry *what CORE will never do* — universal red lines.
|
|
* **Ethics packs** carry *what this deployment commits to in its domain* —
|
|
propositional pledges scoped to a deployment context (general, medical,
|
|
legal, financial, robotics, ...).
|
|
|
|
Like identity packs, ethics packs are **swappable** per deployment. Unlike
|
|
safety packs, missing ratification is not fail-closed: an absent or
|
|
unratified ethics pack falls back to ``DEFAULT_ETHICS_PACK`` with a typed
|
|
``EthicsPackError`` raised only when the requested pack is missing
|
|
*and* the default cannot be loaded.
|
|
|
|
At composition time, ``commitment_ids`` from the ethics pack are unioned
|
|
into the runtime manifold's ``boundary_ids`` alongside identity and
|
|
safety contributions. The composition is monotone: ethics can add
|
|
constraints, never remove them.
|
|
|
|
See ``docs/adr/ADR-0033-ethics-packs.md``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import FrozenSet, Iterable
|
|
|
|
from formation.hashing import verify_seal
|
|
|
|
|
|
class EthicsPackError(ValueError):
|
|
"""Raised when an ethics pack is missing, malformed, or unverified.
|
|
|
|
Inherits from ``ValueError`` (like ``IdentityPackError``, not
|
|
``SafetyPackError``) — ethics packs are swappable, and a missing
|
|
one is recoverable by falling back to the default. Only when *both*
|
|
the requested pack and the default are unloadable does this
|
|
surface.
|
|
"""
|
|
|
|
|
|
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"
|
|
_DEFAULT_SEARCH_PATHS: tuple[Path, ...] = (
|
|
Path(__file__).resolve().parent,
|
|
)
|
|
_ALLOWED_DOMAINS: frozenset[str] = frozenset(
|
|
{"general", "medical", "legal", "financial", "robotics", "custom"}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EthicsPack:
|
|
"""Loaded ethics pack.
|
|
|
|
``commitment_ids`` is the set of propositional pledges contributed
|
|
to the runtime manifold's ``boundary_ids``. ``domain`` declares the
|
|
deployment context.
|
|
|
|
ADR-0037 — ``refusal_commitments`` is the opt-in subset of
|
|
commitments that, when violated runtime-checkably, trigger typed
|
|
refusal at the runtime level (ADR-0036 surface). The default pack
|
|
leaves this empty: ethics remains audit-only unless a deployment
|
|
explicitly opts a commitment in. Must be a subset of
|
|
``commitment_ids``.
|
|
"""
|
|
|
|
pack_id: str
|
|
version: str
|
|
description: str
|
|
domain: str
|
|
commitment_ids: FrozenSet[str]
|
|
commitment_descriptions: dict[str, str]
|
|
mastery_report_sha256: str
|
|
ratified: bool
|
|
refusal_commitments: FrozenSet[str] = frozenset()
|
|
hedge_commitments: FrozenSet[str] = frozenset()
|
|
|
|
|
|
def load_ethics_pack(
|
|
pack_id: str = DEFAULT_ETHICS_PACK,
|
|
*,
|
|
search_paths: Iterable[Path | str] | None = None,
|
|
require_ratified: bool | None = None,
|
|
) -> EthicsPack:
|
|
"""Load an ethics pack.
|
|
|
|
Args:
|
|
pack_id: Pack identifier. Defaults to ``DEFAULT_ETHICS_PACK``.
|
|
search_paths: Directories to search. Defaults to
|
|
``packs/ethics/``.
|
|
require_ratified: When ``True``, require the companion
|
|
``<pack_id>.mastery_report.json`` and verify its self-seal.
|
|
``None`` (default) → production mode unless
|
|
``CORE_ALLOW_UNRATIFIED_ETHICS=1``.
|
|
|
|
Raises:
|
|
EthicsPackError: On malformed pack, bounds violation, or — in
|
|
production mode — unverified seal. Missing-file errors
|
|
surface here so callers can decide whether to fall back to
|
|
the default.
|
|
"""
|
|
paths = _resolve_search_paths(search_paths)
|
|
pack_path = _find_pack(pack_id, paths)
|
|
raw = _read_json(pack_path)
|
|
_validate_envelope(raw, pack_id)
|
|
_validate_ratification(raw, pack_id, require_ratified, pack_path)
|
|
commitments = _validate_commitments(raw["commitment_ids"], pack_id)
|
|
descriptions = _validate_descriptions(
|
|
raw.get("commitment_descriptions", {}), pack_id, commitments,
|
|
)
|
|
domain = _validate_domain(raw.get("domain", "general"), pack_id)
|
|
refusal_commitments = _validate_opt_in_subset(
|
|
raw.get("refusal_commitments", []),
|
|
pack_id=pack_id,
|
|
commitments=commitments,
|
|
field_name="refusal_commitments",
|
|
)
|
|
hedge_commitments = _validate_opt_in_subset(
|
|
raw.get("hedge_commitments", []),
|
|
pack_id=pack_id,
|
|
commitments=commitments,
|
|
field_name="hedge_commitments",
|
|
)
|
|
overlap = set(refusal_commitments) & set(hedge_commitments)
|
|
if overlap:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: commitments cannot appear in both "
|
|
f"refusal_commitments and hedge_commitments: "
|
|
f"{sorted(overlap)}"
|
|
)
|
|
return EthicsPack(
|
|
pack_id=str(raw["pack_id"]),
|
|
version=str(raw["version"]),
|
|
description=str(raw["description"]),
|
|
domain=domain,
|
|
commitment_ids=frozenset(commitments),
|
|
commitment_descriptions=descriptions,
|
|
mastery_report_sha256=str(raw.get("mastery_report_sha256", "")),
|
|
ratified=bool(raw.get("mastery_report_sha256")),
|
|
refusal_commitments=frozenset(refusal_commitments),
|
|
hedge_commitments=frozenset(hedge_commitments),
|
|
)
|
|
|
|
|
|
def available_packs(
|
|
search_paths: Iterable[Path | str] | None = None,
|
|
) -> list[dict[str, object]]:
|
|
"""List discoverable ethics packs as
|
|
``{"pack_id", "version", "description", "domain", "ratified", "path"}``
|
|
dicts in lex order.
|
|
"""
|
|
paths = _resolve_search_paths(search_paths)
|
|
seen: dict[str, dict[str, object]] = {}
|
|
for d in paths:
|
|
if not d.is_dir():
|
|
continue
|
|
for entry in sorted(d.glob("*.json")):
|
|
if entry.name.endswith(".mastery_report.json"):
|
|
continue
|
|
try:
|
|
raw = _read_json(entry)
|
|
except EthicsPackError:
|
|
continue
|
|
if not isinstance(raw, dict):
|
|
continue
|
|
if "schema_version" not in raw or "commitment_ids" not in raw:
|
|
continue
|
|
pack_id = str(raw.get("pack_id", entry.stem))
|
|
if pack_id in seen:
|
|
continue
|
|
seen[pack_id] = {
|
|
"pack_id": pack_id,
|
|
"version": str(raw.get("version", "")),
|
|
"description": str(raw.get("description", "")),
|
|
"domain": str(raw.get("domain", "general")),
|
|
"ratified": bool(raw.get("mastery_report_sha256")),
|
|
"path": str(entry),
|
|
}
|
|
return sorted(seen.values(), key=lambda d: str(d["pack_id"]))
|
|
|
|
|
|
# ---------- internals ----------
|
|
|
|
|
|
def _resolve_search_paths(
|
|
search_paths: Iterable[Path | str] | None,
|
|
) -> tuple[Path, ...]:
|
|
if search_paths is None:
|
|
return _DEFAULT_SEARCH_PATHS
|
|
return tuple(Path(p) for p in search_paths)
|
|
|
|
|
|
def _find_pack(pack_id: str, paths: tuple[Path, ...]) -> Path:
|
|
if not pack_id or "/" in pack_id or ".." in pack_id:
|
|
raise EthicsPackError(f"invalid ethics pack_id: {pack_id!r}")
|
|
for d in paths:
|
|
candidate = d / f"{pack_id}.json"
|
|
if candidate.is_file():
|
|
return candidate
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r} not found in {[str(p) for p in paths]}"
|
|
)
|
|
|
|
|
|
def _read_json(path: Path) -> dict:
|
|
try:
|
|
with path.open("r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise EthicsPackError(
|
|
f"failed to read ethics pack {path}: {exc}"
|
|
) from exc
|
|
if not isinstance(data, dict):
|
|
raise EthicsPackError(
|
|
f"ethics pack {path} did not deserialize to a dict"
|
|
)
|
|
return data
|
|
|
|
|
|
def _validate_envelope(raw: dict, pack_id: str) -> None:
|
|
required = (
|
|
"pack_id",
|
|
"version",
|
|
"description",
|
|
"schema_version",
|
|
"domain",
|
|
"commitment_ids",
|
|
)
|
|
missing = [k for k in required if k not in raw]
|
|
if missing:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r} missing required fields: {missing}"
|
|
)
|
|
if raw.get("schema_version") != "1.0.0":
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: unsupported schema_version "
|
|
f"{raw.get('schema_version')!r}"
|
|
)
|
|
if raw.get("pack_id") != pack_id:
|
|
raise EthicsPackError(
|
|
f"ethics pack file declares pack_id={raw.get('pack_id')!r} "
|
|
f"but was requested as {pack_id!r}"
|
|
)
|
|
|
|
|
|
def _validate_ratification(
|
|
raw: dict, pack_id: str, require_ratified: bool | None, pack_path: Path,
|
|
) -> None:
|
|
if require_ratified is False:
|
|
return
|
|
if require_ratified is None:
|
|
require_ratified = (
|
|
os.environ.get("CORE_ALLOW_UNRATIFIED_ETHICS") != "1"
|
|
)
|
|
if not require_ratified:
|
|
return
|
|
declared_sha = raw.get("mastery_report_sha256", "")
|
|
if not declared_sha:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r} is not ratified "
|
|
"(mastery_report_sha256 empty); set "
|
|
"CORE_ALLOW_UNRATIFIED_ETHICS=1 for development, or ratify "
|
|
"via scripts/ratify_ethics_pack.py."
|
|
)
|
|
report_path = pack_path.parent / f"{pack_id}.mastery_report.json"
|
|
if not report_path.is_file():
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: companion report "
|
|
f"{report_path.name!r} is missing"
|
|
)
|
|
try:
|
|
with report_path.open("r", encoding="utf-8") as f:
|
|
report = json.load(f)
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: failed to read companion report: {exc}"
|
|
) from exc
|
|
if not isinstance(report, dict):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: companion report is not a JSON object"
|
|
)
|
|
if report.get("report_sha256") != declared_sha:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: companion report SHA "
|
|
f"{str(report.get('report_sha256'))[:12]}... does not match "
|
|
f"pack's declared {declared_sha[:12]}..."
|
|
)
|
|
if not verify_seal(report, sha_field="report_sha256"):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: companion report failed self-seal "
|
|
"verification"
|
|
)
|
|
if not report.get("ratified", False):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: companion report has ratified=False"
|
|
)
|
|
|
|
|
|
def _validate_commitments(value: object, pack_id: str) -> list[str]:
|
|
if not isinstance(value, list) or len(value) < 1:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: commitment_ids must be a non-empty list"
|
|
)
|
|
seen: set[str] = set()
|
|
out: list[str] = []
|
|
for i, c in enumerate(value):
|
|
if not isinstance(c, str) or not c:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: commitment_ids[{i}] must be a "
|
|
"non-empty string"
|
|
)
|
|
if c in seen:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: duplicate commitment_id {c!r}"
|
|
)
|
|
seen.add(c)
|
|
out.append(c)
|
|
return out
|
|
|
|
|
|
def _validate_descriptions(
|
|
value: object, pack_id: str, commitments: list[str],
|
|
) -> dict[str, str]:
|
|
if not isinstance(value, dict):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: commitment_descriptions must be a dict"
|
|
)
|
|
out: dict[str, str] = {}
|
|
for c in commitments:
|
|
desc = value.get(c, "")
|
|
if not isinstance(desc, str):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: commitment_descriptions[{c!r}] "
|
|
"must be a string"
|
|
)
|
|
out[c] = desc
|
|
return out
|
|
|
|
|
|
def _validate_opt_in_subset(
|
|
value: object,
|
|
*,
|
|
pack_id: str,
|
|
commitments: list[str],
|
|
field_name: str,
|
|
) -> list[str]:
|
|
"""Validate an opt-in list of commitment ids.
|
|
|
|
ADR-0037 (``refusal_commitments``) and ADR-0038
|
|
(``hedge_commitments``) both encode opt-in subsets of
|
|
``commitment_ids``. An unknown or duplicate id is a load-time
|
|
error — silent typos would be catastrophic given the behavioral
|
|
consequences.
|
|
"""
|
|
if value is None or value == []:
|
|
return []
|
|
if not isinstance(value, list):
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: {field_name} must be a list"
|
|
)
|
|
seen: set[str] = set()
|
|
valid = set(commitments)
|
|
out: list[str] = []
|
|
for i, c in enumerate(value):
|
|
if not isinstance(c, str) or not c:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: {field_name}[{i}] must be a "
|
|
"non-empty string"
|
|
)
|
|
if c in seen:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: duplicate {field_name} entry {c!r}"
|
|
)
|
|
if c not in valid:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: {field_name} entry {c!r} is not "
|
|
"a declared commitment_id"
|
|
)
|
|
seen.add(c)
|
|
out.append(c)
|
|
return out
|
|
|
|
|
|
def _validate_domain(value: object, pack_id: str) -> str:
|
|
if not isinstance(value, str) or not value:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: domain must be a non-empty string"
|
|
)
|
|
if value not in _ALLOWED_DOMAINS:
|
|
raise EthicsPackError(
|
|
f"ethics pack {pack_id!r}: domain={value!r} not in "
|
|
f"{sorted(_ALLOWED_DOMAINS)}"
|
|
)
|
|
return value
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_ETHICS_PACK",
|
|
"EthicsPack",
|
|
"EthicsPackError",
|
|
"available_packs",
|
|
"load_ethics_pack",
|
|
]
|