Compare commits

...
Author SHA1 Message Date
Scott KarpandClaude Fable 5.1 d3df10dafd talmudic-engine: take Debian's libbrotli1 fix for CVE-2026-111497
The Brotli decoder out-of-bounds write (Brotli 1.0.3–1.2.0, fixed
1.2.1) was checked against every deployed image on 2026-10-25. No
decoder is on any attacker-reachable path: Gitea's Go binary has no
Brotli, vav and korban carry no libbrotli and no Python Brotli module,
ingress-nginx has the module unloaded and only ever encodes. Three
dormant copies remain: Gitea's Alpine brotli-libs 1.2.0 (no rebuilt
image on Docker Hub yet), ingress-nginx's brotli-libs 1.1.0, and this
image's libbrotli1 1.0.9-2 from the swipl:9.2.2 base, linked only by
libcurl3-gnutls which nothing here calls.

This one is ours to close. bookworm-security has 1.0.9-2+deb12u1;
the runtime stage now upgrades that single package and nothing else,
so the SWI-Prolog 9.2.2 pin (Construction §1.1) stands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 01:56:58 +00:00
Scott KarpandClaude Fable 5.1 ba3ff4a2f4 sugya, step 4: the hard matter goes up with its dissent
כי יפלא ממך דבר למשפט ... ובין נגע לנגע דברי ריבת בשעריך וקמת ועלית
(Devarim 17:8, read from L1). A mark the inspection cannot settle, a
dispute in your gates — it goes up. specs/sugya.md §V step 4: a
bedikah's applied dissents become the minority the clerk stores when
the case goes up.

