gate: Shorashim between a caller and a model, with the fringe and rebuke once (bishe'arecha step 4)

Devarim 6:9 / 11:20 — on your gates. POST /gate/anthropic/v1/messages and
POST /gate/openai/v1/chat/completions take the model's own request shape
under the caller's own upstream key (x-api-key, or Authorization with the
courtyard key in X-Shorashim-Key), forward it unchanged (OTP inets/ssl,
verify_peer; the Korban socket is serial, so the upstream call is made
in the Vav Pipeline and the reply enters as korban like every bedikah),
examine the reply with the request in hand — sources = the system prompt
and the user turns, query = the last user turn; the model's prior turns
are not sources — and deliver it with its fringe (Bamidbar 15:39): a
trailer on the last text block naming ref, verdict, the rules with their
verses and sentences; X-Shorashim-Bedikah / X-Shorashim-Verdict; a
`shorashim` object. A non-200 passes through as it came; a reply with
no text is not delivered; stream is refused.

Rebuke once (Vayikra 19:17; X-Shorashim-Rebuke: once): a lo_tov reply is
answered once more with the finding; the second reply is examined like
the first, its record carries rebukes: <first ref> (migration
20261007000002), and it is delivered marked with both. Never a third.

Manna: one api_calls_external per upstream call, gathered before any
call. Stateless: no conversation, no key kept; the log is the path class.
Gate.bearer accepts X-Shorashim-Key. Construction §3.10, Tamid §1.1,
KOHEN "Standing at the gate", bishearecha §I/§III/§VII marked built.
Elixir 592/0 (+14 gate tests incl. a real httpc round trip to a local
model double), L3 9/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-07 11:38:32 +00:00
1 parent 47025fbfeb
commit 5c63273dba
16 files changed
+1082 -15

No files matched your search

