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>
138 lines
5.0 KiB
Elixir
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
|