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.
113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
"""Concrete efferent gate policy and trace-safe decision records."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import numpy as np
|
|
|
|
from sensorium.audio.checksum import sha256_json
|
|
from sensorium.protocol import CL41_DIM, AuthorityToken, EfferentVerdict
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EfferentEmissionTrace:
|
|
"""Trace-safe record of an efferent admission or refusal."""
|
|
|
|
pack_id: str
|
|
admitted: bool
|
|
reason: str
|
|
authority_sha256: str
|
|
policy_sha256: str
|
|
capability: str
|
|
trace_sha256: str
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"pack_id": self.pack_id,
|
|
"admitted": self.admitted,
|
|
"reason": self.reason,
|
|
"authority_sha256": self.authority_sha256,
|
|
"policy_sha256": self.policy_sha256,
|
|
"capability": self.capability,
|
|
"trace_sha256": self.trace_sha256,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DefaultEfferentGate:
|
|
"""Capability-scoped efferent gate.
|
|
|
|
Admission requires a valid ``(32,)`` vector and one of:
|
|
``decode:<pack_id>``, ``decode:*``, or ``*`` in the authority token.
|
|
"""
|
|
|
|
policy_id: str = "default-efferent-v1"
|
|
|
|
@property
|
|
def policy_sha256(self) -> str:
|
|
return sha256_json({
|
|
"policy_id": self.policy_id,
|
|
"required_capability": "decode:<pack_id>",
|
|
"wildcards": ["decode:*", "*"],
|
|
"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,
|
|
mv: np.ndarray,
|
|
authority: AuthorityToken,
|
|
) -> EfferentVerdict:
|
|
vec = np.asarray(mv, dtype=np.float32)
|
|
if vec.shape != (CL41_DIM,):
|
|
return EfferentVerdict(
|
|
admitted=False,
|
|
reason=f"invalid efferent vector shape: {vec.shape}",
|
|
authority_sha256=authority.authority_sha256,
|
|
policy_sha256=self.policy_sha256,
|
|
)
|
|
required = f"decode:{pack_id}"
|
|
caps = set(authority.capabilities)
|
|
admitted = required in caps or "decode:*" in caps or "*" in caps
|
|
return EfferentVerdict(
|
|
admitted=admitted,
|
|
reason="admitted" if admitted else f"missing capability: {required}",
|
|
authority_sha256=authority.authority_sha256,
|
|
policy_sha256=self.policy_sha256,
|
|
)
|
|
|
|
def trace(
|
|
self,
|
|
pack_id: str,
|
|
authority: AuthorityToken,
|
|
verdict: EfferentVerdict,
|
|
) -> EfferentEmissionTrace:
|
|
capability = f"decode:{pack_id}"
|
|
payload = {
|
|
"kind": "EfferentEmissionTrace",
|
|
"pack_id": pack_id,
|
|
"admitted": verdict.admitted,
|
|
"reason": verdict.reason,
|
|
"authority_sha256": authority.authority_sha256,
|
|
"policy_sha256": verdict.policy_sha256,
|
|
"capability": capability,
|
|
}
|
|
return EfferentEmissionTrace(
|
|
pack_id=pack_id,
|
|
admitted=verdict.admitted,
|
|
reason=verdict.reason,
|
|
authority_sha256=authority.authority_sha256,
|
|
policy_sha256=verdict.policy_sha256,
|
|
capability=capability,
|
|
trace_sha256=sha256_json(payload),
|
|
)
|