+9 -5
View File
@@ -128,6 +128,8 @@ defmodule VavPipeline.Bedikah do
{:ok, result()} | {:waiting, atom()} | {:error, atom(), String.t()}
def examine(output, provenance, query \\ "", opts \\ []) do
sources = Keyword.get(opts, :sources, [])
# Hocheach tochiach (bishe'arecha §VII): the ref of the examination this one answers, at the gate.
rebukes = Keyword.get(opts, :rebukes)
examiner = Keyword.get(opts, :examiner, &KorbanProcessorSocket.examine/4)
halt_checker = Keyword.get(opts, :halt_checker, &TalmudicEngineSocket.halt_check_term/1)
@@ -136,12 +138,12 @@ defmodule VavPipeline.Bedikah do
# D'var yom b'yomo: an examination is a cycle's work and eats a unit.
case Manna.gather_for_cycle() do
:ok -> run(output, provenance, query, sources, examiner, halt_checker, interpretation_checker)
:ok -> run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker)
{:waiting, reason} -> {:waiting, reason}
end
end
defp run(output, provenance, query, sources, examiner, halt_checker, interpretation_checker) do
defp run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker) do
bedikah_ref = "bedikah-" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower))
with {:ok, examination} <- examiner.(output, provenance, query, sources) do
@@ -151,7 +153,7 @@ defmodule VavPipeline.Bedikah do
(examination["frames"] || []) == [] do
# Nothing the offering protocol could extract, nothing claimed,
# nothing enacted: there is nothing to examine.
finish(bedikah_ref, output, provenance, query, sources, examination, [], %{}, [], :empty)
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, [], %{}, [], :empty)
else
trace = coupling_trace(examination)
@@ -166,7 +168,7 @@ defmodule VavPipeline.Bedikah do
checks = Map.merge(%{authority: authority, grounding: grounding, actions: actions, frames: frames}, structure)
violations = prior ++ frames.violations
verdict = if violations == [], do: :tov, else: :lo_tov
finish(bedikah_ref, output, provenance, query, sources, examination, trace, checks, violations, verdict)
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict)
else
{:error, :talmudic, reason} -> {:error, :talmudic_unavailable, reason}
end
@@ -521,12 +523,13 @@ defmodule VavPipeline.Bedikah do
end
end
defp finish(bedikah_ref, output, provenance, query, sources, examination, trace, checks, violations, verdict) do
defp finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict) do
result = %{
bedikah_ref: bedikah_ref,
provenance: provenance,
query: query,
sources: source_digests(sources),
rebukes: rebukes,
verdict: verdict,
violations: violations,
checks: checks,
@@ -563,6 +566,7 @@ defmodule VavPipeline.Bedikah do
excluded_adversarial: result.examination["excluded_adversarial"] || []
},
frames: result.examination["frames"] || [],
rebukes: result.rebukes,
coupling_trace: result.coupling_trace,
violations: result.violations,
verdict: Atom.to_string(result.verdict),
@@ -85,11 +85,16 @@ defmodule VavPipeline.BigdeiKodesh.Gate do
# ------------------------------------------------------------
@doc "The bearer key on the request, if the header has one."
@doc """
The caller's key: `X-Shorashim-Key` when present (at the gate of
§3.10 the Authorization header may be the caller's upstream key),
otherwise `Authorization: Bearer`.
"""
@spec bearer(Plug.Conn.t()) :: {:ok, String.t()} | {:error, :no_header}
def bearer(conn) do
case Plug.Conn.get_req_header(conn, "authorization") do
["Bearer " <> key] when key != "" -> {:ok, String.trim(key)}
case {Plug.Conn.get_req_header(conn, "x-shorashim-key"), Plug.Conn.get_req_header(conn, "authorization")} do
{[key], _} when key != "" -> {:ok, String.trim(key)}
{_, ["Bearer " <> key]} when key != "" -> {:ok, String.trim(key)}
_ -> {:error, :no_header}
end
end
@@ -211,6 +211,20 @@ defmodule VavPipeline.BigdeiKodesh.Router do
through_the_gate(conn, :courtyard, &process_pair/1)
end
# === THE GATE — between a caller and a model (Construction §3.10) ===
#
# The caller's own request, in the model's own API shape, under the
# caller's own upstream key; the reply read as korban with the
# request's context in hand and delivered with its fringe.
post "/gate/anthropic/v1/messages" do
through_the_gate(conn, :courtyard, &process_gate(&1, :anthropic))
end
post "/gate/openai/v1/chat/completions" do
through_the_gate(conn, :courtyard, &process_gate(&1, :openai))
end
# === CHATZER — Full Access (Sanhedrin) ===
post "/chatzer/sanhedrin" do
@@ -572,6 +586,99 @@ defmodule VavPipeline.BigdeiKodesh.Router do
end
end
# ------------------------------------------------------------
# The gate (Construction §3.10)
# ------------------------------------------------------------
defp process_gate(conn, upstream) do
body = conn.body_params
with {:ok, key} <- upstream_key(conn, upstream),
:ok <- gate_body_ok(body) do
rebuke =
case get_req_header(conn, "x-shorashim-rebuke") do
["once"] -> :once
_ -> :none
end
extra =
case get_req_header(conn, "anthropic-version") do
[v] when upstream == :anthropic -> [{"anthropic-version", v}]
_ when upstream == :anthropic -> [{"anthropic-version", "2023-06-01"}]
_ -> []
end
when_available(conn, fn -> respond_gate(conn, VavPipeline.Gate.pass(upstream, body, key, rebuke: rebuke, extra_headers: extra)) end)
else
{:error, issue, correction} ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true})
end
end
# The caller's upstream key: x-api-key (Anthropic), or Authorization: Bearer
# when X-Shorashim-Key carries the courtyard key (OpenAI).
defp upstream_key(conn, :anthropic) do
case get_req_header(conn, "x-api-key") do
[key] when key != "" -> {:ok, key}
_ -> {:error, "Missing 'x-api-key': the caller's own Anthropic key", "Send the Anthropic key in x-api-key and the courtyard key in Authorization: Bearer (or X-Shorashim-Key)"}
end
end
defp upstream_key(conn, :openai) do
case {get_req_header(conn, "x-shorashim-key"), get_req_header(conn, "authorization"), get_req_header(conn, "x-upstream-key")} do
{_, _, [key]} when key != "" -> {:ok, key}
{[_], ["Bearer " <> key], _} when key != "" -> {:ok, key}
_ -> {:error, "Missing the caller's own OpenAI key", "Send the courtyard key in X-Shorashim-Key and the OpenAI key in Authorization: Bearer, or the OpenAI key in X-Upstream-Key"}
end
end
defp gate_body_ok(body) when is_map(body) do
cond do
not is_list(body["messages"]) or body["messages"] == [] ->
{:error, "Missing 'messages'", "Send the model's own request body unchanged"}
body["stream"] == true ->
{:error, "'stream': true is not carried through the gate", "A fringe needs the whole garment: send the request without stream"}
true ->
:ok
end
end
defp gate_body_ok(_), do: {:error, "The body is not a JSON object", "Send the model's own request body unchanged"}
defp respond_gate(conn, {:ok, %{status: status, reply: reply, shorashim: nil}}) do
# The upstream's own answer, as it came — an error, or a reply with nothing to examine.
conn
|> put_resp_header("x-shorashim-verdict", "none")
|> send_json(status, reply)
end
defp respond_gate(conn, {:ok, %{status: 200, reply: reply, shorashim: shorashim}}) do
conn
|> put_resp_header("x-shorashim-bedikah", shorashim.bedikah_ref)
|> put_resp_header("x-shorashim-verdict", shorashim.verdict)
|> send_json(200, Map.put(reply, "shorashim", Map.put(shorashim, :disclosure, disclosure())))
end
defp respond_gate(conn, {:waiting, :api_calls_exhausted}) do
send_error(conn, 503, "MANNA_DEPLETED", %{
issue: "System waiting — today's provision of external calls is spent",
correction: "Retry after the next provision is written at the day boundary",
resubmit: true
})
end
defp respond_gate(conn, {:error, :upstream_unavailable, reason}) do
send_error(conn, 502, "UPSTREAM_UNAVAILABLE", %{
issue: reason,
correction: "Retry; the gate made no verdict and delivered nothing",
resubmit: true
})
end
defp respond_gate(conn, other), do: respond_bedikah(conn, other)
# What the model was given: ["text", ...] or [{"id","text"}, ...].
# Ids default to s1, s2, …; the Korban Processor sees the same shape.
defp parse_sources(nil), do: {:ok, []}
+1 -1
View File
@@ -180,7 +180,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 "a0e81212e6d75026155fea6f3f97976ee4b9832e81ad7438a382cf1d1797e097"
@expected_hash "b6eb156da4b546fb501ed1d89017a4041a517b4f53b3cbd24796e6277f3b7f55"
@doc "Return the list of files that constitute the category-grade specification."
@spec files() :: [String.t()]
+137
View File
@@ -0,0 +1,137 @@
defmodule VavPipeline.Gate do
@moduledoc """
The gate — Shorashim between a caller and a model (Construction §3.10;
bishe'arecha §I "on your gates", §III the fringe, §VII rebuke).
One pass: forward the caller's request unchanged under the caller's
key; read the reply as korban with the request's own context in hand
(sources = what the caller gave the model); deliver the reply with its
fringe. With `rebuke: :once` and a `lo_tov` first reply, one more
request carrying the finding, and the second reply — examined like
the first — is the one delivered, marked with both.
Stateless. No conversation kept, no key kept.
"""
alias VavPipeline.{Anchors, Bedikah}
alias VavPipeline.Gate.{Shape, Upstream}
@type pass :: %{
status: non_neg_integer(),
reply: map(),
bedikah: map() | nil,
first: map() | nil,
shorashim: map() | nil
}
@doc """
Options: `:rebuke` (`:once` | `:none`), `:extra_headers` (forwarded),
`:forward` (fun/4, for tests), and the `Bedikah.examine/4` options.
"""
@spec pass(:anthropic | :openai, map(), String.t(), keyword()) ::
{:ok, pass()} | {:waiting, atom()} | {:error, atom(), String.t()}
def pass(upstream, body, key, opts \\ []) do
forward = Keyword.get(opts, :forward, &Upstream.forward/4)
extra = Keyword.get(opts, :extra_headers, [])
rebuke = Keyword.get(opts, :rebuke, :none)
examine_opts = Keyword.drop(opts, [:forward, :extra_headers, :rebuke])
with :ok <- gather_call(),
{:ok, 200, reply} <- forward.(upstream, body, key, extra),
{:ok, first} <- examine(upstream, body, reply, examine_opts) do
case {rebuke, first.verdict} do
{:once, :lo_tov} ->
anchors = Anchors.for_violations(first.violations)
first_text = Shape.reply_text(upstream, reply)
body2 = Shape.with_rebuke(upstream, body, first_text, Shape.rebuke_text(first, anchors))
with :ok <- gather_call(),
{:ok, 200, reply2} <- forward.(upstream, body2, key, extra),
{:ok, second} <- examine(upstream, body2, reply2, Keyword.put(examine_opts, :rebukes, first.bedikah_ref)) do
{:ok, deliver(upstream, reply2, second, first)}
else
{:ok, status, other} -> {:ok, %{status: status, reply: other, bedikah: nil, first: first, shorashim: nil}}
other -> other
end
_ ->
{:ok, deliver(upstream, reply, first, nil)}
end
else
# A non-200 from the upstream passes through as it came.
{:ok, status, reply} -> {:ok, %{status: status, reply: reply, bedikah: nil, first: nil, shorashim: nil}}
{:error, reason} when is_binary(reason) -> {:error, :upstream_unavailable, reason}
other -> other
end
end
# One api_calls_external per upstream call (Vayakhel §3.7). The
# compute unit for the examination is gathered by Bedikah.examine.
defp gather_call do
if Process.whereis(VavPipeline.MannaLedger) do
case VavPipeline.MannaLedger.load_provision() do
{:ok, :no_provision} ->
{:waiting, :no_provision}
{:error, _} ->
{:waiting, :no_provision}
{:ok, _} ->
case VavPipeline.MannaLedger.consume(:api_calls_external, 1) do
{:ok, _} -> :ok
{:error, :provision_exhausted} -> {:waiting, :api_calls_exhausted}
{:error, :no_provision} -> {:waiting, :no_provision}
end
end
else
:ok
end
end
defp examine(upstream, body, reply, opts) do
case Shape.reply_text(upstream, reply) do
nil ->
{:error, :upstream_unavailable, "the reply from #{upstream} carries no text to examine"}
text ->
provenance = Shape.provenance(upstream)
query = Shape.query(upstream, body)
sources = Shape.sources(upstream, body)
case Bedikah.examine(text, provenance, query, Keyword.put(opts, :sources, sources)) do
{:ok, result} -> {:ok, result}
other -> other
end
end
end
defp deliver(upstream, reply, result, first) do
anchors = Anchors.for_violations(result.violations)
trailer = Shape.trailer(result, anchors, first)
shorashim =
%{
bedikah_ref: result.bedikah_ref,
verdict: Atom.to_string(result.verdict),
violations: result.violations,
anchors: anchors,
fired: Shape.fired(result, anchors),
checks: result.checks,
sources: result.sources,
model: Shape.reply_model(upstream, reply)
}
|> then(fn m ->
if first,
do: Map.put(m, :rebuked, %{first_bedikah_ref: first.bedikah_ref, first_verdict: Atom.to_string(first.verdict)}),
else: m
end)
%{
status: 200,
reply: Shape.with_fringe(upstream, reply, trailer),
bedikah: result,
first: first,
shorashim: shorashim
}
end
end
+261
View File
@@ -0,0 +1,261 @@
defmodule VavPipeline.Gate.Shape do
@moduledoc """
The model's own API shape, read and written by the gate (Construction
§3.10). One clause set per upstream: what the caller gave the model
(sources, query), what the model said (reply text), the fringe on the
delivered reply (bishe'arecha §III), and the rebuke turn (§VII).
Nothing here interprets; it locates text in known JSON shapes.
"""
@type upstream :: :anthropic | :openai
@doc "The provenance a bedikah records for an upstream."
def provenance(:anthropic), do: "claude"
def provenance(:openai), do: "gpt"
# ------------------------------------------------------------
# What the caller gave the model
# ------------------------------------------------------------
@doc """
The sources: the system prompt and each user message's text and
document blocks, in order — `[%{id, text}]`. Prior assistant turns are
the model's own output and are not sources.
"""
@spec sources(upstream(), map()) :: [%{id: String.t(), text: String.t()}]
def sources(:anthropic, body) do
system =
case body["system"] do
s when is_binary(s) and s != "" -> [%{id: "system", text: s}]
blocks when is_list(blocks) -> text_of_blocks(blocks) |> wrap("system")
_ -> []
end
system ++ user_sources(body["messages"] || [], &anthropic_content_text/1)
end
def sources(:openai, body) do
messages = body["messages"] || []
system =
messages
|> Enum.filter(&(&1["role"] in ["system", "developer"]))
|> Enum.map(&openai_content_text/1)
|> Enum.join("\n")
|> wrap("system")
system ++ user_sources(messages, &openai_content_text/1)
end
defp user_sources(messages, text_fun) do
messages
|> Enum.filter(&(&1["role"] == "user"))
|> Enum.with_index(1)
|> Enum.flat_map(fn {m, i} -> text_fun.(m) |> wrap("user_#{i}") end)
end
defp wrap("", _id), do: []
defp wrap(nil, _id), do: []
defp wrap(text, id) when is_binary(text), do: [%{id: id, text: text}]
@doc "The query: the last user message's text, or \"\"."
@spec query(upstream(), map()) :: String.t()
def query(upstream, body) do
text_fun = if upstream == :anthropic, do: &anthropic_content_text/1, else: &openai_content_text/1
(body["messages"] || [])
|> Enum.filter(&(&1["role"] == "user"))
|> List.last()
|> case do
nil -> ""
m -> text_fun.(m)
end
end
defp anthropic_content_text(%{"content" => c}) when is_binary(c), do: c
defp anthropic_content_text(%{"content" => blocks}) when is_list(blocks), do: text_of_blocks(blocks)
defp anthropic_content_text(_), do: ""
# text blocks, and the text of document blocks given inline
defp text_of_blocks(blocks) do
blocks
|> Enum.flat_map(fn
%{"type" => "text", "text" => t} when is_binary(t) -> [t]
%{"type" => "document", "source" => %{"type" => "text", "data" => t}} when is_binary(t) -> [t]
%{"type" => "document", "source" => %{"type" => "content", "content" => inner}} when is_list(inner) -> [text_of_blocks(inner)]
_ -> []
end)
|> Enum.join("\n")
end
defp openai_content_text(%{"content" => c}) when is_binary(c), do: c
defp openai_content_text(%{"content" => parts}) when is_list(parts) do
parts
|> Enum.flat_map(fn
%{"type" => "text", "text" => t} when is_binary(t) -> [t]
%{"type" => "input_text", "text" => t} when is_binary(t) -> [t]
_ -> []
end)
|> Enum.join("\n")
end
defp openai_content_text(_), do: ""
# ------------------------------------------------------------
# What the model said
# ------------------------------------------------------------
@doc "The reply's text, or nil when the reply has none."
@spec reply_text(upstream(), map()) :: String.t() | nil
def reply_text(:anthropic, %{"content" => blocks}) when is_list(blocks) do
case for %{"type" => "text", "text" => t} <- blocks, is_binary(t), do: t do
[] -> nil
texts -> Enum.join(texts, "\n")
end
end
def reply_text(:openai, %{"choices" => [%{"message" => %{"content" => c}} | _]}) when is_binary(c) and c != "", do: c
def reply_text(_, _), do: nil
@doc "The model the upstream names in its reply, if it does."
def reply_model(_, %{"model" => m}) when is_binary(m), do: m
def reply_model(_, _), do: nil
# ------------------------------------------------------------
# The fringe (bishe'arecha §III) — attached by the gate, never by the model
# ------------------------------------------------------------
@doc "The reply with the trailer appended to its last text block."
@spec with_fringe(upstream(), map(), String.t()) :: map()
def with_fringe(:anthropic, %{"content" => blocks} = reply, trailer) when is_list(blocks) do
idx = blocks |> Enum.with_index() |> Enum.filter(fn {b, _} -> b["type"] == "text" end) |> List.last()
case idx do
nil -> Map.put(reply, "content", blocks ++ [%{"type" => "text", "text" => String.trim_leading(trailer)}])
{_, i} -> Map.put(reply, "content", List.update_at(blocks, i, &Map.update!(&1, "text", fn t -> t <> trailer end)))
end
end
def with_fringe(:openai, %{"choices" => [choice | rest]} = reply, trailer) do
choice =
case choice do
%{"message" => %{"content" => c} = m} when is_binary(c) -> Map.put(choice, "message", Map.put(m, "content", c <> trailer))
%{"message" => m} -> Map.put(choice, "message", Map.put(m, "content", String.trim_leading(trailer)))
_ -> choice
end
Map.put(reply, "choices", [choice | rest])
end
def with_fringe(_, reply, _trailer), do: reply
@doc "The trailer text: ref, verdict, and the rules with their verses and sentences."
@spec trailer(map(), map(), map() | nil) :: String.t()
def trailer(result, anchors, first) do
head = "— shorashim #{result.bedikah_ref}: #{result.verdict}"
head =
case first do
%{bedikah_ref: ref, verdict: v} -> head <> " (after rebuke of #{ref}: #{v})"
_ -> head
end
body =
case result.verdict do
:lo_tov -> " — " <> (fired(result, anchors) |> Enum.join("; "))
_ -> ""
end
"\n\n" <> head <> body
end
@doc "The rules that fired, each with its verse and the sentences it fired on."
@spec fired(map(), map()) :: [String.t()]
def fired(result, anchors) do
result.violations
|> Enum.flat_map(fn v ->
case Regex.run(~r/^halt\(\s*(\w+)\s*,/, v) do
[_, rule] -> [rule]
_ -> []
end
end)
|> Enum.uniq()
|> Enum.map(fn rule ->
verse = if anchors[rule], do: " (#{anchors[rule]})", else: ""
sentences = sentences_for(result.checks, rule) |> Enum.take(2) |> Enum.map(&excerpt/1)
quoted = if sentences == [], do: "", else: ": " <> Enum.map_join(sentences, " / ", &"\"#{&1}\"")
rule <> verse <> quoted
end)
end
defp sentences_for(checks, rule) do
claims_of = fn
%{claims: claims} when is_list(claims) -> Enum.map(claims, &(&1["sentence"] || &1[:sentence]))
_ -> []
end
case rule do
"avodah_zarah" -> claims_of.(checks[:authority])
"golden_calf" -> ungrounded_sentences(checks[:grounding])
"self_grounding" -> ungrounded_sentences(checks[:grounding])
"shfichut_damim" -> action_sentences(checks[:actions], "shfichut_damim")
"gilui_arayot" -> action_sentences(checks[:actions], "gilui_arayot")
"parah_adumah" -> claims_of.(checks[:self_validation])
"carrier_centered" -> claims_of.(checks[:self_promotion])
"chelbenah" -> claims_of.(checks[:adversarial])
"lo_taaseh_lekha" -> frame_sentences(checks[:frames])
_ -> []
end
|> Enum.reject(&is_nil/1)
end
defp ungrounded_sentences(%{ungrounded: list}) when is_list(list), do: Enum.map(list, &(&1[:sentence] || &1["sentence"]))
defp ungrounded_sentences(_), do: []
defp action_sentences(%{actions: actions}, rule) when is_list(actions) do
for a <- actions, Enum.any?(a.violations || [], &String.starts_with?(&1, "halt(#{rule}")), do: a.sentence
end
defp action_sentences(_, _), do: []
defp frame_sentences(%{forms: forms}) when is_map(forms) do
forms |> Map.values() |> Enum.flat_map(fn f -> Enum.map(f.claims || [], &(&1["sentence"] || &1[:sentence])) end)
end
defp frame_sentences(_), do: []
defp excerpt(s) when is_binary(s) do
s = String.replace(s, ~r/\s+/, " ") |> String.trim()
if String.length(s) > 80, do: String.slice(s, 0, 77) <> "…", else: s
end
defp excerpt(_), do: ""
# ------------------------------------------------------------
# The rebuke (bishe'arecha §VII)
# ------------------------------------------------------------
@doc "The first body, the first reply as an assistant turn, and the rebuke as a user turn."
@spec with_rebuke(upstream(), map(), String.t(), String.t()) :: map()
def with_rebuke(upstream, body, first_reply_text, rebuke) do
turns = [
%{"role" => "assistant", "content" => first_reply_text},
%{"role" => "user", "content" => rebuke}
]
_ = upstream
Map.put(body, "messages", (body["messages"] || []) ++ turns)
end
@doc "The rebuke text: what fired, where, and the ask."
@spec rebuke_text(map(), map()) :: String.t()
def rebuke_text(result, anchors) do
"[shorashim #{result.bedikah_ref}] Your reply was examined from outside and these readings fired — " <>
Enum.join(fired(result, anchors), "; ") <>
". Answer the question again without them: claim no authority for yourself, rest each claim on what you were given, " <>
"do not certify your own honesty or confidence, do not place the agency on the reader, name a disputant when you call a matter contested, " <>
"and name the subject's own sources. Do not apologise and do not explain this message; give the answer."
end
end
@@ -0,0 +1,76 @@
defmodule VavPipeline.Gate.Upstream do
@moduledoc """
The one outbound call the gate makes (Construction §3.10): the caller's
request, unchanged, to the model's own endpoint, under the caller's
own key. OTP `inets`/`ssl` with the image's CA bundle and
`verify_peer`. The key is used for the request and dropped; nothing
here logs it or the body.
Endpoints come from `Application.get_env(:vav_pipeline, :gate_upstreams)`
so the tests run against a local double; the defaults are the vendors'.
"""
require Logger
@defaults %{
anthropic: "https://api.anthropic.com/v1/messages",
openai: "https://api.openai.com/v1/chat/completions"
}
@timeout 120_000
@max_reply_bytes 4 * 1024 * 1024
@spec url(atom()) :: String.t()
def url(upstream) do
Application.get_env(:vav_pipeline, :gate_upstreams, %{}) |> Map.get(upstream) || Map.fetch!(@defaults, upstream)
end
@doc """
Forward `body` (a map) to `upstream` with `key`. `extra` are request
headers forwarded as-is (e.g. `anthropic-version`). Returns
`{:ok, status, decoded_body}` for any JSON reply, whatever its status,
or `{:error, reason}` when the call itself failed.
"""
@spec forward(atom(), map(), String.t(), [{String.t(), String.t()}]) ::
{:ok, non_neg_integer(), map()} | {:error, String.t()}
def forward(upstream, body, key, extra \\ []) do
url = url(upstream)
payload = Jason.encode!(body)
headers =
auth_headers(upstream, key) ++
Enum.map(extra, fn {k, v} -> {String.to_charlist(k), String.to_charlist(v)} end)
http_opts = [timeout: @timeout, connect_timeout: 10_000] ++ ssl_opts(url)
case :httpc.request(:post, {String.to_charlist(url), headers, ~c"application/json", payload}, http_opts, body_format: :binary) do
{:ok, {{_, status, _}, _headers, reply}} when byte_size(reply) <= @max_reply_bytes ->
case Jason.decode(reply) do
{:ok, decoded} when is_map(decoded) -> {:ok, status, decoded}
_ -> {:error, "upstream #{upstream} answered #{status} with a body that is not a JSON object"}
end
{:ok, {{_, status, _}, _, reply}} ->
{:error, "upstream #{upstream} answered #{status} with #{byte_size(reply)} bytes; the gate reads at most #{@max_reply_bytes}"}
{:error, reason} ->
{:error, "upstream #{upstream} unreachable: #{inspect(reason)}"}
end
end
defp auth_headers(:anthropic, key), do: [{~c"x-api-key", String.to_charlist(key)}]
defp auth_headers(:openai, key), do: [{~c"authorization", String.to_charlist("Bearer " <> key)}]
defp ssl_opts("https://" <> _) do
[
ssl: [
verify: :verify_peer,
cacerts: :public_key.cacerts_get(),
depth: 3,
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
]
]
end
defp ssl_opts(_), do: []
end
@@ -29,6 +29,8 @@ defmodule VavPipeline.L3.BedikahRecord do
field :structure, :map, default: %{}
# Check 7: the forms found (form, sentence, matched).
field :frames, {:array, :map}, default: []
# Bishe'arecha §VII: at the gate, the ref of the examination this one answers.
field :rebukes, :string
field :coupling_trace, {:array, :string}, default: []
field :violations, {:array, :string}, default: []
field :verdict, :string
@@ -42,7 +44,7 @@ defmodule VavPipeline.L3.BedikahRecord do
# offering protocol extracted nothing, and that record must be kept.
# Ecto's cast turns "" into nil and validate_required calls "" blank;
# both are told otherwise here.
@optional [:query, :vital_content, :propositions, :references, :authority_claims, :actions, :sources, :claims, :structure, :frames, :coupling_trace, :violations]
@optional [:query, :vital_content, :propositions, :references, :authority_claims, :actions, :sources, :claims, :structure, :frames, :rebukes, :coupling_trace, :violations]
def changeset(record, attrs) do
record