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:
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
|
||||
the same pool, with assignment blind to the operator.
|
||||
|
||||
Build state: commit 5 — orchestrator state machine added. See
|
||||
BUILD_AZAZEL.md for the full 10-commit staging plan and the
|
||||
12-property → code mapping from azazel_engineering_reading.md §14.
|
||||
Build state: commit 6 — protocol envelope added. The Python side of
|
||||
the build is now feature-complete; commits 7-9 wire the cross-language
|
||||
integrations (L3, observability endpoint, intake-side routing).
|
||||
|
||||
Properties defended so far:
|
||||
- Property 2 (blind routing / goral) — cast_lot
|
||||
@@ -17,6 +17,8 @@ Properties defended so far:
|
||||
- Property 5 (provisioned infrastructure) — checkpoint persistence
|
||||
- Property 7 (authorized routing) — AzazelRecord owns
|
||||
the routing decision
|
||||
- Property 8 (protocol-restricted access) — envelope gate at
|
||||
dispatch entry
|
||||
- Property 10 (hard termination) — DESTROYED checkpoint
|
||||
+ record without payload
|
||||
- Property 11 (classification discipline) — AzazelStage*
|
||||
@@ -34,9 +36,13 @@ Public surface:
|
||||
GoralError — blind routing
|
||||
AzazelInput, AzazelRecord — orchestrator data types
|
||||
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:
|
||||
protocol envelope (commit 6 — protocol-restricted access)
|
||||
L3 azazel_records integration (commit 7)
|
||||
/peah/azazel_log endpoint (commit 8)
|
||||
Korban→Azazel routing + chatzotzrot (commit 9)
|
||||
@@ -56,6 +62,15 @@ from azazel_processor.goral import (
|
||||
cast_lot,
|
||||
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 (
|
||||
IshItti,
|
||||
ShaliachError,
|
||||
@@ -97,4 +112,11 @@ __all__ = [
|
||||
"dispatch_for_disposal",
|
||||
"list_in_flight",
|
||||
"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.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.stages import AzazelStage, AzazelStaged
|
||||
from azazel_processor.vidui import Vidui
|
||||
@@ -203,6 +204,13 @@ def dispatch_for_disposal(
|
||||
ValueError: case_ref is not filename-safe, or other input
|
||||
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
|
||||
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)."
|
||||
)
|
||||
@@ -30,6 +30,7 @@ from azazel_processor.azazel import (
|
||||
)
|
||||
from azazel_processor.azazel_record import AzazelRecord
|
||||
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.stages import AzazelStage
|
||||
from azazel_processor.vidui import confess_empty_vital
|
||||
@@ -51,6 +52,17 @@ def active_handler():
|
||||
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):
|
||||
return AzazelInput(
|
||||
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,
|
||||
)
|
||||
Reference in new issue
Block a user