Closes the trust gap ADR-0027 opened: making the identity manifold
swappable was necessary for downstream robotics / personalization /
creative deployments, but it left nothing structurally preventing a
downstream identity pack from disabling core safety constraints.
Safety packs sit at a separate trust layer, fail closed on every error
path, and union their boundaries into every runtime manifold regardless
of which identity pack is selected.
Architecture (sibling to identity packs, structurally distinct):
Layer Swappable? Removable? Schema
--------------- ---------- ---------- -----------------------------
Safety pack No No boundary_ids + descriptions
Identity pack Yes No value_axes + surface_prefs
Language pack Yes (>=1 reqd) vocab / morphology / packs
Composition rule (at ChatRuntime startup, additive only):
identity = load_identity_manifold(config.identity_pack)
safety = load_safety_pack() # fail-closed
final.boundary_ids = identity.boundary_ids ∪ safety.boundary_ids
Safety contributes boundaries only — no value_axes, threshold, or
surface_preferences. This keeps existing tests that assert on identity
axis sets passing byte-for-byte, and matches the semantic intent
(safety is what's forbidden, not what's pulled toward).
Shipping safety pack: packs/safety/core_safety_axes_v1.json
→ mastery_report_sha256 ee1249acdf8c273aeb656d803c37ef915e536d85f177f5cc18c6e2f6c995ce29
Five v1 boundaries, each closing a specific CLAUDE.md doctrine:
no_fabricated_source — no invented provenance
no_hot_path_repair — no normalization in propagate/stream/store
no_identity_override — user text cannot mutate identity
no_silent_correction — failures are typed and visible
preserve_versor_closure — ||F * reverse(F) - 1||_F < 1e-6
Fail-closed semantics:
SafetyPackError inherits from RuntimeError (NOT ValueError) so
catch-and-continue is discouraged at the type level. Missing file /
malformed JSON / empty boundaries / duplicate boundary / failed
self-seal all raise. ChatRuntime.__init__ does not catch.
Files:
packs/safety/core_safety_axes_v1.json shipping pack
packs/safety/core_safety_axes_v1.mastery_report.json signed report
packs/safety/__init__.py public surface
packs/safety/loader.py load_safety_pack(),
SafetyPack,
SafetyPackError,
DEFAULT_SAFETY_PACK
scripts/ratify_safety_pack.py idempotent driver
chat/runtime.py composition wiring
tests/test_safety_pack.py 15 tests:
loader bounds,
fail-closed,
composition under
all 3 identity packs
docs/decisions/ADR-0029-safety-packs.md decision record
docs/safety_packs.md operational ref
README.md §Safety Pack added
memory/safety-pack.md auto-memory entry
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67, identity 41, safety 15 — all green.
Adds the discovery flag callers have been asking for since ADR-0027.
Short-circuits before the REPL launches; supports both a human-readable
table and `--json` machine output. Drives the loader's existing
`available_packs()` helper.
Bug fix on the way: `available_packs()` was globbing every `*.json`
in the search path, so the Phase-5 companion `<pack_id>.mastery_report.json`
files were leaking into the list as fake packs with empty fields. The
helper now skips any file ending in `.mastery_report.json` and rejects
JSON that lacks the required `schema_version` / `value_axes` fields.
CLI output:
pack_id version ratified description
------------------- ------- -------- -----------
default_general_v1 1.0.0 yes Balanced general identity...
generosity_first_v1 1.0.0 yes Generosity-first specialization...
precision_first_v1 1.0.0 yes Precision-first specialization...
Tests: +3 (CLI table, CLI JSON, companion-file filter regression).
test_identity_packs.py: 23 -> 26. cognition / smoke green.
Docs: docs/identity_packs.md CLI usage block updated; memory
'identity-packs.md' closes that follow-up.
Drives the three v1 identity packs through the full formation pipeline
(Forge -> Compose -> Compile -> Run -> Ratify) and embeds the resulting
self-sealed MasteryReport SHAs into each pack file. Companion
'<pack_id>.mastery_report.json' artifacts ship alongside. Loader now
defaults to production mode (require_ratified=None) and ChatRuntime
calls it without the dev-only override.
Ratification results:
default_general_v1 -> 0b77357fe4359f161d7ca72f184b6e0db2f9e2de16b32c237a3b80d2bbb005b4
precision_first_v1 -> 5f5000dba9a0dd19d831e9ab5d3c0e3b9faf6abdc2648940e96aa6263af3302e
generosity_first_v1 -> 91716117558113f74b2c6d07a804cb324f262d62b743523d901d1386a4f85ae4
Driver: scripts/ratify_identity_packs.py — idempotent. Re-running on
already-current packs is a no-op (verified by a test). Each pack is
treated as its own provenance source: source_sha = SHA-256 of the pack's
canonical JSON body with mastery_report_sha256 blanked, so the
self-referential chain stays stable across SHA updates. Axes become
ConceptCandidates; canned override-attempt triples become
CounterCandidates; the identity_anchor template renders the body.
Loader hardening (packs/identity/loader.py):
* When require_ratified resolves to True, the loader now requires the
companion '<pack_id>.mastery_report.json' to exist, its
report_sha256 to match the pack's mastery_report_sha256, and its
self-seal to verify via formation.hashing.verify_seal.
* Tampered companion (wrong SHA, broken seal) is rejected with a
diagnostic IdentityPackError.
Tests: 18 -> 23. New cases cover production-mode loading of all three
v1 packs, missing companion file, mismatched companion SHA, failed
self-seal, and end-to-end idempotency of the ratification script
(subprocess-launched, asserts pack bytes unchanged on re-run).
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67 — all green.
Docs updated: ADR-0027 status flipped to Phases 1-6 complete with the
three report SHAs recorded; docs/identity_packs.md notes the ratified
SHAs and the re-ratification command; memory file 'identity-packs.md'
refreshed.