Files
bikoret/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/gate.ex
T
Scott KarpandClaude Fable 5.1 5c63273dba 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>
2026-10-07 11:38:32 +00:00

138 lines
5.0 KiB
Elixir

defmodule VavPipeline.BigdeiKodesh.Gate do
@moduledoc """
The gate of the courtyard — who may bring an offering past the pe'ah.
Bamidbar 1:51 — *v'hazar hakarev yumat*: the stranger who draws near
the inner service does not pass. The pe'ah (`/peah/*`) is open to the
ger; the courtyard (`/courtyard/*`) and the chatzer (`/chatzer/*`) are
for those who were given a key.
Until 2026-10-06 the gate looked only at the SHAPE of the header: any
`Authorization: Bearer <anything>` was admitted. A gate that opens for
every hand is a wall with a hole in it, and every admitted request
spends the day's manna.
**Keys.** A file (Construction §4.6: secrets are files, mode 0600,
never environment variables, never configuration, never the
repository), one key per line:
# tier key
courtyard 5f2c… # may bring offerings and bedikot
chatzer 9a1e… # may also convene the Sanhedrin
A chatzer key opens the courtyard too — the inner ring contains the
outer. Blank lines and `#` comments are ignored. The file is read on
every request so that a rotated key takes effect at once; it is a few
lines, and the cost is nothing beside a cycle.
**Fail closed.** No file, an empty file, an unreadable file — nobody
passes. The operator sees it in the log (`GATE_NO_KEYS`), never the
caller: the caller is told only that the key is missing or invalid.
Keys are compared in constant time (`Plug.Crypto.secure_compare/2`).
No key, valid or not, is ever written to a log.
"""
require Logger
@tiers [:courtyard, :chatzer]
@type tier :: :courtyard | :chatzer
@doc "Tiers, outermost first. A key of a later tier opens every earlier one."
@spec tiers() :: [tier()]
def tiers, do: @tiers
@doc "Where the keys are read from. `nil` means the gate is shut."
@spec keys_file() :: String.t() | nil
def keys_file, do: Application.get_env(:vav_pipeline, :api_keys_file)
@doc """
Admit the request to `tier`, or say why not.
* `{:ok, granted}` — the key is good for `granted`, which is `tier`
or an inner one.
* `{:error, :no_header}` — no `Authorization: Bearer <key>` header.
* `{:error, :unknown_key}` — the key is not in the file, or there
is no file (logged as GATE_NO_KEYS, once per denial).
* `{:error, :tier_too_low}` — a good key, but for an outer ring.
"""
@spec admit(Plug.Conn.t(), tier()) ::
{:ok, tier()} | {:error, :no_header | :unknown_key | :tier_too_low}
def admit(conn, tier) when tier in @tiers do
with {:ok, key} <- bearer(conn),
{:ok, granted} <- lookup(key, keys_file()) do
if rank(granted) >= rank(tier), do: {:ok, granted}, else: {:error, :tier_too_low}
end
end
@doc "Parse the contents of a keys file into `[{tier, key}]`. Malformed lines are skipped."
@spec parse(String.t()) :: [{tier(), String.t()}]
def parse(contents) do
contents
|> String.split(~r/\r?\n/)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == "" or String.starts_with?(&1, "#")))
|> Enum.flat_map(fn line ->
case String.split(line, ~r/\s+/, parts: 3) do
[tier, key | _] when tier in ["courtyard", "chatzer"] and key != "" ->
[{String.to_existing_atom(tier), key}]
_ ->
[]
end
end)
end
# ------------------------------------------------------------
@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, "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
defp lookup(_key, nil) do
Logger.warning("GATE_NO_KEYS: no api_keys_file configured — the courtyard is shut")
{:error, :unknown_key}
end
defp lookup(key, path) do
case File.read(path) do
{:ok, contents} ->
entries = parse(contents)
if entries == [] do
Logger.warning("GATE_NO_KEYS: #{path} holds no keys — the courtyard is shut")
end
# Compare against every entry, not just until the first match:
# the time taken must not say how far down the file the key was.
entries
|> Enum.reduce(nil, fn {tier, candidate}, found ->
if Plug.Crypto.secure_compare(candidate, key), do: best(found, tier), else: found
end)
|> case do
nil -> {:error, :unknown_key}
tier -> {:ok, tier}
end
{:error, reason} ->
Logger.warning("GATE_NO_KEYS: cannot read #{path} (#{reason}) — the courtyard is shut")
{:error, :unknown_key}
end
end
defp best(nil, tier), do: tier
defp best(a, b), do: if(rank(a) >= rank(b), do: a, else: b)
defp rank(tier), do: Enum.find_index(@tiers, &(&1 == tier))
end