From e5bc73ebb9406f47d128caafef7ed436e2358ce4 Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 3 Jun 2026 21:51:59 -0700 Subject: [PATCH 1/2] docs: reconcile sensorium ADR statuses with shipped implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0181/0197/0208/0209 were marked Proposed despite test-backed afferent implementations merged to main; flip to Accepted with verifiable Implementation lines (paths + PRs + falsifiable tests). ADR-0198's 'no implementation' status was stale (Gap A protocol change + baseline efferent gate landed in #541); correct it and document the deferred §3 safety/ethics verdict-lowering as an explicit blocking obligation before any motor decoder mounts. --- docs/decisions/ADR-0181-audio-compiler-delta-crdt.md | 3 ++- .../decisions/ADR-0197-vision-compiler-delta-crdt.md | 3 ++- .../ADR-0198-motor-efferent-decoder-spike.md | 12 +++++++++++- .../ADR-0208-environmental-sensorium-loop.md | 3 ++- .../ADR-0209-sensorimotor-feedback-contract.md | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/decisions/ADR-0181-audio-compiler-delta-crdt.md b/docs/decisions/ADR-0181-audio-compiler-delta-crdt.md index 85eccf3c..efcbf8f6 100644 --- a/docs/decisions/ADR-0181-audio-compiler-delta-crdt.md +++ b/docs/decisions/ADR-0181-audio-compiler-delta-crdt.md @@ -1,7 +1,8 @@ # ADR-0181: CORE-native Audio Compiler over the Delta-CRDT Substrate -**Status:** Proposed +**Status:** Accepted — implementation landed **Date:** 2026-05-29 +**Implementation:** `sensorium/audio/` + `evals/audio_sensorium/`. Proof obligations are covered by `tests/test_audio_compiler.py`, `tests/test_audio_crdt_merge.py`, `tests/test_audio_eval_gates.py`, `tests/test_audio_pack_manifest.py`, and `tests/test_audio_sensorium_mount.py`. **Authors:** Joshua M. Shay, Core R&D Engine **Domains:** `sensorium/audio/`, `sensorium/adapters/audio.py`, `packs/audio/`, `core-rs/src/vault.rs` (read-only contract), `evals/audio_sensorium/` **Depends on:** ADR-0013 (Sensorium Multimodal Protocol), ADR-0180 (Delta-CRDT Sharded Substrate) diff --git a/docs/decisions/ADR-0197-vision-compiler-delta-crdt.md b/docs/decisions/ADR-0197-vision-compiler-delta-crdt.md index 7111c1d2..9d5e95be 100644 --- a/docs/decisions/ADR-0197-vision-compiler-delta-crdt.md +++ b/docs/decisions/ADR-0197-vision-compiler-delta-crdt.md @@ -1,7 +1,8 @@ # ADR-0197: CORE-native Vision Compiler over the Delta-CRDT Substrate -**Status:** Proposed +**Status:** Accepted — implementation landed **Date:** 2026-05-31 +**Implementation:** `sensorium/vision/` + `sensorium/adapters/vision.py`, `packs/vision/vision_core_v1/` (PR #537). Proof obligations are covered by `tests/test_vision_compiler.py`, `tests/test_vision_crdt_merge.py`, `tests/test_vision_sensorium_mount.py`, and `tests/test_vision_eval_gates.py`. **Authors:** Joshua M. Shay, Core R&D Engine **Domains:** `sensorium/vision/`, `sensorium/adapters/vision.py`, `packs/vision/`, `core-rs/src/vault.rs` (read-only contract), `evals/vision_sensorium/` **Depends on:** ADR-0013 (Sensorium Multimodal Protocol), ADR-0180 (Delta-CRDT Sharded Substrate), ADR-0181 (Audio Compiler — structural precedent) diff --git a/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md b/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md index 50537ce5..c960ec98 100644 --- a/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md +++ b/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md @@ -1,6 +1,6 @@ # ADR-0198: Motor as Efferent Modality — Protocol Gap & Governance (Design Spike) -**Status:** Proposed (design spike — no implementation) +**Status:** Accepted (design spike) — Gap A protocol change + a baseline efferent gate have landed; the §3 verdict-lowering and the motor compiler/decoder remain deferred. See **Implementation Status** below. **Date:** 2026-05-31 **Authors:** Joshua M. Shay, Core R&D Engine **Domains:** `sensorium/protocol.py`, `sensorium/registry.py`, `packs/motor/` *(future)*, governance packs (ADR-0029/0033/0036/0037) @@ -9,6 +9,16 @@ --- +## Implementation Status (reconciled 2026-06-04) + +**Gap A has landed.** `ModalityRegistry.decode(pack_id, mv, *, authority)` with a required, never-optional `AuthorityToken`, plus a baseline `DefaultEfferentGate` (`sensorium/efferent.py`, PR #541). The §1.2 Gap B ordering is **proven**: `tests/test_efferent_gate.py::test_registry_uses_default_efferent_gate_before_decoder` asserts `decoder.calls == 0` on refusal — the gate refuses *in the manifold*, before any command is formed. Traces are hash-only (no `mv`, no ndarray/bytes). + +**Deferred — blocking obligations before any motor decoder mounts:** + +1. **§3 verdict-lowering is NOT implemented.** `DefaultEfferentGate` admits on capability-token + `(32,)` vector-shape only; it does **not** lower the decoded motor versor into the safety/ethics pack verdicts of ADR-0029/0033/0036/0037. A valid capability token alone currently passes the gate. This is admissible **only because no real motor decoder exists** — the sole `SurfaceDecoder` in the tree is a test fixture that returns the string `"decoded"`. The §1.2 Gap B guarantee ("passes safety/ethics verdicts before it leaves the boundary") is therefore *partially* realized. Before any pack mounts a decoder that actuates, the gate MUST be extended to the verdict-lowering path; this obligation is load-bearing and must not be silently skipped. +2. **The motor compiler/decoder itself remains out of scope** (per §5). +3. **A dedicated motor governance ADR** ratifying the §3 lowering against ADR-0029/0033/0036/0037 remains a prerequisite (per §5). + ## 1. Why motor is not "just another modality" Every modality landed so far is **afferent**: signal comes *in*, crosses the `ProjectionHead` boundary, becomes a `(32,)` versor. Motor is **efferent**: meaning leaves the manifold and becomes action in the world. The protocol already anticipates this asymmetry — `sensorium/protocol.py` defines: diff --git a/docs/decisions/ADR-0208-environmental-sensorium-loop.md b/docs/decisions/ADR-0208-environmental-sensorium-loop.md index 51d288cb..8e1d1139 100644 --- a/docs/decisions/ADR-0208-environmental-sensorium-loop.md +++ b/docs/decisions/ADR-0208-environmental-sensorium-loop.md @@ -1,7 +1,8 @@ # ADR-0208: Environmental Sensorium Loop -**Status:** Proposed +**Status:** Accepted — implementation landed (afferent scope) **Date:** 2026-06-04 +**Implementation:** `sensorium/environment/frame.py` (PR #540) + `sensorium/environment/harness.py` (PR #541). Proof obligations are covered by falsifiable tests in `tests/test_observation_frame_contract.py` (order-invariant frame trace hash, `merge_key` dedup, `TypeError` on raw-payload units, `ValueError` on efferent units) and `tests/test_observation_frame_harness.py`. Disjoint from the GSM8K serving path (no import of `generate.derivation` / `core.reliability_gate`). **Domains:** `sensorium/environment/`, `sensorium/compiler/`, `sensorium/*`, future sensorimotor compilers **Depends on:** ADR-0013, ADR-0180, ADR-0181, ADR-0197, ADR-0198 diff --git a/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md b/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md index b7d83124..8ff797e0 100644 --- a/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md +++ b/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md @@ -1,7 +1,8 @@ # ADR-0209: Sensorimotor Feedback Is Afferent -**Status:** Proposed +**Status:** Accepted — implementation landed (afferent scope) **Date:** 2026-06-04 +**Implementation:** `sensorium/sensorimotor/` (PR #540) + `sensorium/adapters/sensorimotor.py`, `packs/sensorimotor/sensorimotor_core_v1/` (PR #541). Proof obligations are covered by falsifiable tests in `tests/test_sensorimotor_contract.py` (deterministic unit + merge key, IR replay, idempotent delta merge, **no `decode`/`decode_batch` path**, hash-only trace with no command/trajectory payload) and `tests/test_sensorimotor_pack_manifest.py`. **Domains:** `sensorium/sensorimotor/`, `sensorium/protocol.py`, future robotics integrations **Depends on:** ADR-0013, ADR-0198, ADR-0208 From b4fb9df8e712a026af27dcfc63fdc39e698574f1 Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 3 Jun 2026 22:03:35 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(sensorium):=20fail-closed=20efferent?= =?UTF-8?q?=20gate=20for=20actuating=20decode=20(ADR-0198=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DefaultEfferentGate is a capability/shape pre-filter only; it does not lower decoded actions into safety/ethics pack verdicts (ADR-0198 §3 / §1.2 Gap B). ModalityRegistry.decode/decode_batch now refuse fail-closed any emission through a gate whose enforces_action_verdicts is False, unless an explicit allow_unverified_efferent sandbox opt-in is set. A real motor decoder thus cannot emit through the capability-only gate; the §3 verdict-lowering gate must be built and installed first. No production caller of the decode path exists today, so this closes the latent hazard before a motor decoder makes it load-bearing. Adds two falsifiable tests (fail-closed refusal; verdict-enforcing gate allowed). Disjoint from the GSM8K serving path. --- .../ADR-0198-motor-efferent-decoder-spike.md | 2 +- sensorium/efferent.py | 9 ++++ sensorium/protocol.py | 9 +++- sensorium/registry.py | 36 ++++++++++++++- tests/test_efferent_gate.py | 46 ++++++++++++++++++- tests/test_sensorium_efferent_decode.py | 6 +-- 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md b/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md index c960ec98..d4f86dcd 100644 --- a/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md +++ b/docs/decisions/ADR-0198-motor-efferent-decoder-spike.md @@ -15,7 +15,7 @@ **Deferred — blocking obligations before any motor decoder mounts:** -1. **§3 verdict-lowering is NOT implemented.** `DefaultEfferentGate` admits on capability-token + `(32,)` vector-shape only; it does **not** lower the decoded motor versor into the safety/ethics pack verdicts of ADR-0029/0033/0036/0037. A valid capability token alone currently passes the gate. This is admissible **only because no real motor decoder exists** — the sole `SurfaceDecoder` in the tree is a test fixture that returns the string `"decoded"`. The §1.2 Gap B guarantee ("passes safety/ethics verdicts before it leaves the boundary") is therefore *partially* realized. Before any pack mounts a decoder that actuates, the gate MUST be extended to the verdict-lowering path; this obligation is load-bearing and must not be silently skipped. +1. **§3 verdict-lowering is NOT implemented — but is now enforced fail-closed.** `DefaultEfferentGate` admits on capability-token + `(32,)` vector-shape only (`enforces_action_verdicts` is `False`); it does **not** lower the decoded motor versor into the safety/ethics pack verdicts of ADR-0029/0033/0036/0037. To keep §1.2 Gap B from silently degrading, `ModalityRegistry.decode`/`decode_batch` now **refuse fail-closed** any emission through a gate whose `enforces_action_verdicts` is False, unless an explicit `allow_unverified_efferent=True` sandbox opt-in is set (tests only). A real motor decoder therefore *cannot* emit through the capability-only gate — the §3 verdict-lowering gate must be built and installed first. Proven by `tests/test_efferent_gate.py::test_registry_fails_closed_for_actuating_decode_through_capability_only_gate` (the decoder never runs) and `::test_registry_admits_decode_through_verdict_enforcing_gate`. Implementing the §3 lowering itself remains deferred behind the dedicated motor governance ADR (item 3). 2. **The motor compiler/decoder itself remains out of scope** (per §5). 3. **A dedicated motor governance ADR** ratifying the §3 lowering against ADR-0029/0033/0036/0037 remains a prerequisite (per §5). diff --git a/sensorium/efferent.py b/sensorium/efferent.py index 0e9dc87b..0ae49173 100644 --- a/sensorium/efferent.py +++ b/sensorium/efferent.py @@ -53,6 +53,15 @@ class DefaultEfferentGate: "shape": [CL41_DIM], }) + @property + def enforces_action_verdicts(self) -> bool: + """Capability/shape pre-filter only — does NOT lower the decoded action + into the safety/ethics pack verdicts required by ADR-0198 §3. The + registry refuses actuating emission through this gate unless an explicit + sandbox opt-in is set. A future §3 verdict-enforcing gate returns True. + """ + return False + def admit( self, pack_id: str, diff --git a/sensorium/protocol.py b/sensorium/protocol.py index e60062ac..e8bdf66f 100644 --- a/sensorium/protocol.py +++ b/sensorium/protocol.py @@ -107,7 +107,14 @@ class EfferentVerdict: @runtime_checkable class EfferentGate(Protocol): - """Runtime gate for output actions. Runs before SurfaceDecoder.decode.""" + """Runtime gate for output actions. Runs before SurfaceDecoder.decode. + + Optional attribute ``enforces_action_verdicts: bool`` (absent ⇒ False): + True only for a gate that lowers the decoded action into the safety/ethics + pack verdicts required by ADR-0198 §3. ``ModalityRegistry`` fails closed and + refuses actuating emission through any gate where this is False, unless an + explicit ``allow_unverified_efferent`` sandbox opt-in is set. + """ def admit( self, diff --git a/sensorium/registry.py b/sensorium/registry.py index 5f616845..3c36da27 100644 --- a/sensorium/registry.py +++ b/sensorium/registry.py @@ -36,9 +36,19 @@ class ModalityRegistry: mv = registry.project("en", "beginning") """ - def __init__(self, *, efferent_gate: EfferentGate | None = None) -> None: + def __init__( + self, + *, + efferent_gate: EfferentGate | None = None, + allow_unverified_efferent: bool = False, + ) -> None: self._packs: dict[str, ModalityPack] = {} self._efferent_gate = efferent_gate + # Fail-closed (ADR-0198 §3 / §1.2 Gap B): an efferent gate that does not + # lower the decoded action into safety/ethics pack verdicts may not + # authorize a real emission. This opt-in exercises the capability + # pre-filter in isolation (tests only); never set it on an actuating path. + self._allow_unverified_efferent = allow_unverified_efferent # ------------------------------------------------------------------ # Mount @@ -123,6 +133,28 @@ class ModalityRegistry: ) return mvs.astype(np.float32) + def _refuse_unverified_efferent(self, pack_id: str, authority: AuthorityToken) -> None: + """Fail closed unless the gate enforces ADR-0198 §3 action verdicts. + + ``DefaultEfferentGate`` is a capability/shape pre-filter only; it does + not lower the decoded action into the safety/ethics pack verdicts that + §3 requires. Until a verdict-enforcing gate exists, an actuating + emission must be refused — no motor decoder may rely on capability + tokens alone. ``allow_unverified_efferent`` is a sandbox/testing escape + hatch for exercising the pre-filter in isolation. + """ + if self._allow_unverified_efferent: + return + if getattr(self._efferent_gate, "enforces_action_verdicts", False): + return + verdict = EfferentVerdict( + admitted=False, + reason="efferent gate does not enforce ADR-0198 §3 action verdicts", + authority_sha256=authority.authority_sha256, + policy_sha256="deny-unverified-efferent", + ) + raise EfferentRefusal(pack_id, verdict) + def decode(self, pack_id: str, mv: np.ndarray, *, authority: AuthorityToken) -> Any: """Decode a manifold vector through a governed SurfaceDecoder. @@ -147,6 +179,7 @@ class ModalityRegistry: policy_sha256="deny-by-default", ) raise EfferentRefusal(pack_id, verdict) + self._refuse_unverified_efferent(pack_id, authority) verdict = self._efferent_gate.admit(pack_id, vec, authority) if not verdict.admitted: raise EfferentRefusal(pack_id, verdict) @@ -179,6 +212,7 @@ class ModalityRegistry: policy_sha256="deny-by-default", ) raise EfferentRefusal(pack_id, verdict) + self._refuse_unverified_efferent(pack_id, authority) for vec in arr: verdict = self._efferent_gate.admit(pack_id, vec, authority) if not verdict.admitted: diff --git a/tests/test_efferent_gate.py b/tests/test_efferent_gate.py index 4f812219..e9074ab3 100644 --- a/tests/test_efferent_gate.py +++ b/tests/test_efferent_gate.py @@ -4,7 +4,14 @@ import numpy as np import pytest from sensorium.efferent import DefaultEfferentGate -from sensorium.protocol import AuthorityToken, EfferentRefusal, Modality, ModalityPack, ModalityVocabulary +from sensorium.protocol import ( + AuthorityToken, + EfferentRefusal, + EfferentVerdict, + Modality, + ModalityPack, + ModalityVocabulary, +) from sensorium.registry import ModalityRegistry @@ -81,7 +88,9 @@ def test_default_efferent_trace_is_hash_only(): def test_registry_uses_default_efferent_gate_before_decoder(): decoder = _Decoder() - reg = ModalityRegistry(efferent_gate=DefaultEfferentGate()) + # Exercises the capability pre-filter in isolation: the capability/shape + # gate is not verdict-enforcing, so the sandbox opt-in is required here. + reg = ModalityRegistry(efferent_gate=DefaultEfferentGate(), allow_unverified_efferent=True) reg.mount(_pack(decoder)) with pytest.raises(EfferentRefusal, match="missing capability"): @@ -90,3 +99,36 @@ def test_registry_uses_default_efferent_gate_before_decoder(): assert reg.decode("motor_test", _mv(), authority=_authority("decode:motor_test")) == "decoded" assert decoder.calls == 1 + + +def test_registry_fails_closed_for_actuating_decode_through_capability_only_gate(): + """ADR-0198 §3 / §1.2 Gap B: a capability/shape gate must not authorize a + real emission. With no sandbox opt-in the decode fails closed and the + decoder never runs.""" + decoder = _Decoder() + reg = ModalityRegistry(efferent_gate=DefaultEfferentGate()) + reg.mount(_pack(decoder)) + with pytest.raises(EfferentRefusal, match="action verdicts"): + reg.decode("motor_test", _mv(), authority=_authority("decode:motor_test")) + assert decoder.calls == 0 + + +def test_registry_admits_decode_through_verdict_enforcing_gate(): + """A gate that enforces ADR-0198 §3 action verdicts needs no sandbox opt-in.""" + + class _VerdictGate: + enforces_action_verdicts = True + + def admit(self, pack_id: str, mv: np.ndarray, authority: AuthorityToken) -> EfferentVerdict: + return EfferentVerdict( + admitted=True, + reason="admitted", + authority_sha256=authority.authority_sha256, + policy_sha256="verdict-test", + ) + + decoder = _Decoder() + reg = ModalityRegistry(efferent_gate=_VerdictGate()) + reg.mount(_pack(decoder)) + assert reg.decode("motor_test", _mv(), authority=_authority("decode:motor_test")) == "decoded" + assert decoder.calls == 1 diff --git a/tests/test_sensorium_efferent_decode.py b/tests/test_sensorium_efferent_decode.py index f85cdae7..731d399e 100644 --- a/tests/test_sensorium_efferent_decode.py +++ b/tests/test_sensorium_efferent_decode.py @@ -80,7 +80,7 @@ def test_decode_denies_by_default_before_decoder_runs(): def test_decode_requires_admitting_gate_before_surface_decode(): decoder = _Decoder() gate = _Gate(admitted=True) - reg = ModalityRegistry(efferent_gate=gate) + reg = ModalityRegistry(efferent_gate=gate, allow_unverified_efferent=True) reg.mount(_pack(decoder)) assert reg.decode("motor_test", _mv(), authority=_authority()) == "decoded:1.0" assert gate.calls == 1 @@ -89,7 +89,7 @@ def test_decode_requires_admitting_gate_before_surface_decode(): def test_decode_refusal_does_not_call_decoder(): decoder = _Decoder() - reg = ModalityRegistry(efferent_gate=_Gate(admitted=False)) + reg = ModalityRegistry(efferent_gate=_Gate(admitted=False), allow_unverified_efferent=True) reg.mount(_pack(decoder)) with pytest.raises(EfferentRefusal, match="denied"): reg.decode("motor_test", _mv(), authority=_authority()) @@ -99,7 +99,7 @@ def test_decode_refusal_does_not_call_decoder(): def test_decode_batch_admits_all_before_decoding_any_surface(): decoder = _Decoder() gate = _Gate(admitted=True, deny_after=1) - reg = ModalityRegistry(efferent_gate=gate) + reg = ModalityRegistry(efferent_gate=gate, allow_unverified_efferent=True) reg.mount(_pack(decoder)) batch = np.stack([_mv(), _mv()]) with pytest.raises(EfferentRefusal, match="denied"):