9.4 KiB
ADR-0051 — Trust-Boundary Hardening Pass
Status: Accepted Date: 2026-05-18 Author: Shay
Context
CLAUDE.md § Security and Trust Boundaries enumerates six high-risk
surfaces and a required approach. Up to this point those rules have been
enforced by convention across ADR-0027 (identity packs), ADR-0029
(safety packs), ADR-0033 (ethics packs), and the core pack validate
CLI introduced earlier. Several of the load-bearing properties had
ad-hoc enforcement and no dedicated regression tests, which means a
future refactor could quietly weaken them.
This ADR is an audit pass over the four mutable surfaces named in the doctrine and converts the live properties into type-level and test-level guards:
core pack validatedynamic validator execution.- Language / source pack loading via
language_packs.compiler. - OOV token grounding error messages.
- Pack mutation proposal-only enforcement.
The ADR adds no new capability. It hardens what is already there and pins the trust boundary in regression tests so it can be verified in CI on every change.
Decision
Surface 1 — core pack validate dynamic validator execution
Audited state. Already hardened in core/cli.py:
_safe_pack_id(pack_id)rejects empty ids,..traversal, leading/, and embedded path separators before anyPathjoin.cmd_pack_validaterequires--allow-arbitrary-codeto execute the pack'svalidators.py. Default behaviour is fail-closed.--dry-runreports validator presence withoutimportlib.utilresolving the module spec.
This ADR adds no change to that surface and confirms via
tests/test_cli_pack_validate_security.py (pre-existing) that the
boundary holds. Result: the boundary is doctrinally complete.
Surface 2 — Language / source pack loading
Audited state — gap found. language_packs/compiler.py exposed
three public entrypoints — load_pack(pack_id),
load_pack_entries(pack_id), and load_mounted_packs(pack_ids) — that
concatenated their argument straight into a filesystem path
(Path(__file__).parent / "data" / pack_id) without any traversal
guard. The sibling pack loaders (packs/identity/loader.py,
packs/safety/loader.py, packs/ethics/loader.py) already had a
_find_pack guard; the language-pack loader did not.
Fix. A module-private _validate_pack_id runs before any
Path operation at every public entrypoint. It rejects:
- non-string inputs,
- empty strings,
..substrings,/or\separators,- leading
.(hidden-dir convention), - any character outside ASCII alphanumerics,
_, or-.
The guard's exception message routes the offending fragment through
core._safe_display.safe_pack_id so a hostile pack id cannot inject
control characters into the error string.
Surface 3 — OOV grounding error messages
Audited state. The OOV-grounding error message in
chat/runtime.py:_apply_oov_policy interpolates a raw user-controlled
token into a KeyError string
(KeyError(f"OOV token requires vocab proposal: {token}")). The
exception is raised at the runtime boundary and is caught by
respond(), but other callers consume the exception text directly.
Fix. This ADR introduces a central safe-display sanitiser at
core/_safe_display.py with two helpers:
safe_display(value, *, max_len=64)— neutralises ASCII C0 and C1 control characters (the ANSI ESC\x1bprefix, newlines, carriage returns, NULs, DEL, and the C1 range 0x80-0x9F), coerces non-strings throughrepr, and truncates with a trailing"...". The transformation is deterministic, pure, and lossy on purpose.safe_pack_id(value)— a narrower mask suitable for pack ids: only ASCII alphanumerics,-,_, and.survive; everything else is replaced with?.
chat/runtime.py is on the doctrine-fenced file list for this ADR, so
the runtime call-site itself is not rewired here — core._safe_display
ships as the canonical helper that the runtime OOV site (and any
future log/error site) must route through. That follow-up wiring
is a small, mechanical change reserved for a separate ADR that touches
the chat runtime under its own review.
The sanitiser is doctrinally a logging/display helper. It must never
be imported by algebra/, generate/, field/, or vault/ — the
module docstring states this and the test suite asserts it stays out
of those paths transitively.
Surface 4 — Pack mutation proposal-only enforcement
Audited state. teaching/store.py:PackMutationProposal is already
declared as @dataclass(frozen=True, slots=True) with
applied: bool = False and epistemic_status = EpistemicStatus.SPECULATIVE defaults. with_status returns a new
instance via dataclasses.replace. No apply_proposal /
apply_mutation path exists in the codebase.
Fix. No code change — the type was already correct. This ADR
adds tests/test_mutation_proposal_type.py, which encodes the
invariants as CI-enforced guards:
- The class is a dataclass.
- Frozen — direct attribute assignment raises
FrozenInstanceError. - Slots — arbitrary attribute monkey-patching raises
AttributeError. applieddefaults toFalse.- Default
epistemic_statusisSPECULATIVE. with_statusreturns a new instance, never mutates the original.- No field named
final,frozen,axiom,permanent, orimmutableexists on the dataclass (ADR-0021 non-hardening invariant).
If a future refactor weakens any of these, CI fails before the change merges.
Why this is doctrine-aligned
CLAUDE.md prohibits hidden normalisation, hot-path repair, stochastic fallback, approximate recall, and unreviewed mutation. This ADR:
- Adds no algebra.
versor_condition(F) < 1e-6is unaffected — no new operator, no field state touched. - Adds no hot-path normalisation.
core/_safe_display.pyis a display sanitiser, not a runtime normaliser. Importing it fromalgebra/,generate/,field/, orvault/is explicitly out of scope. - Adds no LLM fallback or stochastic sampling. The sanitiser is deterministic and pure.
- Adds no broad infrastructure. Two new small modules
(
core/_safe_display.py, the_validate_pack_idhelper insidelanguage_packs/compiler.py) and three new test files. - Pins the boundary in tests. The invariants previously enforced by convention now fail closed in CI.
Consequences
What changes
- New module
core/_safe_display.py— central sanitiser for user- controlled fragments in error / log messages. language_packs/compiler.pygains_validate_pack_idand wires it intoload_pack,load_pack_entries, andload_mounted_packs.- Three new test files:
tests/test_safe_display.py(20 tests)tests/test_language_pack_load_safety.py(24 tests)tests/test_mutation_proposal_type.py(9 tests)
What does not change
- No runtime behaviour for any happy-path code (real pack ids load unchanged).
chat/runtime.py,chat/pack_grounding.py,chat/telemetry.py,chat/verdicts.py,generate/intent.py,generate/intent_bridge.py,teaching/*,core/physics/identity.py, andcore/cognition/pipeline.pyare untouched.core eval cognitionmetrics are unchanged. Baseline:intent_accuracy = 100.0 %,surface_groundedness = 69.2 %,term_capture_rate = 58.3 %,versor_closure_rate = 100.0 %.
Follow-up reserved for a separate ADR
- Wiring
chat/runtime.py:_apply_oov_policy'sKeyError(f"OOV token requires vocab proposal: {token}")to route throughcore._safe_display.safe_display(token). The mechanical change is one line; the fence keeps it out of scope here.
Scope limits
- This ADR is an audit pass, not a feature. It does not change any user-facing surface.
- The sanitiser caps at 64 characters by default and 48 for pack ids —
callers that need full fidelity in a trusted context can pass a
larger
max_len, but the default is the conservative choice.
Cross-References
CLAUDE.md § Security and Trust Boundaries— the doctrine this ADR audits.- ADR-0021 — the
non-hardening invariant on proposal status pinned by
test_no_forbidden_finality_flags_on_proposal. - ADR-0027 /
ADR-0029 /
ADR-0033 — sibling pack loaders that
already carry the
_find_packtraversal guard the language-pack loader now mirrors. - ADR-0040 — the redact-by-default telemetry sink whose discipline this ADR generalises to error- message sites.
Verification
tests/test_safe_display.py 20 tests, all green
tests/test_language_pack_load_safety.py 24 tests, all green
tests/test_mutation_proposal_type.py 9 tests, all green
tests/test_cli_pack_validate_security.py (pre-existing) all green
Lanes (all green on this branch):
core test --suite smoke 67 passed
core test --suite cognition 121 passed
core test --suite runtime 19 passed
core test --suite packs 6 passed
core test --suite algebra 132 passed
core eval cognition (baseline preserved):
intent_accuracy 100.0% (=)
surface_groundedness 69.2% (=)
term_capture_rate 58.3% (=)
versor_closure_rate 100.0% (=)
The non-negotiable field invariant (versor_condition(F) < 1e-6) is
preserved — this ADR introduces no algebra, no rotor construction, and
no field update.