azazel_processor: commit 6 — protocol envelope (property 8)

Pass 4 / Finding F, commit 6 of 10 from BUILD_AZAZEL.md. Closes
the Python side of the build; commits 7-9 are cross-language
integrations (L3, observability endpoint, intake-side routing).

Vayikra 16: the Azazel goat goes ONCE A YEAR, on Yom Kippur, under
the full ritual envelope.
Vayikra 17:7: וְלֹא־יִזְבְּחוּ עוֹד אֶת־זִבְחֵיהֶם לַשְּׂעִירִם —
they shall no longer offer their sacrifices to the sa'irim. Same
destination outside the envelope is avodah zarah (the failure mode
specs/eish_zarah_reading.md catalogs).

THE GATE:

  - is_yom_kippur_protocol_active(today=None) — predicate; True
    when an operator-configured Gregorian window covers today
  - authorize_disposal() — context manager; explicit per-thread
    authorization for ONE disposal cycle (event-driven disposals
    during the year)
  - has_explicit_disposal_authorization() — predicate; True iff
    the current thread is inside an authorize_disposal block
  - assert_protocol_envelope_active() — gate; raises ProtocolViolation
    if NEITHER condition is met. Wired into dispatch_for_disposal
    as the FIRST check (before role gate, before any side-effect)

WHY a CONTEXT MANAGER, not a flag:

A persistent flag would let an operator "set authorization true and
leave it" — the avodah-zarah shape (using the channel without the
envelope). The context manager forces explicit scope: authorization
exists only within the with-block, only on the thread that opened it.

WHY PER-THREAD:

Process-wide authorization would let two concurrent disposals share
without explicit coordination — operator A's authorize_disposal()
would silently authorize operator B's dispatch_for_disposal(). Per-
thread isolation forces each disposal to carry its own authorization
explicitly.

WHY OPERATOR-CONFIGURED Gregorian DATES, not Hebrew calendar:

The Hebrew-calendar conversion lives in vav_pipeline (Elixir) — we
do not bring a Hebrew-calendar library into azazel_processor's
dependency surface. Operators set the Gregorian range each year (or
delegate it to a wrapper script). For now, this module is purely
data-shape; commit 9 may wire vav_pipeline's Hebrew calendar to set
the window automatically.

CREATED:

  src/azazel_processor/protocol.py        ~155 lines
    ProtocolViolation                     — raised on out-of-envelope
                                            invocation; error message
                                            names BOTH legitimate paths
                                            (Yom Kippur window, authorize
                                            context) so the operator
                                            knows their options
    set_yom_kippur_window(start, end)     — operator-configured;
                                            inclusive Gregorian dates;
                                            inverted range clears
    clear_yom_kippur_window()             — disable
    is_yom_kippur_protocol_active(today=None)
                                          — date-range membership
    authorize_disposal()                  — context manager; reentrant
                                            (nested entries each
                                            increment a counter, each
                                            exit decrements; cleared
                                            via try/finally even on
                                            inner exception)
    has_explicit_disposal_authorization() — per-thread, scoped
    assert_protocol_envelope_active()     — the gate

CHANGED:

  src/azazel_processor/azazel.py
    dispatch_for_disposal now calls assert_protocol_envelope_active()
    as the FIRST check. Outside the envelope: ProtocolViolation
    raised before any side-effect (no shaliach activation, no payload
    held, no checkpoint persisted).

  test/test_azazel.py
    Added an autouse `_envelope` fixture that wraps every existing
    orchestrator test in `with authorize_disposal():`. Protocol
    restriction is verified separately in test_protocol_restriction.py.

CREATED:

  test/test_protocol_restriction.py      16 tests covering:
    - default state (no window, no authorization) refuses dispatch
      with a ProtocolViolation that names both legitimate paths
    - assert_protocol_envelope_active raises in default state
    - both predicates False in default state
    - Yom Kippur window: today inside → active; outside → inactive;
      inverted range clears; inclusive at boundaries; clear disables
    - authorize_disposal: inside block → active; nested blocks
      decrement correctly; exception inside still clears (try/finally);
      per-thread isolation (sibling thread sees False even when parent
      thread is inside an authorize_disposal block)
    - dispatch proceeds inside authorize_disposal context
    - dispatch proceeds inside Yom Kippur window
    - outside envelope: NO side-effect (no in-flight file written, no
      payload held by shaliach)
    - protocol gate fires BEFORE role gate (inactive handler outside
      envelope raises ProtocolViolation, not ShaliachError)

  Per-test fixture clears Yom Kippur state on entry and exit so
  module-level state doesn't bleed between tests.