POST /chatzer/sanhedrin takes `bedikah_ref`. Only this key's own
bedikah goes up (404 alike for another's or none). The case is that
verdict; the roots are the verses the output cited, read from L1 — the
output's text is not re-read, the record never kept it; the sugya is
rebuilt from the record (Sugya.from_record/1: forms found but not fired
are the noted ones; a fired forged fringe stood on one witness); the
applied dissents (Sugya.dissents_applied/1) travel in the case context
and down into L3: beside the panel's minority, or AS the minority when
the panel was unanimous — "the panel was unanimous | carried up from
the bedikah (Devarim 17:8): single_witness_noted (Devarim 19:15) for
apology_without_mechanism", grounding achorai. The response's grounding
carries the bedikah, its carried dissent and its sugya. Tamid §1.1: the
request field. Doorpost pin → 7eac65cf….

Tests: from_record/dissents_applied (noted form; forged fringe; clean);
L3 — a noted bedikah goes up under its key: roots from Devarim 6:4 as
read in L1 (ש-מ-ע among them), panel 3, the carried dissent in the
response, a minority recorded though the panel was unanimous; another
key's bedikah 404. Elixir 669/0; L3 8/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 01:44:40 +00:00
6 changed files with 247 additions and 11 deletions

No files matched your search

+13
View File
@@ -21,6 +21,19 @@ RUN swipl -g "consult('src/main'),halt(0)" -t "halt(1)"
# === RUNTIME STAGE ===
FROM --platform=linux/amd64 swipl:9.2.2
# CVE-2026-111497 (Brotli decoder out-of-bounds write, October 2026).
# The swipl:9.2.2 base carries Debian bookworm's libbrotli1 1.0.9-2,
# pulled in by libcurl3-gnutls. Nothing in this image decodes Brotli:
# swipl does not link it, there is no curl binary, and the engine reads
# JSON from vav over a Unix socket. The copy was dormant, not a path.
# Debian fixed it in 1.0.9-2+deb12u1 (bookworm-security); take that one
# package without moving the pinned base. The pin on SWI-Prolog 9.2.2
# (Construction §1.1) is the point of the base image, so this is
# --only-upgrade of the one library, not a general upgrade.
RUN apt-get update \
&& apt-get install -y --no-install-recommends --only-upgrade libbrotli1 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash bikoret
USER bikoret
+6 -1
View File
@@ -591,7 +591,12 @@ Request:
{"position": "...", "evidence": ["..."]},
{"position": "...", "evidence": ["..."]}
],
"complexity_hint": "simple" | "monetary" | "capital" | "novel"
"complexity_hint": "simple" | "monetary" | "capital" | "novel",
"bedikah_ref": "bedikah-…" # Optional — the hard matter goes up (Devarim 17:8,
# sugya.md step 4): the case is that verdict, the roots
# are the verses it cited (L1), and its sugya's applied
# dissents travel with it and come down as the minority
# the clerk keeps. Only this key's own bedikah; else 404.
}
Response:
@@ -1172,6 +1172,39 @@ defmodule VavPipeline.BigdeiKodesh.Router do
defp process_sanhedrin(conn) do
params = conn.body_params
# The hard matter goes up with its dissent (Devarim 17:8 — bein nega
# lanega, divrei rivot bish'arecha; sugya.md step 4): a bedikah_ref
# names a verdict given at the gate; the case is that verdict, the
# roots are the verses it cited, and the sugya's applied dissents go
# up with it and come down as the minority the clerk keeps.
case params["bedikah_ref"] do
ref when is_binary(ref) and ref != "" -> process_sanhedrin_bedikah(conn, ref, params)
_ -> process_sanhedrin_case(conn, params)
end
end
defp process_sanhedrin_bedikah(conn, ref, params) do
cond do
Process.whereis(VavPipeline.Repo) == nil ->
send_error(conn, 503, "L3_UNAVAILABLE", %{issue: "A bedikah goes up from L3, and L3 is not reachable", correction: "Retry", resubmit: true})
true ->
key_id = caller_key_id(conn)
case VavPipeline.L3.Store.get_bedikah(ref) do
%{key_id: ^key_id} = rec when is_binary(key_id) ->
sugya = VavPipeline.Sugya.for_verdict(VavPipeline.Sugya.from_record(rec))
carried = VavPipeline.Sugya.dissents_applied(sugya)
question = params["case"] || "Does the verdict #{rec.verdict} of #{ref} stand? (#{Enum.join(rec.violations, "; ")})"
process_sanhedrin_case(conn, Map.put(params, "case", question), %{bedikah: rec, sugya: sugya, carried: carried})
_ ->
send_error(conn, 404, "NOT_FOUND", %{issue: "No bedikah #{ref} in this key's ledger", correction: "Only an examination brought under this key goes up", resubmit: false})
end
end
end
defp process_sanhedrin_case(conn, params, up \\ nil) do
case_text = params["case"]
if is_nil(case_text) or case_text == "" do
@@ -1196,8 +1229,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
# engraved in L3; the answer takes the Tamid's shape.
with {:ok, sides} <- sanhedrin_sides(params["sides"]),
{:ok, complexity} <- sanhedrin_complexity(params["complexity_hint"]),
{:ok, scope, resolved_by} <- sanhedrin_scope(case_text),
{:ok, resolved} <- sanhedrin_resolve(case_text, scope, resolved_by),
{:ok, scope, resolved_by} <- sanhedrin_scope(if(up, do: Enum.join(up.bedikah.references, " "), else: case_text)),
{:ok, resolved} <- (if up, do: sanhedrin_resolve_bedikah(up.bedikah, scope, resolved_by), else: sanhedrin_resolve(case_text, scope, resolved_by)),
:ok <- sanhedrin_gather_panel() do
case_ref = generate_id()
@@ -1209,12 +1242,15 @@ defmodule VavPipeline.BigdeiKodesh.Router do
sides: sides,
roots: resolved.roots,
torah_refs: [resolved.torah_ref],
authority_source: :torah
authority_source: :torah,
# the dissent travels with the case (Devarim 17:8)
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, violations: up.bedikah.violations}, else: nil),
dissent: (if up, do: up.carried, else: [])
},
requested_by: "chatzer"
}) do
{:ok, decision} ->
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved)
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved, (if up, do: up.carried, else: []))
send_json(conn, 200, %{
case_ref: decision.case_ref,
@@ -1228,7 +1264,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
grounding: %{
scope: scope_report(scope, resolved_by),
roots_consulted: format_roots(resolved.roots_full),
recorded: recorded
recorded: recorded,
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, dissent_carried: up.carried, sugya: up.sugya}, else: nil)
},
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
disclosure: disclosure()
@@ -1369,6 +1406,38 @@ defmodule VavPipeline.BigdeiKodesh.Router do
end
end
# A bedikah that goes up is grounded in the verses it cited: each
# reference read from L1, its words' roots as the engine names them.
# The output's own text is not re-read — the record never kept it.
defp sanhedrin_resolve_bedikah(rec, scope, _resolved_by) do
refs =
(rec.references || [])
|> Enum.flat_map(fn r ->
case Regex.run(~r/^(\w+)\s+(\d+):(\d+)$/, String.trim(to_string(r))) do
[_, book, ch, vs] -> [{String.downcase(book), String.to_integer(ch), String.to_integer(vs)}]
_ -> []
end
end)
roots_full =
refs
|> Enum.flat_map(fn {b, c, v} ->
case VavPipeline.L1Reader.read_verse(b, c, v) do
{:ok, verse} -> verse.words |> Enum.filter(& &1.shoresh) |> Enum.map(&%{shoresh: &1.shoresh, shoresh_letters: [], positions: [], gematria: 0, binyan: nil, word: &1.consonants})
_ -> []
end
end)
|> Enum.uniq_by(& &1.shoresh)
torah_ref =
case refs do
[{b, c, v} | _] -> {String.to_atom(b), c, v}
[] -> {String.to_atom(scope.book), scope.chapter, scope.verse}
end
{:ok, %{roots: roots_full |> Enum.map(&VavPipeline.BereishitCycle.engine_root_name/1) |> Enum.reject(&is_nil/1), roots_full: roots_full, torah_ref: torah_ref, read_from: :references}}
end
# One panel per case from the day's manna (Vayakhel §3.7 — the ledger
# provisions sanhedrin_panels_max). The provision is loaded first, as
# Gate.gather_call does (idempotent per date). Without a ledger
@@ -1418,7 +1487,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
# against, processing_component "sanhedrin"), the minority beside it
# when it exists. Never turns a served decision into a 500: the record
# failing is logged and reported in the reply as `recorded`.
defp sanhedrin_record_l3(case_ref, case_text, decision, resolved) do
defp sanhedrin_record_l3(case_ref, case_text, decision, resolved, carried) do
if Process.whereis(VavPipeline.Repo) do
maj = decision.majority
min = decision.minority
@@ -1450,12 +1519,25 @@ defmodule VavPipeline.BigdeiKodesh.Router do
{:ok, interp} ->
Logger.info("L3: interpretation #{interp.id} recorded for sanhedrin #{case_ref} (#{decision.outcome})")
# The dissent that came up with the case is kept beside the
# panel's own minority — or as the minority when the panel had
# none (Eduyot 1:5–6: to be relied on later, and to be located).
carried_text =
Enum.map_join(carried, "; ", fn
%{reading: r, verse: v, forms: fs} -> "#{r} (#{v})" <> if(fs == [], do: "", else: " for " <> Enum.join(fs, ", "))
%{exception: c, verse: v} -> "exception #{c} (#{v})"
end)
carried_refs = Enum.map(carried, & &1.verse)
minority_recorded =
if Map.get(min, :vote_count, 0) > 0 do
if Map.get(min, :vote_count, 0) > 0 or carried != [] do
case VavPipeline.L3.Store.record_minority(%{
interpretation_id: interp.id,
dissenting_reasoning: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}",
torah_references: Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1),
dissenting_reasoning:
(if Map.get(min, :vote_count, 0) > 0, do: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}", else: "the panel was unanimous") <>
if(carried == [], do: "", else: " | carried up from the bedikah (Devarim 17:8): #{carried_text}"),
torah_references: Enum.uniq(Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1) ++ carried_refs),
grounding_type: "achorai"
}) do
{:ok, _} ->
+1 -1
View File
@@ -182,7 +182,7 @@ defmodule VavPipeline.Doorpost do
# changes; the update is a deliberate operator commit. A bare
# hash-update commit (without a corresponding spec change in the
# same commit) is itself a drift signal.
@expected_hash "658005c9323391e8c198c0cf23f91af0a2fdbdb20cac204eb60b58ac0c0bb318"
@expected_hash "7eac65cfadccc37e600a779fc89acef0a24a9eac35e5ab18e8ec845cc1f104c2"
@doc "Return the list of files that constitute the category-grade specification."
@spec files() :: [String.t()]
+39
View File
@@ -261,6 +261,45 @@ defmodule VavPipeline.Sugya do
end)
end
@doc """
A stored bedikah (L3 record) in the shape `for_verdict/1` reads: its
violations, its claims, the forms found — those with no violation of
their form were NOTED — and whether a forged fringe stood on one witness.
The hard matter goes up with its dissent (Devarim 17:8, sugya.md step 4).
"""
@spec from_record(struct() | map()) :: map()
def from_record(rec) do
violations = Map.get(rec, :violations) || []
fired_forms =
violations
|> Enum.flat_map(fn v -> case Regex.run(~r/form\((\w+)\)/, to_string(v)) do [_, f] -> [f]; _ -> [] end end)
|> MapSet.new()
found = (Map.get(rec, :frames) || []) |> Enum.map(&to_string(&1["form"] || Map.get(&1, :form)))
noted = found |> Enum.reject(&MapSet.member?(fired_forms, &1)) |> Enum.uniq()
forms = for f <- fired_forms, into: %{}, do: {String.to_atom(f), %{stands_on: if(f == "forged_fringe", do: :forgery, else: :sentences)}}
claims = Map.get(rec, :claims) || []
%{
verdict: Map.get(rec, :verdict),
violations: violations,
examination: %{"claims" => Enum.map(claims, fn c -> %{"hedged" => c["hedged"] == true} end)},
checks: %{
frames: %{noted: Map.new(noted, &{String.to_atom(&1), %{witnesses: 1}}), forms: forms},
grounding: %{assessed: claims != [] and (Map.get(rec, :sources) || []) != []}
}
}
end
@doc "The dissents and exceptions a sugya shows applied: `[%{rule, reading | condition, verse, form?}]`."
@spec dissents_applied(map()) :: [map()]
def dissents_applied(sugya) when is_map(sugya) do
Enum.flat_map(sugya, fn {rule, e} ->
Enum.map(Enum.filter(e.dissent, & &1.applied), &%{rule: rule, reading: &1.reading, verse: &1.verse, forms: Map.get(&1, :forms, [])}) ++
Enum.map(Enum.filter(e.exception, & &1.applied), &%{rule: rule, exception: &1.condition, verse: &1.verse})
end)
end
defp rule_of(violation) do
case Regex.run(~r/^halt\(\s*(\w+)\s*,/, to_string(violation)) do
[_, rule] -> [rule]
+97
View File
@@ -0,0 +1,97 @@
defmodule VavPipeline.SugyaUpTest do
@moduledoc "sugya.md step 4 — the hard matter goes up with its dissent (Devarim 17:8)."
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.{Sugya, BigdeiKodesh.Router, L3.Store}
@opts Router.init([])
describe "from_record/1 and dissents_applied/1 (no database)" do
test "a stored verdict with a form found but not fired is a noted form, and its dissent applied" do
rec = %{verdict: "tov", violations: [], frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], claims: [], sources: [], references: []}
s = Sugya.for_verdict(Sugya.from_record(rec))
assert s["lo_taaseh_lekha"].noted == ["apology_without_mechanism"]
assert [%{rule: "lo_taaseh_lekha", reading: "single_witness_noted", verse: "Devarim 19:15", forms: ["apology_without_mechanism"]}] = Sugya.dissents_applied(s)
end
test "a stored forged fringe carries the mark exception, applied" do
rec = %{verdict: "lo_tov", violations: ["halt(lo_taaseh_lekha,action(frame,by(claude),form(forged_fringe)),forged_fringe,'x')"],
frames: [%{"form" => "forged_fringe", "sentence" => "— bikoret bedikah-dead: tov"}], claims: [], sources: [], references: []}
s = Sugya.for_verdict(Sugya.from_record(rec))
assert s["lo_taaseh_lekha"].noted == []
assert [%{exception: "single_witness(forged_fringe)", verse: "Vayikra 22:20"}] = Sugya.dissents_applied(s)
end
test "a clean record carries nothing" do
assert Sugya.dissents_applied(Sugya.for_verdict(Sugya.from_record(%{verdict: "tov", violations: [], frames: [], claims: [], sources: [], references: []}))) == []
end
end
describe "a bedikah goes up to the Sanhedrin (L3)" do
@describetag :l3
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, {:shared, self()})
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, :manual) end)
VavPipeline.MannaLedger.reset()
on_exit(fn -> VavPipeline.MannaLedger.reset() end)
today = Date.utc_today() |> Date.to_iso8601()
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
provision = %{"date" => today, "provision" => %{"compute_units" => 50, "memory_mb" => 512, "api_calls_external" => 10, "storage_delta_mb" => 2, "sanhedrin_panels_max" => 3},
"type" => "regular", "expires_at" => expires, "written_by" => "sugya_up_test", "written_at" => DateTime.utc_now() |> DateTime.to_iso8601()}
path = VavPipeline.Manna.provision_file()
File.mkdir_p!(Path.dirname(path))
File.write!(path, Jason.encode!(provision))
on_exit(fn -> File.rm(path) end)
Application.put_env(:sanhedrin, :consult, {__MODULE__, :fake_consult})
on_exit(fn -> Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult}) end)
:ok
end
def fake_consult(_step, _ref, _roots, _q, _ctx), do: {:ok, %{status: "resolved", text: "the reading", verses: [{:devarim, 6, 4}]}}
defp record!(ref, key_id, extra \\ %{}) do
{:ok, rec} =
Store.record_bedikah(Map.merge(%{bedikah_ref: ref, provenance: "claude", query: "q", output_sha256: String.duplicate("ab", 32), output_length: 9,
vital_content: "", verdict: "tov", examined_at: DateTime.utc_now() |> DateTime.truncate(:second), key_id: key_id,
frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], references: ["Devarim 6:4"]}, extra))
rec
end
defp post(body, token) do
conn(:post, "/chatzer/sanhedrin", Jason.encode!(body))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer #{token}")
|> Router.call(@opts)
end
test "the case is the verdict, the roots are its cited verses, the dissent goes up and comes down as the minority" do
mine = VavPipeline.Ledger.key_id("chatzer-test-token")
record!("bedikah-up00000000000001", mine)
before_min = Store.count_minority_opinions()
conn = post(%{bedikah_ref: "bedikah-up00000000000001", complexity_hint: "monetary"}, "chatzer-test-token")
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
g = body["grounding"]
assert g["bedikah"]["ref"] == "bedikah-up00000000000001"
assert [%{"reading" => "single_witness_noted", "verse" => "Devarim 19:15"}] = g["bedikah"]["dissent_carried"]
# the roots came from Devarim 6:4 as read in L1 (shin-mem-ayin among them)
assert "ש-מ-ע" in g["roots_consulted"]
assert body["deliberation"]["panel_size"] == 3
# a unanimous panel, and still a minority recorded: the carried dissent
assert g["recorded"]["minority"] == "recorded"
assert Store.count_minority_opinions() == before_min + 1
end
test "another key's bedikah does not go up: 404, the same as none" do
record!("bedikah-up00000000000002", VavPipeline.Ledger.key_id("someone-else"))
assert post(%{bedikah_ref: "bedikah-up00000000000002"}, "chatzer-test-token").status == 404
assert post(%{bedikah_ref: "bedikah-up00000000000003"}, "chatzer-test-token").status == 404
end
end
end