azazel commit 9 — Korban→Azazel intake routing + Chatzotzrot ack (properties 1 + 6)
Pass 4 / Finding F, commit 9 of 10 from BUILD_AZAZEL.md. Closes the
last two property gaps: 1 (parity provisioning, end-to-end) and 6
(out-of-band acknowledgment via Chatzotzrot). The Azazel Processor
is now functionally complete; commit 10 is the closure-and-review
pass with no further code.
PROPERTY 1 — PARITY PROVISIONING, two layers:
LAYER 1 (import-time): external_offering imports azazel_processor
at module load. If the import fails, external_offering refuses to
load with a Torah-anchored ImportError. The two-goat architecture is
SYMMETRIC OR IT IS BROKEN — operators cannot run the processing
channel without the disposal channel being importable. The strongest
formulation: if you cannot dispose, you cannot process.
LAYER 2 (runtime): when zerikah produces no vital content,
consult_external_model now routes the korban to disposal rather than
returning an empty ProcessedKorban with the rejection in `discarded`.
The empty-vital case is named (vidui), routed (eretz_gezerah),
recorded (L3 azazel_records), and acknowledged (chatzotzrot :moadim).
PROPERTY 6 — OUT-OF-BAND ACKNOWLEDGMENT:
The Python orchestrator drops the AzazelRecord JSON in the pending-l3
directory. The Elixir AzazelIngest GenServer reads it, persists to
L3 via record_disposal/1, AND fires Chatzotzrot.signal(:moadim, ...)
with a categorical summary (case_ref + shaliach + categories_filled).
The chatzotzrot is the side channel — distinct from the disposal
channel itself. The orchestrator does NOT know whether the signal
was successfully transmitted; the disposal completes regardless.
The :moadim signal type was the right choice: Yom Kippur completion
semantics. This disposal cycle is closed; the public fact (property
9) is now witnessable via /peah/azazel_log.
PRIVACY BOUNDARY ALSO APPLIES TO THE CHATZOTZROT:
The signal's `reason` field carries a CATEGORICAL summary, not the
vidui items. A test verifies that a known secret string in a vidui
chatot does NOT leak into the chatzotzrot signal — the operator-side
event channel is held to the same privacy boundary as the public
endpoint.
CHANGED:
korban_processor/src/korban_processor/external_offering.py
+ Import azazel_processor at module load (raises ImportError
with Torah-anchored explanation if azazel_processor is not
importable — property 1, layer 1)
+ DEFAULT_AZAZEL_PENDING_L3_DIR
+ ExternalConsultation.disposal_record: Optional[AzazelRecord]
Non-None iff the consultation was routed to disposal
+ _content_addressable_case_ref(content, source_query)
SHA-256-based stable identifier for audit
+ _make_pending_l3_recorder(pending_dir)
Returns a recorder callable that drops the JSON file
atomically (write to .tmp, then os.replace)
+ _route_to_azazel(...) — internal helper running the full
protocol envelope:
- opens authorize_disposal context
- activates a fresh IshItti shaliach
- builds vidui via confess_empty_vital
- dispatches via dispatch_for_disposal
- tevilah's the shaliach
+ consult_external_model:
- new keyword args azazel_kohen_gadol, azazel_shaliach_id,
azazel_pending_l3_dir
- branch on `if not vital.strip()`: route to disposal,
attach record to ExternalConsultation
vav_pipeline/lib/vav_pipeline/azazel_ingest.ex
+ ingest_file/2 fires Chatzotzrot.signal(:moadim, ...) after
successful L3 write, BEFORE shredding the source file. So the
ack comes BEFORE the in-flight artifact disappears — recovery
is possible if either side crashes.
+ Process.whereis(Chatzotzrot)-guarded so tests without the full
supervision tree still work.
+ chatzotzrot_summary/1 builds the categorical summary
(case_ref + shaliach + categories_filled). Privacy boundary:
no vidui items, only counts/category names.
TESTS:
korban_processor/test/test_external_offering.py +4 tests
- empty-vital response routes to azazel:
- processed.vital_content == ""
- disposal_record is an AzazelRecord with kohen + shaliach
- pending-l3 file dropped with correct shape
- processable response does NOT create disposal_record (the
negative case — vital content goes through normally)
- PRIVACY: pending-l3 file does not contain the model's
response text (verified with a known secret_phrase)
- case_ref is content-addressable: same content + same
source_query → same case_ref (audit can recompute)
vav_pipeline/test/azazel_ingest_test.exs +2 tests
- successful ingest fires Chatzotzrot :moadim signal
(subscribe :all, observe — :moadim routes to :all per
chatzotzrot.ex routing table)
- PRIVACY: chatzotzrot signal does NOT include vidui item
content (known secret_phrase verified absent from the signal)
Tests:
- korban_processor: 207 passed, 1 skipped (was 203/1; +4)
- azazel_processor: 125 passed (unchanged)
- vav_pipeline: 363 passed, 0 failures, 35 excluded
(was 361/35; +2 azazel_ingest chatzotzrot tests)
Property coverage after commit 9:
All 12 properties of azazel_engineering_reading.md §14 defended
by code:
1 parity provisioning ✓ (import-time + runtime)
2 blind routing / goral ✓
3 enumerative commit ✓
4 role-separated operator ✓
5 provisioned infrastructure ✓
6 out-of-band acknowledgment ✓
7 authorized routing ✓
8 protocol-restricted access ✓
9 observable disposal ✓ (operator-internal + public)
10 hard termination ✓
11 classification discipline ✓
12 unmanaged destination ✓
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f493b7df65
commit
1a27a283ae
4 files changed
+431
-7
No files matched your search
@@ -160,6 +160,137 @@ def test_system_prompt_is_forwarded():
|
||||
assert result.request.system_prompt == "You are a Torah scholar. Cite verse references."
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Empty-vital → Azazel disposal routing (commit 9, properties 1 + 6)
|
||||
# ============================================================
|
||||
# Property 1 at runtime: an empty-vital korban is routed to disposal,
|
||||
# not silently dropped. The disposal record IS the permanent
|
||||
# acknowledgment that the system received this input and could not
|
||||
# metabolize it.
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from azazel_processor import AzazelRecord
|
||||
|
||||
|
||||
def _empty_vital_response():
|
||||
"""A model response that contains no extractable Torah content
|
||||
(no Torah refs, no Hebrew, no insight markers, no roots).
|
||||
zerikah will return empty vital for this."""
|
||||
return "ok thanks. let me know if you want me to do anything else."
|
||||
|
||||
|
||||
def test_empty_vital_korban_is_routed_to_azazel():
|
||||
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
|
||||
pending = pathlib.Path(d)
|
||||
|
||||
result = consult_external_model(
|
||||
query="anything",
|
||||
source_query="empty_vital_test_001",
|
||||
caller=_fake_caller(_empty_vital_response()),
|
||||
azazel_kohen_gadol="kohen-gadol-test",
|
||||
azazel_shaliach_id="ish-itti-test",
|
||||
azazel_pending_l3_dir=pending,
|
||||
)
|
||||
|
||||
# The processed korban is empty (zerikah returned no vital)
|
||||
assert result.processed.vital_content == ""
|
||||
|
||||
# The disposal_record field is now populated
|
||||
assert result.disposal_record is not None
|
||||
assert isinstance(result.disposal_record, AzazelRecord)
|
||||
assert result.disposal_record.shaliach == "ish-itti-test"
|
||||
assert result.disposal_record.authorized_by == "kohen-gadol-test"
|
||||
assert result.disposal_record.triggering_call == (
|
||||
"external_offering.consult_external_model"
|
||||
)
|
||||
|
||||
# A pending-l3 file was dropped for AzazelIngest to pick up
|
||||
files = list(pending.glob("*.json"))
|
||||
assert len(files) == 1
|
||||
|
||||
body = json.loads(files[0].read_text())
|
||||
assert body["case_ref"] == result.disposal_record.case_ref
|
||||
assert body["routing"] == "eretz_gezerah"
|
||||
assert body["shaliach"] == "ish-itti-test"
|
||||
# The vidui is on the pending file (operator-internal channel)
|
||||
assert body["vidui"]["confessor"] == "kohen-gadol-test"
|
||||
assert body["vidui"]["chatot"] # non-empty
|
||||
|
||||
|
||||
def test_processable_response_does_not_create_disposal_record():
|
||||
"""The companion to the above: an offering with extractable Torah
|
||||
content goes through the processing channel and the disposal_record
|
||||
field stays None."""
|
||||
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
|
||||
result = consult_external_model(
|
||||
query="explain bereishit 1:1",
|
||||
source_query="processable_test_001",
|
||||
caller=_fake_caller(
|
||||
"Bereishit 1:1 — the shoresh ב-ר-א means to create. The verse "
|
||||
"indicates beginnings."
|
||||
),
|
||||
azazel_pending_l3_dir=pathlib.Path(d),
|
||||
)
|
||||
|
||||
assert result.disposal_record is None
|
||||
assert result.processed.vital_content != ""
|
||||
# No file was dropped — nothing for AzazelIngest to pick up
|
||||
assert list(pathlib.Path(d).glob("*.json")) == []
|
||||
|
||||
|
||||
def test_pending_l3_file_does_not_contain_payload():
|
||||
"""Property 10 follow-through: the file the orchestrator drops
|
||||
for the Elixir bridge contains the vidui (what was disposed) but
|
||||
not the disposed thing itself. The model's actual response text
|
||||
must not appear in the pending-l3 file."""
|
||||
secret_phrase = "this-string-must-not-leak-to-the-pending-l3-file"
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
|
||||
pending = pathlib.Path(d)
|
||||
consult_external_model(
|
||||
query="any",
|
||||
source_query="leak_check_001",
|
||||
caller=_fake_caller(secret_phrase), # response is just the secret
|
||||
azazel_pending_l3_dir=pending,
|
||||
)
|
||||
|
||||
files = list(pending.glob("*.json"))
|
||||
assert len(files) == 1
|
||||
body_text = files[0].read_text()
|
||||
assert secret_phrase not in body_text
|
||||
|
||||
|
||||
def test_case_ref_is_content_addressable():
|
||||
"""Audit can verify the routing was for a specific input by
|
||||
recomputing the case_ref. Property 7's audit-trail half — the
|
||||
routing decision is owned and traceable."""
|
||||
response = _empty_vital_response()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
|
||||
pending = pathlib.Path(d)
|
||||
result1 = consult_external_model(
|
||||
query="any",
|
||||
source_query="audit_test_001",
|
||||
caller=_fake_caller(response),
|
||||
azazel_pending_l3_dir=pending,
|
||||
)
|
||||
|
||||
# Same content + same source_query → same case_ref
|
||||
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
|
||||
pending = pathlib.Path(d)
|
||||
result2 = consult_external_model(
|
||||
query="different-query", # query is not in the case_ref
|
||||
source_query="audit_test_001",
|
||||
caller=_fake_caller(response),
|
||||
azazel_pending_l3_dir=pending,
|
||||
)
|
||||
|
||||
assert result1.disposal_record.case_ref == result2.disposal_record.case_ref
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Live API integration test — opt-in only
|
||||
# ============================================================
|
||||
|
||||
Reference in new issue
Block a user