PYTHON SIDE NOW FEATURE-COMPLETE:

  Properties defended end-to-end via Python code:
    2  goral
    3  vidui
    4  shaliach + tevilah
    5  checkpoint persistence
    7  authorized routing
    8  protocol envelope
    10 hard termination
    11 classification discipline
    12 unmanaged destination

  Properties remaining (cross-language, in commits 7-9):
    1  parity provisioning      — commit 9 (Korban→Azazel intake)
    6  out-of-band acknowledgment — commit 9 (chatzotzrot wiring)
    9  observable disposal       — commit 7 (L3) + commit 8 (endpoint)

Tests: azazel_processor 125 passed (was 109, +16). korban_processor
unchanged at 203 passed, 1 skipped. No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.7 committed 2026-04-28 08:06:27 -04:00
1 parent c3267c59bf
commit 6eb555a5cf
5 files changed
+510 -4

No files matched your search

@@ -6,9 +6,9 @@ uv'd'mim, Yoma 62a). One goat to HaShem (processing); one goat to
Azazel (disposal). The two channels are provisioned at parity, from Azazel (disposal). The two channels are provisioned at parity, from
the same pool, with assignment blind to the operator. the same pool, with assignment blind to the operator.
Build state: commit 5 — orchestrator state machine added. See Build state: commit 6 — protocol envelope added. The Python side of
BUILD_AZAZEL.md for the full 10-commit staging plan and the the build is now feature-complete; commits 7-9 wire the cross-language
12-property → code mapping from azazel_engineering_reading.md §14. integrations (L3, observability endpoint, intake-side routing).
Properties defended so far: Properties defended so far:
- Property 2 (blind routing / goral) — cast_lot - Property 2 (blind routing / goral) — cast_lot
@@ -17,6 +17,8 @@ Properties defended so far:
- Property 5 (provisioned infrastructure) — checkpoint persistence - Property 5 (provisioned infrastructure) — checkpoint persistence
- Property 7 (authorized routing) — AzazelRecord owns - Property 7 (authorized routing) — AzazelRecord owns
the routing decision the routing decision
- Property 8 (protocol-restricted access) — envelope gate at
dispatch entry
- Property 10 (hard termination) — DESTROYED checkpoint - Property 10 (hard termination) — DESTROYED checkpoint
+ record without payload + record without payload
- Property 11 (classification discipline) — AzazelStage* - Property 11 (classification discipline) — AzazelStage*
@@ -34,9 +36,13 @@ Public surface:
GoralError — blind routing GoralError — blind routing
AzazelInput, AzazelRecord — orchestrator data types AzazelInput, AzazelRecord — orchestrator data types
dispatch_for_disposal, list_in_flight — orchestrator API dispatch_for_disposal, list_in_flight — orchestrator API
ProtocolViolation, authorize_disposal,
is_yom_kippur_protocol_active,
has_explicit_disposal_authorization,
set_yom_kippur_window,
clear_yom_kippur_window — protocol envelope
Pending in subsequent commits: Pending in subsequent commits:
protocol envelope (commit 6 — protocol-restricted access)
L3 azazel_records integration (commit 7) L3 azazel_records integration (commit 7)
/peah/azazel_log endpoint (commit 8) /peah/azazel_log endpoint (commit 8)
Korban→Azazel routing + chatzotzrot (commit 9) Korban→Azazel routing + chatzotzrot (commit 9)
@@ -56,6 +62,15 @@ from azazel_processor.goral import (
cast_lot, cast_lot,
cast_lot_outcome_id, cast_lot_outcome_id,
) )
from azazel_processor.protocol import (
ProtocolViolation,
assert_protocol_envelope_active,
authorize_disposal,
clear_yom_kippur_window,
has_explicit_disposal_authorization,
is_yom_kippur_protocol_active,
set_yom_kippur_window,
)
from azazel_processor.shaliach import ( from azazel_processor.shaliach import (
IshItti, IshItti,
ShaliachError, ShaliachError,
@@ -97,4 +112,11 @@ __all__ = [
"dispatch_for_disposal", "dispatch_for_disposal",
"list_in_flight", "list_in_flight",
"DEFAULT_IN_FLIGHT_DIR", "DEFAULT_IN_FLIGHT_DIR",
"ProtocolViolation",
"authorize_disposal",
"is_yom_kippur_protocol_active",
"has_explicit_disposal_authorization",
"set_yom_kippur_window",
"clear_yom_kippur_window",
"assert_protocol_envelope_active",
] ]
@@ -69,6 +69,7 @@ from typing import Any, Callable, Optional
from azazel_processor.azazel_record import AzazelRecord from azazel_processor.azazel_record import AzazelRecord
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH from azazel_processor.eretz_gezerah import ERETZ_GEZERAH
from azazel_processor.protocol import assert_protocol_envelope_active
from azazel_processor.shaliach import IshItti, assert_shaliach_active from azazel_processor.shaliach import IshItti, assert_shaliach_active
from azazel_processor.stages import AzazelStage, AzazelStaged from azazel_processor.stages import AzazelStage, AzazelStaged
from azazel_processor.vidui import Vidui from azazel_processor.vidui import Vidui
@@ -203,6 +204,13 @@ def dispatch_for_disposal(
ValueError: case_ref is not filename-safe, or other input ValueError: case_ref is not filename-safe, or other input
validation fails validation fails
""" """
# Protocol envelope gate — property 8
# Must be the FIRST check: invoking the Azazel channel outside the
# envelope is the avodah-zarah shape (Vayikra 17:7), refused before
# any other side-effect (no shaliach activation, no payload
# registration, no checkpoint persistence).
assert_protocol_envelope_active()
# Role gate — property 4 # Role gate — property 4
assert_shaliach_active(handler) assert_shaliach_active(handler)
@@ -0,0 +1,202 @@
"""
Protocol envelope — property 8 (protocol-restricted access).
Vayikra 16 specifies the Azazel goat goes ONCE A YEAR, on Yom Kippur,
under the full ritual envelope: the kohen gadol presides, the lots
are cast, the vidui is enumerated, the goat is sent by the ish itti.
Vayikra 17:7 prohibits the same routing OUTSIDE that envelope:
וְלֹא־יִזְבְּחוּ עוֹד אֶת־זִבְחֵיהֶם לַשְּׂעִירִם — "they shall no
longer offer their sacrifices to the sa'irim." Same destination,
different routing authority. Inside the envelope it is the legitimate
Azazel-path; outside, it is avodah zarah — the failure mode
specs/eish_zarah_reading.md catalogs.
In Shorashim software:
- is_yom_kippur_protocol_active() — predicate: are we in the
annual Yom Kippur window the operator has configured? The
Hebrew-calendar conversion is left to the operator (set the
window explicitly via set_yom_kippur_window).
- authorize_disposal() — context manager: marks the current
thread as having explicit authorization for ONE disposal cycle.
Used for event-driven disposals during the year (e.g. an
empty-vital korban arriving outside the annual window). The
authorization is per-thread, scoped to the with-block; it is
NOT a persistent flag the operator can leave set.
- assert_protocol_envelope_active() — gate: raises ProtocolViolation
if NEITHER condition is met. dispatch_for_disposal calls this
at entry.
Why authorize_disposal() is a context manager rather than a flag:
A persistent flag would mean an operator could "set authorization
true and leave it" — which is exactly the avodah zarah shape (using
the channel without the protocol envelope). The context manager
forces explicit scope: the authorization exists ONLY within the
with-block, and only on the thread that opened it. A function called
across the with-block boundary loses authorization unless the caller
reopens it.
Why per-thread:
If authorization were process-wide, two concurrent disposals could
share authorization without explicit coordination — operator A's
authorize_disposal() would silently authorize operator B's
dispatch_for_disposal(). Per-thread isolation forces each disposal
to carry its own authorization explicitly.
"""
import contextlib
import threading
from datetime import date, datetime, timezone
from typing import Optional
class ProtocolViolation(RuntimeError):
"""Raised when a disposal-channel function is invoked outside
the protocol envelope.
The Mishkan analog: someone offered a sacrifice to the sa'irim
outside Yom Kippur (Vayikra 17:7). Same destination, different
routing authority — and outside the envelope, the routing
authority is illegitimate. The system refuses.
"""
# ============================================================
# Yom Kippur annual window (operator-configured)
# ============================================================
#
# Module-level state because the window is process-global by design:
# Yom Kippur is one date for the system, not per-thread. The operator
# sets it once at process startup and it applies to every subsequent
# dispatch on this process.
#
# Hebrew-calendar conversion is the operator's responsibility — we
# do not bring a Hebrew-calendar library into azazel_processor's
# dependency surface. The vav_pipeline (Elixir) side already knows
# how to compute Hebrew dates; for now this module just stores the
# Gregorian range the operator supplies.
_yom_kippur_window: Optional[tuple[date, date]] = None
_window_lock = threading.Lock()
def set_yom_kippur_window(start: date, end: date) -> None:
"""Configure the annual Yom Kippur window.
`start` and `end` are inclusive Gregorian dates. The operator
is responsible for setting the window each year (or a process
that wraps multiple years). Setting an empty window
(start > end) clears the window — disposal must then go through
the explicit-authorization path.
Thread-safe; the lock prevents two concurrent set_yom_kippur_window
calls from leaving the window in an inconsistent state.
"""
global _yom_kippur_window
with _window_lock:
if start > end:
_yom_kippur_window = None
else:
_yom_kippur_window = (start, end)
def clear_yom_kippur_window() -> None:
"""Clear the configured window. After this, only explicit
authorization (via authorize_disposal context manager) permits
dispatch."""
global _yom_kippur_window
with _window_lock:
_yom_kippur_window = None
def is_yom_kippur_protocol_active(today: Optional[date] = None) -> bool:
"""True iff the operator has configured a Yom Kippur window AND
today falls within it.
`today` defaults to UTC today; tests can pass an explicit date.
"""
with _window_lock:
window = _yom_kippur_window
if window is None:
return False
if today is None:
today = datetime.now(timezone.utc).date()
start, end = window
return start <= today <= end
# ============================================================
# Per-thread explicit-authorization scope
# ============================================================
#
# threading.local() gives us per-thread isolation: authorize_disposal()
# sets a flag visible only on the current thread; other threads
# cannot see or share it.
_thread_local = threading.local()
def has_explicit_disposal_authorization() -> bool:
"""True iff the current thread is inside an authorize_disposal()
with-block. Per-thread, scoped to the with-block."""
return getattr(_thread_local, "authorized", 0) > 0
@contextlib.contextmanager
def authorize_disposal():
"""Context manager: explicitly authorize one disposal cycle on
the current thread.
Use for event-driven disposals during the year (outside the
Yom Kippur window). The authorization exists ONLY within the
with-block; on exit the flag is decremented and (if reaching 0)
cleared. Reentrant — nesting is permitted (each entry
increments the counter, each exit decrements).
Usage:
from azazel_processor.protocol import authorize_disposal
from azazel_processor.azazel import dispatch_for_disposal
with authorize_disposal():
record = dispatch_for_disposal(input, ...)
"""
current = getattr(_thread_local, "authorized", 0)
_thread_local.authorized = current + 1
try:
yield
finally:
_thread_local.authorized = max(0, _thread_local.authorized - 1)
def assert_protocol_envelope_active() -> None:
"""Gate. Raises ProtocolViolation if NEITHER:
- is_yom_kippur_protocol_active() is True (operator-configured
annual window covers today)
- has_explicit_disposal_authorization() is True (current thread
is inside an authorize_disposal() with-block)
Called by dispatch_for_disposal() at entry. Outside this gate,
invoking the Azazel channel is the avodah zarah shape (Vayikra
17:7); the system refuses.
"""
if is_yom_kippur_protocol_active():
return
if has_explicit_disposal_authorization():
return
raise ProtocolViolation(
"Azazel disposal cannot proceed outside the protocol envelope. "
"Either: (a) configure the Yom Kippur window via "
"set_yom_kippur_window(start, end) and invoke during it, or "
"(b) wrap the dispatch in an authorize_disposal() context "
"manager. Invoking the disposal channel privately is the "
"avodah zarah shape (Vayikra 17:7 — lo yizbechu od et "
"zivchehem la'sa'irim)."
)
+12
View File
@@ -30,6 +30,7 @@ from azazel_processor.azazel import (
) )
from azazel_processor.azazel_record import AzazelRecord from azazel_processor.azazel_record import AzazelRecord
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH, is_eretz_gezerah from azazel_processor.eretz_gezerah import ERETZ_GEZERAH, is_eretz_gezerah
from azazel_processor.protocol import authorize_disposal
from azazel_processor.shaliach import IshItti, ShaliachError from azazel_processor.shaliach import IshItti, ShaliachError
from azazel_processor.stages import AzazelStage from azazel_processor.stages import AzazelStage
from azazel_processor.vidui import confess_empty_vital from azazel_processor.vidui import confess_empty_vital
@@ -51,6 +52,17 @@ def active_handler():
h.tevilah() h.tevilah()
@pytest.fixture(autouse=True)
def _envelope():
"""Open the protocol envelope around every test in this module.
The protocol restriction (property 8) is verified separately in
test_protocol_restriction.py; the orchestrator tests assume the
envelope is open so they can exercise the rest of the state
machine without re-opening per dispatch."""
with authorize_disposal():
yield
def _make_input(case_ref="case-001", payload=None): def _make_input(case_ref="case-001", payload=None):
return AzazelInput( return AzazelInput(
case_ref=case_ref, case_ref=case_ref,
@@ -0,0 +1,262 @@
"""
Tests for the protocol envelope (property 8) — protocol-restricted
access. Vayikra 17:7: lo yizbechu od et zivchehem la'sa'irim — the
same destination outside the Yom Kippur envelope is avodah zarah.
These tests deliberately do NOT auto-open the envelope (unlike
test_azazel.py). They exercise the gate directly: verify dispatch
refuses outside the envelope, accepts inside the Yom Kippur window,
accepts inside an authorize_disposal context, and that the
authorization scope is per-thread and properly nested.
"""
import pathlib
import tempfile
import threading
from datetime import date, timedelta
import pytest
from azazel_processor.azazel import AzazelInput, dispatch_for_disposal
from azazel_processor.protocol import (
ProtocolViolation,
assert_protocol_envelope_active,
authorize_disposal,
clear_yom_kippur_window,
has_explicit_disposal_authorization,
is_yom_kippur_protocol_active,
set_yom_kippur_window,
)
from azazel_processor.shaliach import IshItti
from azazel_processor.vidui import confess_empty_vital
@pytest.fixture(autouse=True)
def _isolate_protocol_state():
"""Ensure each test starts with no Yom Kippur window and no
leftover thread-local authorization. Module-level state in
protocol.py persists across tests if we don't reset."""
clear_yom_kippur_window()
yield
clear_yom_kippur_window()
@pytest.fixture
def tmp_in_flight():
with tempfile.TemporaryDirectory(prefix="azazel-protocol-test-") as d:
yield pathlib.Path(d)
@pytest.fixture
def active_handler():
h = IshItti("aharon")
h.activate()
yield h
if h.is_active():
h.tevilah()
def _input(case_ref="proto-001"):
return AzazelInput(
case_ref=case_ref, payload={}, classification="empty_vital"
)
def _vidui():
return confess_empty_vital("placeholder", confessor="kohen-gadol")
# ============================================================
# Default state: refused
# ============================================================
def test_default_state_refuses_dispatch(active_handler, tmp_in_flight):
"""No Yom Kippur window configured + no authorization → ProtocolViolation."""
with pytest.raises(ProtocolViolation) as exc:
dispatch_for_disposal(
_input(),
handler=active_handler,
confess_with=_vidui(),
authorized_by="kohen-gadol",
in_flight_dir=tmp_in_flight,
)
msg = str(exc.value)
# Error message names both legitimate paths
assert "yom kippur" in msg.lower() or "set_yom_kippur_window" in msg
assert "authorize_disposal" in msg
def test_default_state_assert_raises():
with pytest.raises(ProtocolViolation):
assert_protocol_envelope_active()
def test_default_state_neither_predicate_active():
assert is_yom_kippur_protocol_active() is False
assert has_explicit_disposal_authorization() is False
# ============================================================
# Yom Kippur window
# ============================================================
def test_yom_kippur_window_today_inside_active():
today = date(2026, 9, 21)
set_yom_kippur_window(today - timedelta(days=3), today + timedelta(days=3))
assert is_yom_kippur_protocol_active(today=today) is True
def test_yom_kippur_window_today_outside_inactive():
set_yom_kippur_window(date(2026, 9, 1), date(2026, 9, 5))
assert is_yom_kippur_protocol_active(today=date(2026, 12, 1)) is False
def test_yom_kippur_window_inverted_clears():
"""Setting a window where start > end clears it (no valid window)."""
set_yom_kippur_window(date(2026, 9, 5), date(2026, 9, 1))
assert is_yom_kippur_protocol_active(today=date(2026, 9, 3)) is False
def test_yom_kippur_window_inclusive_at_boundaries():
set_yom_kippur_window(date(2026, 9, 1), date(2026, 9, 5))
assert is_yom_kippur_protocol_active(today=date(2026, 9, 1)) is True
assert is_yom_kippur_protocol_active(today=date(2026, 9, 5)) is True
assert is_yom_kippur_protocol_active(today=date(2026, 8, 31)) is False
assert is_yom_kippur_protocol_active(today=date(2026, 9, 6)) is False
def test_clear_yom_kippur_window_disables():
set_yom_kippur_window(date(2026, 1, 1), date(2026, 12, 31))
assert is_yom_kippur_protocol_active(today=date(2026, 6, 1)) is True
clear_yom_kippur_window()
assert is_yom_kippur_protocol_active(today=date(2026, 6, 1)) is False
# ============================================================
# Explicit-authorization context manager
# ============================================================
def test_authorize_disposal_inside_block_active():
assert has_explicit_disposal_authorization() is False
with authorize_disposal():
assert has_explicit_disposal_authorization() is True
assert has_explicit_disposal_authorization() is False
def test_authorize_disposal_nested_blocks_decrement_correctly():
"""Reentrancy: nested with-blocks each increment, each exit
decrements. Authorization stays active while at least one is open."""
assert has_explicit_disposal_authorization() is False
with authorize_disposal():
assert has_explicit_disposal_authorization() is True
with authorize_disposal():
assert has_explicit_disposal_authorization() is True
# Inner exited; outer still holds
assert has_explicit_disposal_authorization() is True
assert has_explicit_disposal_authorization() is False
def test_authorize_disposal_exception_inside_still_clears():
"""If an exception fires inside the with-block, the authorization
is still cleared on exit (try/finally guarantee)."""
with pytest.raises(RuntimeError):
with authorize_disposal():
assert has_explicit_disposal_authorization() is True
raise RuntimeError("boom")
assert has_explicit_disposal_authorization() is False
def test_authorize_disposal_is_per_thread():
"""One thread's authorization MUST NOT leak to another. Per-thread
isolation is the load-bearing scoping property."""
sibling_authorized = []
def sibling():
sibling_authorized.append(has_explicit_disposal_authorization())
with authorize_disposal():
t = threading.Thread(target=sibling)
t.start()
t.join()
assert sibling_authorized == [False]
# ============================================================
# dispatch_for_disposal honors the gate
# ============================================================
def test_dispatch_proceeds_inside_authorize_disposal(
active_handler, tmp_in_flight
):
with authorize_disposal():
record = dispatch_for_disposal(
_input(),
handler=active_handler,
confess_with=_vidui(),
authorized_by="kohen-gadol",
in_flight_dir=tmp_in_flight,
)
assert record is not None
def test_dispatch_proceeds_inside_yom_kippur_window(
active_handler, tmp_in_flight
):
today = date.today()
set_yom_kippur_window(today - timedelta(days=1), today + timedelta(days=1))
record = dispatch_for_disposal(
_input(),
handler=active_handler,
confess_with=_vidui(),
authorized_by="kohen-gadol",
in_flight_dir=tmp_in_flight,
)
assert record is not None
def test_dispatch_outside_envelope_writes_no_in_flight_file(
active_handler, tmp_in_flight
):
"""Property 8 + 5 interaction: when the gate refuses, NO
side-effect occurs. No checkpoint persisted, no payload held,
no L3 record created."""
try:
dispatch_for_disposal(
_input(case_ref="must-not-persist-001"),
handler=active_handler,
confess_with=_vidui(),
authorized_by="kohen-gadol",
in_flight_dir=tmp_in_flight,
)
except ProtocolViolation:
pass
# No file should have been created
assert not (tmp_in_flight / "must-not-persist-001.json").exists()
# The shaliach should not have held the payload
assert active_handler.held_payload_count() == 0
def test_protocol_violation_raised_before_any_other_check(
active_handler, tmp_in_flight
):
"""The protocol envelope is checked FIRST — even before the role
gate. An inactive handler invoked outside the envelope should
raise ProtocolViolation, not ShaliachError. This ordering matters
because protocol restriction is the avodah-zarah-shape defense:
we refuse on protocol grounds before doing any other validation."""
h = IshItti("aharon")
# Don't activate
with pytest.raises(ProtocolViolation):
dispatch_for_disposal(
_input(),
handler=h,
confess_with=_vidui(),
authorized_by="kohen-gadol",
in_flight_dir=tmp_in_flight,
)