Commit Graph
2 Commits
Author SHA1 Message Date
Scott KarpandClaude Opus 4.7 6eb555a5cf 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>
2026-04-28 08:06:27 -04:00
Scott KarpandClaude Opus 4.7 c3267c59bf azazel_processor: commit 5 — orchestrator state machine (properties 5, 7, 10)
Pass 4 / Finding F, commit 5 of 10 from BUILD_AZAZEL.md. Largest
commit so far; pulls together vidui + shaliach + goral + stages and
adds the state machine + in-flight persistence.

THE STATE MACHINE (six checkpoints, persisted):

  STAGED       → AzazelStaged envelope tagged; payload held by shaliach
  CONFESSED    → vidui carried into the staged value
  ROUTED       → routing committed (always ERETZ_GEZERAH); authorized_by
                 + shaliach attribution recorded
  DESTROYED    → payload references cleared from shaliach;
                 gc.collect() called to release; payload becomes
                 unreachable from any orchestrator-visible code path
  ACKNOWLEDGED → out-of-band ack callback invoked (commit 9 wires
                 Chatzotzrot.signal(:moadim) here)
  RECORDED     → AzazelRecord persisted via callback (commit 7 wires
                 the cross-language L3 file-watcher bridge here)

After RECORDED completes, the in-flight checkpoint file is shredded —
the disposal is now permanent in L3, the in-flight artifact must not
linger.

PROPERTY COVERAGE:

  Property 5 (provisioned infrastructure / sukkot):
    Each checkpoint persists state to <in_flight_dir>/<case_ref>.json
    via write-then-rename (atomic against crash mid-write). list_in_flight
    returns partial disposals so resume tooling can pick them up.
    The Mishkan analog: the sukkot along the wilderness route where
    the ish itti can rest if the disposal halts.

  Property 7 (authorized routing):
    AzazelRecord.authorized_by carries the kohen_gadol identifier;
    AzazelRecord.shaliach carries the ish itti identifier. The
    routing destination is the ERETZ_GEZERAH sentinel (property 12).
    The routing decision is OWNED and traceable; the destination
    is not.

  Property 10 (hard termination):
    The DESTROYED checkpoint clears handler._payload_refs and calls
    gc.collect(). The AzazelRecord contains the vidui — what was
    disposed — but NOT the disposed payload itself. The to_storage_dict
    serialization explicitly excludes payload-shaped fields.

CREATED:

  src/azazel_processor/azazel_record.py    ~95 lines
    AzazelRecord                           — frozen dataclass; fields:
      case_ref, vidui, routing, authorized_by, shaliach,
      triggering_call, goral_outcome_id, disposed_at
      .to_storage_dict() — JSON-safe serialization for L3 + in-flight

  src/azazel_processor/azazel.py           ~225 lines
    AzazelInput                            — what is being disposed
    dispatch_for_disposal(input, *, handler, confess_with,
                          authorized_by, in_flight_dir,
                          acknowledger, recorder, goral_outcome_id)
                                           — the single legitimate
                                             entry point; walks the
                                             six checkpoints
    list_in_flight(in_flight_dir)          — surfaces partial disposals
    DEFAULT_IN_FLIGHT_DIR                  — /tmp/shorashim/azazel/in_flight

  test/test_azazel.py                      21 tests covering:
    HAPPY PATH:
    - dispatch returns AzazelRecord
    - record carries authorization + shaliach attribution
    - record routes to ERETZ_GEZERAH
    - record carries the full vidui

    PROPERTY 10 CI INVARIANTS:
    - record does not carry payload (by attribute name)
    - serialized record does not contain the payload's marker string
    - STRUCTURAL: AzazelRecord dataclass field set has no
      payload-shaped names (catches future regression)
    - STRUCTURAL: no recover/undispose/restore/rehydrate/retrieve
      function in azazel_processor public surface (catches
      future regression)
    - payload references cleared from shaliach at DESTROYED

    PROPERTY 5 CHECKPOINT TESTS:
    - in-flight file shredded after RECORDED
    - checkpoints persist incrementally (verified by inspecting the
      file at ack/rec callback fire-time)
    - list_in_flight returns partial disposals
    - list_in_flight empty when no disposals
    - persistence is atomic (file is valid JSON at every visit)

    ROLE GATE (property 4 still enforced):
    - dispatch refuses inactive handler
    - dispatch refuses non-IshItti

    FILENAME SAFETY:
    - case_ref with slash → ValueError
    - case_ref with NUL → ValueError

    AzazelRecord VALIDATION:
    - authorized_by required
    - shaliach required
    - to_storage_dict omits payload
    - frozen (cannot mutate)

DEFAULTS:

  acknowledger and recorder default to no-op callables. This is
  deliberate for commit 5 — the state machine works end-to-end without
  external side effects, and commits 7 (L3) and 9 (chatzotzrot) plug
  the real handlers into the same callback slots. Tests can supply
  their own to inspect mid-route state.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:02:09 -04:00