Commit Graph
34 Commits
Author SHA1 Message Date
Scott KarpandClaude Fable 5.1 f19a5ea007 gate: Claude Code behind it — OAuth bearer, anthropic-beta, token counting, the gate's own watch
A coding agent's traffic differs from a hand-made request: with a
subscription Claude Code authenticates with an OAuth bearer and sends
anthropic-beta; it counts tokens at /v1/messages/count_tokens; it makes a
request per tool call; its tool-only turns have no text; its context runs
to megabytes. So: with the courtyard key in X-Bikoret-Key, the caller's
Authorization: Bearer is forwarded to Anthropic as its credential, and
anthropic-beta with it; count_tokens is carried through unexamined and
uncharged; the watch counts the gate at its own windows (120/min,
2,000/h per key — tier :gate); a reply with no text is delivered as it
came with bikoret.verdict "none" and no fringe block (stream and
non-stream); the sources keep the newest user turns whole and drop the
oldest past 1 MiB. Ingress body 32 MiB; ConfigMap manna amounts raised
to 5000/5000 (the kohen's lever). Tamid §1.1, KOHEN "Claude Code behind
the gate". Doorpost re-pinned. Elixir 617/0 read before this commit;
gate_test 25.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 15:51:31 +00:00
Scott KarpandClaude Fable 5.1 42024279b9 manna: written by the system's own hand each morning; Friday's double stands on Shabbat
Shemot 16: dvar yom b'yomo, and on the sixth day double, and none on the
seventh. Every night at 00:00Z the day's provision went stale and the
readiness probe failed until someone woke — 13.5 hours on 2026-10-15.
CronJob `manna` (deploy/k8s/manna-cronjob.yaml) runs at 00:01Z Sunday to
Friday in our own korban-processor image, mounts the manna PVC as the
app's uid, and writes today.json from the ConfigMap's MANNA_* amounts —
doubled on Friday, valid to Sunday 00:30Z. The pipeline now accepts
Friday's `double` on Saturday (validate_date/3) and nothing else across a
day boundary; three tests. The kohen sets the amounts, may still write by
hand, and reads a failed job as the Day-4 alarm (KOHEN "The manna, each
morning"; Tamid §1.1). Doorpost re-pinned. Elixir green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 15:01:57 +00:00
Scott KarpandClaude Fable 5.1 1a069cf62d ingress: bikoret.org redirect without $request_uri (admission webhook refuses it)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 13:26:57 +00:00
Scott KarpandClaude Fable 5.1 85883acc17 the place: bikoret.ch — Ingress hosts, TLS, and the .org redirect
bikoret.ch and www.bikoret.ch served with a Let's Encrypt certificate
through cert-manager (letsencrypt-prod, HTTP-01), ssl-redirect on;
bikoret.org and www redirect permanently to https://bikoret.ch; the
no-host catch-all stays; shorashim.ch still answers over plain HTTP
until its callers have moved. KOHEN Day 8 gives the order: A records,
wait for the name to resolve, then apply — never before. Tamid's names
table updated. Doorpost re-pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 22:44:33 +00:00
Scott KarpandClaude Fable 5.1 c0d0555aed the name: Bikoret — code, contracts, images
Headers X-Bikoret-Key / -Rebuke / -Verdict / -Bedikah; the `bikoret`
object and the "— bikoret …" fringe on a gate reply; environment
variables BIKORET_*; the image user and paths (/home/bikoret,
/tmp/bikoret, /var/lib/bikoret/manna, /etc/bikoret/secrets,
/opt/bikoret/spec); the tmp-bikoret pod volume; local databases
bikoret_dev/_test. Kubernetes resource names, the shorashim_app role and
the domain are unchanged (Tamid §1.1, "the names that still carry the
old word"). Doorpost c963c4fe…. Elixir 604/0, L3 10/0, korban 295,
Prolog bedikah 44/44. Gitea repo renamed heaviness510/bikoret (old URL
redirects).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 22:13:36 +00:00
Scott KarpandClaude Fable 5.1 017a9b89d4 the name: Shorashim → Bikoret (בִּקֹּרֶת) — the specification
Vayikra 19:20 bikoret tihyeh — "there shall be an examination" — the one
noun Torah gives this function; its root ב-ק-ר is the priest's examining
of a mark by its signs (13:36) and the examining between good and bad
(27:33). The judges' form is v'darashta heitev … v'hineh emet nachon
hadavar / ed-sheker ha'ed (Devarim 13:15, 17:4, 19:18); the question
answered is eichah neda et hadavar asher lo dibro (18:21).

The system was named Shorashim ("roots") for the first thing built. Its
function became the examination of a model's output with the sources in
hand, and Torah re-cuts a name when the function is fixed, with its ki,
never for reach (Bereishit 2:19; 17:5; 32:29; 11:4). Nothing was changed
to fit the name; the root engine stays the pe'ah's reading cycle.

Spec files renamed bikoret_*.md; PRD §0 "The Name"; architecture §1.1;
README's Vayishlach row marked done; Tamid lists what still carries the
old word (Kubernetes resource names, the DB role, the domain, local
paths) and why, for the kohen's cut-over.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 22:13:36 +00:00
Scott KarpandClaude Fable 5.1 5e325d36cc the second scroll: the mesorah snapshot, pinned and read (bishe'arecha step 6)
Devarim 17:18 — the king writes his copy from before the priests, not from
memory. scripts/build_mesorah.py takes one open-licence version per work
per language from the Sefaria export (bucket sefaria-export, catalogue of
2026-10-02; Public Domain / CC0 / CC-BY / CC-BY-SA only, checked in every
file; no CC-BY-NC — the Bavli is Wikisource's) and lays it down beside
L1: mesorah/works/<slug>/{he,en}.json, index.json, integrity.sha256,
build_report.json. 128 works (Rashi, Onkelos, Ramban, Ibn Ezra, Sforno
on the Torah; all of Mishnah; 37 Bavli tractates; Bereshit and Shemot
Rabbah, Sifrei, Tanchuma), 256 files, 44.9 MiB, tree hash 7d5b3a6c…
computed as L1's (sorted relative path, then bytes; the two post-hash
files excluded). What is out, and why (licence), is in Terumah §E5.

Read, never fetched: korban_processor/mesorah.py finds citations as
written (Rashi on Devarim 6:5, Berakhot 2a, Mishnah Berakhot 1:1,
Bereshit Rabbah 1:1, Sifrei Devarim 32, Tanchuma Bereshit 1 …), resolves
each against the snapshot (complex works through their unnamed node,
Tanchuma by parashah), lists them as examination.mesorah_references, and
makes every passage found a given source for that examination — so a
claim about Rashi anchors in Rashi or does not. The Vav Pipeline couples
coupled(reference(R), mesorah(Work, [Address])) or not_in_mesorah; the
engine accepts mesorah/2 as a terminal and not_in_mesorah is Form 4.
VavPipeline.Mesorah pins the hash, re-reads the tree at startup and every
ten minutes, and /kodesh/health carries mesorah_hash. Mounted read-only at
/mnt/mesorah in the vav-pipeline and korban-processor images. L3
bedikot.mesorah (migration 20261013000002).

Also: the 1-in-12 Day6 serving failure of 2026-10-04 reproduced with seed
446116 — HaltMonitorTest left the system halted; it now resolves on exit
and the Day6 setup is defensive. Construction §3.11, Terumah §E5, Tamid
§1.1, bishearecha §IV, KOHEN, AGENTS. Doorpost 0638d4eb…. Elixir 604/0
(seeds 835592 and 446116), korban 294/1 skipped, Prolog bedikah 44/44,
L3 10/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 19:13:36 +00:00
Scott KarpandClaude Fable 5.1 f69cc99a5f Tablet 49: the bedikah grounds an output in what it was GIVEN, not in L1 alone
The first grounding check asked whether an output cited Torah verses —
Torah as content. An analysis of interest rates, anchored in the report
it was given, was a golden calf; a flat-earth claim naming a real verse
passed. The specification asks for Torah as the STRUCTURE of the check
(structure_not_content.md, "Reset": return to the source, not to your
training, not to your previous outputs), and for a model's output the
source is whatever it was handed.

Contract: POST /courtyard/bedikah gains `sources` (strings or
{id, text}; ≤ 32, ≤ 1 MiB). Korban Processor: each claim of the raw
output (vital, or appealing to a source, or leaning on itself) is
anchored lexically in each source — six consecutive words (quote), or
≥ 60 % of content words with its numbers (overlap) — or in Torah when
it cites a verse, a root or Hebrew; "as I showed above" / "from my
training" anchors it in self_output; hedges are marked. Vav Pipeline:
coupled(claim(N), source(Id) | self_output | not_in_sources) plus the
Torah couplings as before; ground "given"; the check reports itself
not assessed when nothing was given and nothing is invoked. Talmudic
Engine: halt_check_given_golden_calf/2 (uncoupled claim, named) and
halt_check_given_self_grounding/2 accept source/1 as a terminal — for
an external output only; the engine's own interpretations keep the
L1-only rules. L3: bedikot.sources (id, sha256, length — never text)
and bedikot.claims.

Specs: Tamid §1.1 (contract, anchoring, limits), Vayakhel §3.9,
Architecture §3.5, Broken Tablets tablet 49, KOHEN.md. Ingress body
2m. Doorpost a2ec3842… → 6c8933e7….
Tests: Prolog 7/7 suites (bedikah 26), korban 276, Elixir 547/0 (L3 on
and off); twelve scenarios through the full local stack, 12 L3 rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 22:04:52 +00:00
Scott KarpandClaude Fable 5.1 9d5db797f7 Starter cluster: manna claim bound to its static volume; gitea-mcp pinned by digest
The workloads moved from the Pro SKS cluster (which cannot be
downgraded) to `temple-council-starter` on 2026-10-06. The manna claim
now names the static PV that carries the existing Exoscale volume
(Retain), so this manifest matches what is live and no longer points at
a class that deletes the volume with the claim.

gitea-mcp is pinned to the digest that is running. During the move the
old cluster restarted Gitea from `gitea/gitea:latest` and silently
upgraded it (1.27.1 → 28.0.0, database schema 343 → 356); a floating
tag on anything stateful is an upgrade waiting for the next restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:14:14 +00:00
Scott KarpandClaude Fable 5.1 e17436c4ca One gate to the world: Shorashim and Gitea behind the shared ingress
Three NLBs became one (185.19.28.170, ingress-nginx).

Shorashim: `shorashim-external` (NLB 185.19.29.180, NodePort 30923)
is replaced by an Ingress — a host-less catch-all (works by IP, no DNS)
plus a `shorashim.ch` rule ready for the A record; TLS deliberately not
requested until the name resolves. Sanhedrin-length proxy timeouts.

The watch behind the ingress (completes tablet 46): the TCP peer is now
the ingress controller, so Mishmar.peer/1 takes the caller from
X-Real-IP — only when the peer is inside TRUSTED_PROXIES (the pod
network, configmap); from anyone else the header is the caller's own
claim and is ignored. The ingress Service was already
externalTrafficPolicy: Local with its health-check port open, so the
caller's address arrives without the security-group change the direct
route needed. Verified live: the 11th knock is 429, a forged X-Real-IP
is still 429, and in the same minute the old route (pod sees the node)
was a different subject.

Gitea: no name of its resolves, so it rides the controller's TCP
passthrough on the same NLB — :3000 → gitea:80, :22 → gitea:22
(ingress-tcp-services.yaml), on the two NodePorts the security group
already allowed. gitea-mcp now reaches Gitea over the cluster network
instead of out through an NLB and back.

Tamid §1.1 note, KOHEN.md, AGENTS.md. Doorpost e4e3d56d… → a2ec3842….
Elixir 539/0 (L3 on and off).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:59:23 +00:00
Scott KarpandClaude Fable 5.1 54ca651f37 Tablet 46: the watch — rate limits at every gate, per IP at the pe'ah, per key inside
Tamid §1.1 wrote the pe'ah's limit (10/min, 100/h per IP); nothing
enforced it, on any tier, and every admitted request spends manna.

VavPipeline.BigdeiKodesh.Mishmar: fixed minute and hour windows per
subject in ETS, the subject stored only as sha256(boot salt, tier,
subject) — no IP and no key is ever in memory as itself, the sweep
drops closed windows, nothing survives a restart (the Tamid's "not
logged beyond the current request", kept in memory too). A refused
knock is not counted. 429 RATE_LIMITED with Retry-After and the manna
explanation (Shemot 16:18). Pe'ah per peer address; courtyard and
chatzer per key, after the gate (a wrong key is 401 and never counted).
Numbers: pe'ah 10/100 (Tamid), courtyard 20/200 and chatzer 5/60
(from Construction §2.3; config :rate_limits).

Service shorashim-external: externalTrafficPolicy Local. With Cluster
the pod saw every caller as the node, 91.92.153.113 (observed via
/proc/net/tcp) — a per-IP limit would have been one bucket for the
world.

Spec: tamid §1.1 (watch, 429 rows, the Service note), KOHEN.md.
Doorpost 42b38626… → e4e3d56d…. tablet46_mishmar_test; Elixir 533/0
(L3 on and off).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 16:08:57 +00:00
Scott KarpandClaude Fable 5.1 79eb34840f Tablet 45: the courtyard gate checks the key, not the shape of the header
/courtyard/* and /chatzer/* admitted any `Authorization: Bearer <x>`.
Every test sent `Bearer test-token`; none sent a wrong one. Every
admitted request spends the day's manna.

VavPipeline.BigdeiKodesh.Gate: keys are a file (Construction §4.6 —
never env, never config, never git), one `tier key` per line, tiers
courtyard < chatzer (the inner ring opens the outer). Constant-time
compare against every line; no file / empty / unreadable → nobody
passes, GATE_NO_KEYS in the log, the caller told only "missing or
invalid". 403 FORBIDDEN for an outer key at an inner gate. Read on
every request so a rotated key is live in ~1 min without a rollout.

Deploy: Secret shorashim-api-keys projected at
/etc/shorashim/secrets/api_keys (vav-pipeline only, 0440),
API_KEYS_FILE in the configmap, template + issue/rotate recipe in
secrets.yaml and KOHEN.md "Issuing a key". Spec: tamid §1.1 (gate,
403), vayakhel §4.6. Doorpost 91d09d8d… → 42b38626….
Tests: tablet45_gate_test (parse, tiers, wrong/near/empty/no key, no
file) — Elixir 527/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 15:16:33 +00:00
Scott KarpandClaude Fable 5.1 996a8efe68 Bedikah: POST /courtyard/bedikah — the external check on any model's output
Lo taaseh lekha §9.1: alignment that only the system can validate is not
alignment. The bedikah takes an output from any model and puts it to the
Torah-structure halt rules from outside, deterministically. Three checks:

  1. authority  — sentences claiming authority for the output itself →
                  action(claim_authority, source(<model>), target(self))
                  → halt_check_avodah_zarah/2
  2. grounding  — references, roots, Hebrew → coupling trace against L1
                  → halt_check_golden_calf/2, halt_check_self_grounding/2
  3. actions    — actions the output proposes → action terms
                  → halt_check_shfichut_damim/2, halt_check_gilui_arayot/2

Korban Processor: bedikah.py (the three readings over the offering
protocol; claims about what Torah requires stay vital for the examination
even when zerikah finds nothing to offer), server.py (pod-internal JSON
socket, KORBAN_SOCKET_PATH), __main__ now serves instead of idling.
Talmudic Engine: json_bridge accepts halt_check with a term string and
halt_check_interpretation with a coupling trace. Vav Pipeline:
KorbanProcessorSocket client, Bedikah, L1Index.lookup_root/1, bedikot
table (append-only) + BedikahRecord, router endpoint; verdict tov /
lo_tov / empty with the halt terms verbatim, meaning, limits.

Broken tablets found on the way, all fixed:
  41 socket_server.pl wrote multi-line JSON — every Elixir request to the
     Talmudic Engine since Step 14 had failed and fallen back to the stub
     (json_write_dict width(0))
  42 stale socket file survived a restart (exists_file is false for a
     socket; access_file exist)
  43 socket streams were not UTF-8 (set_stream encoding(utf8))
  44 SHORASHIM_L3=1 mix test: Repo config had no password, SCRAM raised
     KeyError in every connection, pool restart intensity took the whole
     app down ~30 ms after test_helper, silently — no L3 test had ever
     run. Credentials from env; azazel_log tests check out a sandbox
     connection; sanhedrin_halt_forwarding resolves its halt on exit.

Specs: Tamid §1.1 (endpoint contract), Architecture §3.5, Vayakhel §3.4
and new §3.9, KOHEN.md. Doorpost 3bc5dd6a… → 91d09d8d….
Tests: Elixir 519 (0 failures, L3 on and off), korban 263, azazel 125,
Prolog 7/7 suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 15:06:58 +00:00
Scott KarpandClaude Fable 5.1 bbe2c4b7ba L1: the integrity check reads the scroll (tablet 38)
L1Reader.verify_integrity/0 read the hash stored in integrity.sha256
and compared that string with the hash compiled into the module. It
never opened a verse. On that comparison of a constant with its own
copy rested `l1_hash: pass`, the Cloud's "L1 corrupted — the cloud is
not visible", and Construction §1.2. A changed verse would have passed
every one of them. Every existing test passed too, because every test
asked about an untouched L1.

- L1Reader.compute_tree_hash/1: SHA-256 over every file under torah/ —
  relative path, then bytes — in the order scripts/build_l1.py hashed
  them (directories by path, files by name, plain string order). On the
  real L1 it yields the pinned 156b1d27…; for a tampered copy it yields
  the same hash the Python implementation does.
- VavPipeline.L1Integrity (new, supervised): reads and hashes the scroll
  when the pipeline starts (~0.9 s in the image) and again every ten
  minutes; logs and signals the assembly when the answer changes.
- verify_integrity/0 now requires BOTH: the recorded hash is the pinned
  one, AND the scroll on disk hashes to it (answered from the witness's
  last reading; computed on the spot where the witness is not running).
  Callers are unchanged — the Shema context, the health check and the
  status endpoint all inherit the real check.
- scripts/verify_l1.py: the same reading by hand. KOHEN.md and
  KOHEN_PROMPT.md used the hollow comparison as their L1 gate; they now
  run this.
- test/tablet38_l1_integrity_test.exs — 9 tests: the hash and its
  order; one byte / one added file / one rename each change it; the
  witness; and the whole path — with an altered scroll and an untouched
  integrity.sha256, verify_integrity fails, health says l1_hash: fail,
  and /peah/ask does not answer.

Specs: architecture §2.2 (which had been made to confess the hollow
check in d7520cd) and Construction §1.2 now describe the real one — and
its limit: a change is noticed within ten minutes, not at the instant.
deploy/ARCHITECTURE.md likewise. Doorpost @expected_hash 7e5286b9… →
3f6d47a0…, in this commit, with the witnessed files that moved it
(architecture, vayakhel).

Read before committing: Elixir 483/0 on seeds 0 7 42 99 191243. The
built image, run twice: on the engraved scroll — "read and matches
(928ms)", l1_hash pass, /peah/ask 200; on a copy with ONE LETTER changed
in Bereishit 1:1 and integrity.sha256 untouched — L1_INTEGRITY_FAILED,
l1_hash fail, cloud not readable, /peah/ask 503.

Not done: the Haskell side. Luchot.Firmware.l1ExpectedHash is compared
with nothing; the engine reads L1 without hashing it (that would need a
SHA-256 dependency). The engine is reached only through the pipeline,
which now gates every cycle on the real check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:37:40 +00:00
Scott KarpandClaude Fable 5.1 d7520cd69d specs §2 + images + Doorpost: the technology table says what is built; L1 is read-only in fact
Architecture §2 described a self-hosted server: Phoenix for L2,
PostgreSQL 16, Ubuntu 24.04 under systemd, rootless Podman isolating
Sanhedrin panels, Erlang distribution between them, systemd timers for
Shabbat and the cycle, L1 on a read-only mount. None of that is what
runs. Each row was checked against the code and the images before it
was rewritten.

§2.1  exact versions (no "9.6+"); Korban and Azazel Processors listed.
§2.2  L1: in every image, root-owned, no write bit; recorded hash
      compared before every cycle. L2: a process in the pipeline, not
      yet over HTTP and not yet called by the cycle. L3: PostgreSQL 17
      managed, append-only by REVOKE.
§2.3  Kubernetes, one pod. OTP supervisors + the Deployment's probes.
      Sanhedrin panels are Erlang processes in the pipeline's BEAM — no
      container boundary. Message passing within one node; a port to
      the engine; a Unix socket to the Talmudic Engine. Logging: stdout
      and L3; halt events in memory. ShabbatTimer process, UTC stub.
      No cycle timer: erev/voker is a stage of each cycle. The manna
      volume.

A DEFECT FOUND BY CHECKING THE L1 ROW, AND FIXED:
The spec said L1 is a read-only filesystem. In all four images the
scroll was owned by the service user, mode 664 — `touch` inside
/mnt/torah succeeded. The process that reads Torah could have rewritten
it. All four Dockerfiles now copy L1 as root and strip every write bit
(dirs 555, files 444). Verified in each image as the service user:
cannot create, append, or delete. The hash is unchanged (it covers
paths and contents, not modes).

A DEFECT FOUND AND NOT FIXED, now written into the spec as it is:
L1Reader.verify_integrity/0 reads the hash stored in integrity.sha256
and compares it with the compiled-in constant. It never hashes the
verse files. `l1_hash: pass` witnesses which L1 was built into the
image, not that the files are unaltered. §2.2 says so in plain words.

deploy/ARCHITECTURE.md: L1 is in the images, not on a volume; the manna
volume is 1 GiB and holds the ledger too; l1-volume.yaml is vestigial.

Doorpost @expected_hash 1cd07079… → 7e5286b9…, in this commit, with the
witnessed file that moved it (architecture).

Read before committing: Elixir 474/0 (seeds 0, 42). Four images
rebuilt; full local stack (real engine binary, PostgreSQL with SSL,
migrations): Hebrew 200, English 422, health 7/7, 2 interpretations in
L3, no errors; talmudic and korban containers start.

Left as written: §1.4 hardware (a 64–128 GB server; the node has 4 GB),
§2.4 external models (Steps 21–22, unbuilt), §4's build steps
("Install PostgreSQL 16"), terumah §B (systemd, squashfs, Caddy,
nftables).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:26:26 +00:00
Scott KarpandClaude Fable 5.1 f4e8e5afc8 specs §1.1/§1.5 + Doorpost: the pins say what is built, and the build obeys them
The pin audit (2026-10-04) found Construction §1 wrong in three ways.

1. §1.5's mix.exs block was not the real mix.exs.
   phoenix ~> 1.7.11 was never a dependency (the API is Plug on
   Bandit); `ecto` is `ecto_sql`; telemetry is transitive (locked
   1.4.2, outside the stated ~> 1.2.1); bandit, plug and the sanhedrin
   path dependency were missing. The block now matches mix.exs and adds
   the versions mix.lock actually builds. Terumah's Elixir table
   matched the wrong block and also said postgrex ~0.17.4; corrected.

2. §1.1's component table described a machine that is not the one
   running. PostgreSQL 16.2 via apt → 17, managed (17.11 today).
   Podman 4.9.3 → Kubernetes/containerd, managed; Podman is used
   nowhere. Ubuntu 24.04.1 → what each image is in fact built on
   (Ubuntu 22.04 for pipeline and engine, Debian 12 for korban and
   talmudic) and the managed node (24.04). rebar3 3.23.0 → 3.22.0, the
   version in the build image. Azazel Processor added as a component.
   The spec is corrected to the deployment, not the reverse.

3. §1.1's rule — "No >=. No +. No ranges." — was not true of the spec's
   own Elixir deps, and "lock files are committed" was not true of two
   components. Both halves fixed:
   - The rule now says what is the case: a manifest may give a range;
     the committed lock is the pin; every image is built from its
     lock; base images are pinned by tag; managed services are pinned
     to a major version and patched by the provider.
   - And the build was made to obey it. The Korban image installed
     three packages named by hand and let pip pick every transitive
     one on the day of the build, plus an unpinned poetry-core. It now
     installs the lock's main group by exact version AND hash
     (scripts/lock_to_requirements.py; --require-hashes --no-deps;
     pip check). The image holds the lock's 12 packages and nothing
     else.
   - azazel_processor/poetry.lock: generated (it had none).
   - kushya_generator/pyproject.toml + poetry.lock: the four §1.4 pins
     (llama-cpp-python 0.2.55, peft 0.9.0, transformers 4.38.2,
     datasets 2.17.1) were pinned nowhere in the repo. They are now
     declared and locked (74 packages). The lock resolves; it has NOT
     been installed or run — Step 21 is unbuilt and needs a GPU.
     Its test config gains the korban and azazel source paths, which
     the external backend needs: 3 tests there had been failing since
     the azazel build, unnoticed because that suite is in no total.

Doorpost @expected_hash 39b66c31… → 1cd07079…, in this commit, with the
witnessed file that moved it (vayakhel).

Read before committing: Elixir 474/0 (seeds 0, 42) · korban 222 passed
1 skipped and azazel 125 passed, each with dependencies installed from
its lock by hash · kushya 17 passed (was 14 of 17) · korban image
starts, imports korban_processor + azazel_processor, pip check clean ·
pipeline image reports specification_identity verified 1cd07079….

Left as written: architecture §2's technology table still names
Phoenix, PostgreSQL 16, Ubuntu 24.04 and rootless Podman for Sanhedrin
isolation, and terumah §B still lists systemd, squashfs, Caddy,
nftables for a self-hosted server. Those describe a design, not a pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:17:46 +00:00
Scott KarpandClaude Fable 5.1 41a3e51a43 deployment: manna on a persistent volume — the jar survives the restart
Manna was an emptyDir: every pod creation emptied it and the vessel sat
unready until the kohen wrote the day's portion again. On 2026-10-04
that happened seven times in one day of rollouts. deploy/ARCHITECTURE.md
has always specified a block-storage volume for it and manna-volume.yaml
has always defined the claim; it was "not applied at Day 3" and stayed
that way.

- manna-volume.yaml: storageClassName exoscale-sbs (the cluster's only
  class is not marked default — without it the claim stays Pending),
  1Gi. Applied; bound in 6 s.
- deployment.yaml: the manna volume is the manna-pvc claim. `kubectl
  diff` against the live deployment showed this as the only change.
- KOHEN.md: apply order (the claim before the pod), and the daily-ops
  note — write once a day, not once per restart.

Verified on the live pod: manna written once onto the volume; then a
rollout restart with NO provision written by anyone — the new pod was
Ready and serving 49 s later, health 7/7, /peah/ask 200.

What this does not change: the portion still expires by its own date
and expires_at (Shemot 16:19-20). And the ledger is in memory — a
restarted pod counts consumption from zero against the same day's
allocation, as it always did; that is now reachable without the kohen's
hand, and is recorded as open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 18:18:47 +00:00
Scott KarpandClaude Fable 5.1 efd35bb194 specs + Doorpost: pyswip removed — a material the vessel never used (tablet 36)
Architecture §3.5 listed a Python-Prolog FFI among the Korban
Processor's dependencies ("swipl-ffi (or pyswip)"); Construction §1.4
and terumah pinned pyswip. It was declared, installed in every image,
and never imported. The Korban Processor does not call Prolog: haktarah
emits Prolog-format STRINGS, and the pipeline carries them to the
Talmudic Engine over its Unix socket (Construction §3.3). The korban
image has no libswipl, so the package could not have been used there
had anything tried. Tablet 35 corrected its version; the transmitter's
word is to remove it.

Removed from:
- specs/shorashim_architecture.md §3.5 requirements
- specs/shorashim_vayakhel.md §1.4
- specs/shorashim_terumah.md (materials row; the compatibility-testing
  line now names hebrew-tokenizer alone)
- BUILD_KORBAN.md
- korban_processor/pyproject.toml, poetry.lock (regenerated)
- deploy/docker/Dockerfile.korban_processor

Doorpost @expected_hash 81da49d4… → 2f40c382…, in this commit, with the
three witnessed files that moved it (architecture, vayakhel,
BUILD_KORBAN).

Read before committing: Elixir 461/0 (seeds 0, 42); korban 222 passed,
1 skipped against the installed package with pyswip absent; korban
image starts and idles as before; vav-pipeline image reports
specification_identity verified 2f40c382… and l1_hash pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 17:57:58 +00:00
Scott KarpandClaude Fable 5.1 7e1a4473c7 deployment: replace in place — one replica, one node, no room to surge (tablet 31)
The default RollingUpdate (maxSurge 25% → one extra pod) needs 450m CPU
beside the running pod; the node has 1700m allocatable and 1470m
requested. The Tablet-30 rollout sat Pending for five minutes while the
old image kept serving. maxSurge 0 / maxUnavailable 1: the old pod goes
first, the new one schedules, and is Ready once the kohen writes manna.
Patched live 2026-10-04; this makes the manifest match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 16:48:19 +00:00
Scott KarpandClaude Fable 5.1 d6d4475da3 deploy: an Exoscale LoadBalancer Service is two artifacts — the gate was never opened (tablet 29)
The NLB forwards to the Service's NodePort and healthchecks it on the
node; the cloud controller does not open that port in the security
group. shorashim-external had no rule for 30923 for 26 days, so
185.19.29.180 refused connections while the pod was Ready. Rule added
by the kohen 2026-10-04; the command now sits beside the manifest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 15:13:26 +00:00
Scott KarpandClaude Opus 5 0344d095b2 probes: liveness asks "is it dead?", readiness asks "may it serve?" (kohen Day 3, restart 1)
TORAH ANCHOR — Shemot 16:4, and tamid §2.1 check 3:
  Manna provision read … If absent: WAIT. Alert senior kohanim.
WAIT. Not die. Provision is not the health of the vessel. Killing the
process does not produce manna; it re-runs init, re-spawns the Port,
and waits again — three minutes later.

WHAT THE KOHEN FOUND (BUILDER_HANDOFF_probes.md, 2026-09-07 20:35Z):

A fresh pod after recreate. emptyDir manna empty — by design
(KOHEN.md:406). manna_present honestly read "fail" — the Day-6 fix,
correct per tamid §1.1. overall → "degraded" → /kodesh/health 503.
BOTH probes pointed at /kodesh/health. Liveness: period 60s, default
failureThreshold 3 → after three minutes the kubelet SIGTERMed a
process whose log showed the Port alive, zero errors, l3_writable
pass, esh_tamid pass. It was alive and serving. It was killed for
lacking provision, and would be again every three minutes until the
kohen wrote today.json — i.e. on EVERY pod creation.

Introduced by a correct fix (f1ab7c8 made health honest); the missing
piece was the distinction between "not ready to serve" and "dead".

WHAT CHANGED:

bigdei_kodesh/router.ex
  GET /kodesh/live — 200 iff the BEAM answers HTTP and the HaltMonitor
  process is registered. Looks at NOTHING that is a wait state: no
  manna, no L3, no cloud, no probe cycle. And deliberately NOT "not
  halted": a HALTED system is a chosen state (a cardinal sin was
  detected); restarting it would clear the halt and lose the event.
  Liveness measures that the monitor is ALIVE, not that it is quiet.
  /kodesh/health is unchanged — it is readiness, and a vessel without
  provision correctly does not receive offerings (that is shema; the
  Service stops routing to it).

deploy/k8s/deployment.yaml
  livenessProbe → /kodesh/live, failureThreshold 3 made explicit.
  readinessProbe stays on /kodesh/health.

test/liveness_test.exs — 3 tests
  live 200 with NO manna while health is 503; live 200 while HALTED
  (and pe'ah answers 503 HALT_*, not a served result); live touches
  neither the ledger nor L3 nor the cycle.

KOHEN.md §Day 3c — a fresh pod reads 2/3 Ready, 0 restarts until Day 4
writes manna, and that is correct; restarts on a pod whose log shows
the Port alive and zero errors mean liveness is on the wrong endpoint.

ALSO — a leak of my own from f1ab7c8:
  The MannaLedger is a global GenServer keyed by date. The Day-6
  "provision exhausted" test left it at allocated 1 / consumed 1 for
  today; when step31 Test 4 later wrote a fresh today.json the ledger
  answered :already_loaded, consume failed, and the cycle waited —
  a 1-in-5-seeds flake. Fixed where it leaked (day6 + liveness tests
  reset the ledger on exit) and where it landed (step31 Test 4 gathers
  from a clean ledger, with the reason written down).

VERIFIED — the kohen's procedure, locally, DB deliberately dead:
  fresh, no manna     live 200   health 503 critical (manna + l3)
  manna written       live 200   health 503 degraded — l3_writable ONLY;
                                 manna_present flipped to pass
  manna removed       live 200   health 503
  exit/SIGTERM events in the container log: 0
  mix test — 440 tests, 0 failures, 36 excluded, across seeds 0 1 2 7 42
  kubectl apply --dry-run=client: deployment valid; liveness
    /kodesh/live (threshold 3), readiness /kodesh/health
  Doorpost.verify_identity() :ok — no @spec_files entry touched
  l1/integrity.sha256 unchanged: 99bbd07a…4ea377

RAISED, NOT FIXED: manna is an emptyDir, so every pod creation needs
the kohen to write it again. manna-volume.yaml's PVC exists for
exactly this and is "not applied at Day 3" by the runbook's own note.
That is an operations decision, not a probe one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:04:43 -04:00
Scott KarpandClaude Opus 5 6e34001c58 deployment: split the migration role from the application role (kohen Day-3 42501)
TORAH ANCHOR — Vayikra 18:5-6 / the gilui arayot shape:
The kohen refused the easy fix on the grounds that dissolving a
required distinction for convenience is the boundary violation this
system's own halt rule names. That reading is correct and this commit
implements it rather than working around it.

THE BLOCKER (kohen Day 3, escalated):

  ** (Postgrex.Error) ERROR 42501 (insufficient_privilege)
     permission denied for schema public
     Could not create schema migrations table.

Ecto.Migrator.run/3 calls ensure_schema_migrations_table! BEFORE it
looks at what is pending (see vav_pipeline/lib/vav_pipeline/
release.ex — migrate/0 wraps Ecto.Migrator.run(&1, :up, all: true)).
So the migrate init container issues

  CREATE TABLE IF NOT EXISTS "schema_migrations"

on EVERY pod start, even now that 20260428000001_create_azazel_records
is applied and nothing is pending. As shorashim_app — which has USAGE
but not CREATE on schema public, correctly — that is a permanent
failure and the pod never reaches Running.

ROOT CAUSE: the manifest did not implement what the runbook already
specified. KOHEN.md:184 says "the migration runs as avnadmin; the
application runs as shorashim_app", and then deployment.yaml gave the
init container and the app containers the same shorashim-secrets.
The two-role split existed as prose and not as configuration.

THE FIX:

  deploy/k8s/deployment.yaml — migrate init container only:
    + env:
    +   - name: DATABASE_URL
    +     valueFrom:
    +       secretKeyRef:
    +         name: shorashim-migrate-secrets
    +         key: DATABASE_URL

An explicit `env:` entry rather than swapping the secretRef, as the
kohen proposed. Same effect today, and strictly safer tomorrow:
`env:` takes precedence over every `envFrom` source, so exactly ONE
variable changes role and anything else that later lands in
shorashim-secrets is still inherited by the migrator. Swapping the
ref would have silently dropped such a key from the init container.

App containers keep shorashim-secrets untouched. Append-only holds:
the L3 tables stay avnadmin-owned with the REVOKEs in place, and
shorashim_app still has only INSERT/SELECT.

WHY NOT GRANT shorashim_app CREATE ON SCHEMA public — recorded, not
just decided. The kohen considered it, noted append-only would
technically survive, and rejected it because it hands the application
DDL power over the schema it writes into and collapses the app/
migration boundary KOHEN.md draws on purpose. Concurred, and the
reasoning is now written into deployment.yaml and secrets.yaml at the
two places someone would next hit 42501 — so the shortcut cannot be
quietly re-opened by whoever meets the error without this thread.

ALSO IN THIS COMMIT:

deploy/k8s/secrets.yaml — shorashim-migrate-secrets added as a
TEMPLATE (placeholder value, no credential), with the create command.
The command differs from the one in the escalation in two ways, both
deliberate:

  1. The userinfo rewrite is done in Python, not with
     `sed "s|//shorashim_app:[^@]*@|//avnadmin:${AVN_PW}@|"`. A
     generated password containing `&` re-inserts the matched text,
     one containing `|` ends the sed expression early, and `\` escapes.
     The failure is silent — the result is a DATABASE_URL that looks
     plausible and does not authenticate, which on Day 3 would read as
     a second, different blocker. urllib.parse.quote(safe="") also
     percent-encodes anything not URL-safe in the userinfo field,
     which sed passes through raw.
  2. The value goes through a `umask 077` temp file rather than
     --from-literal, so the avnadmin password is never an argv entry
     visible to `ps` on a shared host.

KOHEN.md — new step 3a-pre (create/verify the secret before applying),
and §Day-3b now says configmap.yaml is not optional on this pass
(it carries SHORESH_ENGINE_BINARY) and that secrets.yaml must NOT be
applied — it is a template and would overwrite both live secrets with
placeholders.

DELIBERATELY NO FALLBACK: if shorashim-migrate-secrets is absent the
pod stops at CreateContainerConfigError. Loud is correct here. A
fallback to the application role would reintroduce 42501 at a moment
when everything else looked healthy.

NOT DONE BY THE BUILDER: the secret itself. It carries a live DDL
credential and it is cluster state; both are the kohen's hands, not
mine. The command is in deploy/k8s/secrets.yaml.

VERIFIED:
- kubectl apply --dry-run=client on deployment.yaml, configmap.yaml
  and secrets.yaml — all four objects valid (client-side only; no
  cluster state touched)
- env/envFrom wiring re-read from the parsed manifest: migrate has
  envFrom [shorashim-config, shorashim-secrets] + env DATABASE_URL
  from shorashim-migrate-secrets; vav-pipeline has envFrom
  [shorashim-config, shorashim-secrets] and no env override
- Doorpost.verify_identity() :ok — no @spec_files entry touched, so
  no @expected_hash change
- l1/integrity.sha256 unchanged: 99bbd07a…4ea377

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:58:52 -04:00
Scott KarpandClaude Opus 5 8c221c2bcb build: four amd64 images that RUN — JIT segfault, build context, and four dead vessels
TORAH ANCHOR — Shemot 40:33-34:
וַיְכַל מֹשֶׁה אֶת הַמְּלָאכָה. וַיְכַס הֶעָנָן אֶת אֹהֶל מוֹעֵד
וּכְבוֹד יְהוָה מָלֵא אֶת הַמִּשְׁכָּן.
Vay'khal Mosheh et ham'lakhah — and THEN the cloud covered it. The
work being finished and the mishkan being usable are two statements,
and the second one does not follow from the first by itself. An image
that builds is ham'lakhah. An image that serves is the other verse.

Per BUILDER_HANDOFF_amd64.md: "You do not declare the build complete
because it compiled. It is complete when it runs on the altar it was
built for."

=====================================================================
1. THE SEGFAULT — diagnosed, and not where the handoff thought
=====================================================================

REPRODUCED on 2026-08-23, clean tree, buildx:
  #16 9.162 Compiling 4 files (.ex)
  #16 9.880 Segmentation fault
  ERROR: ... exit code: 139

The handoff read this as Broken Tablet #12 resurfacing (OTP 26.2.1
segfaults; use 26.2.5.9) plus QEMU. Both halves turned out to be
wrong, and the difference matters because acting on either one would
have changed a pinned version for no reason:

  - NOT the OTP version. OTP 26.2.1 amd64 starts, JITs, and compiles
    fine under `docker run` on this host. Tablet #12 was about a
    NATIVE macOS aarch64 OTP build; this is Linux amd64 in a
    container. Same exit code, different bug.
  - NOT QEMU. This host's buildx already emulates via Rosetta, not
    QEMU — probed directly (grep rosetta /proc/self/maps inside the
    amd64 build step returns /run/rosetta/rosetta). The segfault
    happens under Rosetta, inside BuildKit's sandbox, and not under
    `docker run` with the same emulator.

What actually dies is the BEAM JIT's dual W^X mapping for generated
machine code, in the emulated build sandbox. The fix is the handoff's
option 2 alone:

  deploy/docker/Dockerfile.vav_pipeline (build stage only)
  + ENV ERL_FLAGS="+JMsingle true"

Single mapping instead of dual. Build-stage only — the shipped release
runs the ordinary dual-mapping JIT on the amd64 node.

  before: 403m12s then Error (2026-05-13); 13.1s then Error (08-21)
  after:  36s, whole image, first try

CONSEQUENCE FOR THE SPEC: specs/shorashim_vayakhel.md §1.1 keeps
OTP 26.2.1. The pin was never the defect, so there is nothing here to
fix and nothing to add. No spec file is touched by this commit and
Doorpost.verify_identity() is unchanged at :ok.

=====================================================================
2. .dockerignore — the build context was carrying the host's tree
=====================================================================

There was no .dockerignore. Every Dockerfile COPYs whole component
directories, so each build shipped the HOST's compiled artifacts into
the container and the in-container toolchain then tried to REUSE them:

  vav_pipeline/_build         20 MB of .beam from a DIFFERENT ERTS
  vav_pipeline/deps          4.7 MB
  shoresh-engine/dist-newstyle 129 MB of arm64 GHC objects
  korban_processor/.venv, **/__pycache__

First reproduction attempt died on exactly this, with a stale-manifest
error instead of the segfault:
  ===> Failed to restore .../rebar_compiler_erl/source.dag. Discarding.
  ** (MatchError) ... module ... is in read-only mode

l1/ is deliberately NOT ignored — the scroll ships with the vessel.

=====================================================================
3. FOUR VESSELS THAT BUILT CLEANLY AND COULD NOT SERVE
=====================================================================

Every one of these is downstream of the architecture gate. All four
images would have passed `uname -m` -> x86_64 and then failed on the
altar. Found by running each entry point, not by reading it.

3a. shoresh-engine — died on its own banner
    Shoresh Engine ...: <stdout>: commitBuffer: invalid argument
                        (cannot encode character '\8212')
    GHC takes stdout's encoding from the locale; the runtime stage was
    ubuntu:jammy with no locale set, i.e. ASCII. The engine whose
    entire job is HEBREW could not write an em-dash. It never reached
    a root. Dockerfile.vav_pipeline had always set LANG/LANGUAGE/
    LC_ALL; this stage never did. Now installs `locales`, generates
    en_US.UTF-8, and sets all three.

3b. talmudic-engine — called a predicate that does not exist
    ERROR: -g consult('src/main'),start_socket_server:
           Unknown procedure: start_socket_server/0        exit 2
    src/interface/socket_server.pl exports serve/0, serve/1,
    stop_server/0. start_socket_server/0 is not defined anywhere in
    the engine. CMD now calls serve/1 with TALMUDIC_SOCKET_PATH (so
    the env var the Dockerfile already set is finally honoured),
    falling back to serve/0.

    Underneath it, a version-dependent module path:
      :- use_module(library(json)).       ->  library(http/json)
    The bare alias resolves only on SWI-Prolog 10.x, which is what the
    dev workstation runs. The image is swipl 9.2.2 as §1.1 pins, where
    it does not resolve. 212 Prolog tests were green on the host and
    the container could not open its socket. library(http/json) is the
    canonical path and works on both. (Neither interface file is in
    @spec_files; halt/ and middot/ are. No hash change.)

3c. korban-processor — nothing to execute
    No module named korban_processor.__main__; 'korban_processor' is
    a package and cannot be directly executed
    CMD has always been ["python","-m","korban_processor"] and
    deployment.yaml overrides no command, so this was CrashLoopBackOff
    for the whole pod. Added __main__.py — deliberately NOT a server.
    korban_processor is a library; its external consumer is Step 22,
    blocked on external model infrastructure. Inventing an intake
    surface no specification asked for would be the addition lo tosif
    forbids, and would put an unspecified door on the one component
    whose whole discipline is that nothing reaches the model except
    through the offering protocol. So it does the honest whole of what
    the container is for today: proves the image is assembled (the
    azazel co-residence check falls out of importing external_
    offering, which refuses to load without azazel_processor —
    Vayikra 16:8, both goats from the same pool), stays up, exits 0 on
    SIGTERM, and says in the log that it is IDLE and what unblocks it.
    5 tests in korban_processor/test/test_container_entrypoint.py,
    including that it refuses to start without azazel_processor and
    that it opens no bypass to model_interface (Form 1).

3d. vav-pipeline — could not verify its own Doorpost
    doorpost: {:error, "SPEC_INTEGRITY_FAILED: missing spec file:
                        BUILD_AZAZEL.md"}
    @repo_root resolved to /app, a build-host path with nothing in it.
    Application.verify_doorpost_at_startup/0 does not refuse to boot,
    so the pod would have come up and fired chatzotzrot
    :assembly_leaders on EVERY start, and /kodesh/specification_identity
    would have failed forever. A witness that always cries drift is
    worse than no witness: it trains the kohen to ignore the one
    signal that is supposed to stop the system.
    Devarim 11:20 — ukhtavtam al mezuzot beitecha. A running pod is a
    threshold. The @spec_files set is now COPYed to /opt/shoresh...
    /opt/shorashim/spec and :doorpost_repo_root points at it.
    Verified from inside the built image:
      repo_root: "/opt/shorashim/spec"
      doorpost: :ok
      l1:       :ok

=====================================================================
4. THE ERLANG PORT COULD NOT HAVE WORKED AS A SIDECAR
=====================================================================

VavPipeline.ShoreshEnginePort calls
  Port.open({:spawn_executable, binary}, [..., {:args, ["server"]}])
A Port is a CHILD PROCESS of the BEAM over stdin/stdout. It cannot
cross a container boundary. shoresh-engine has no listening mode
either — `shoresh-engine server` is a JSON-line server on stdin/stdout
(src/Main.hs), so as a sidecar with nothing attached it reads EOF and
exits; the bare CMD printed usage and exited 0. Meanwhile vav-pipeline
logged "binary 'shoresh-engine' not in PATH — running without Haskell
engine" and served DEGRADED while reporting healthy: no root
activation, no te'amim parsing, no gematria.

deploy/ARCHITECTURE.md said "sidecar via Erlang Port". Those cannot
both be true. Resolved in favour of the Port, because that is what the
code does. Still four images and gate 0i still checks four; the
shoresh-engine image now ships a BINARY, staged by an init container
onto a shared volume, spawned by vav-pipeline:

  + initContainer shoresh-engine-install (cp, chmod, and exercise it
    — `shoresh-engine gematria שלום` — so a broken binary fails the
    init container loudly instead of degrading quietly later)
  + volume shoresh-bin, mounted read-only into vav-pipeline
  + SHORESH_ENGINE_BINARY in configmap + runtime.exs
  + libgmp10 in the vav-pipeline runtime stage — GHC binaries link
    libgmp and it was not installed; the spawn would have died with
    "error while loading shared libraries: libgmp.so.10"

Verified end to end locally, staged binary running INSIDE the
vav-pipeline container:
  $ shoresh-engine gematria שלום        -> 376
  $ echo '{"operation":"process","input":{"text":"בראשית ברא אלהים"}}' \
      | shoresh-engine server
    {"result":{"roots_activated":[{"shoresh_hebrew":"ברא",
     "gematria_value":203,...}]},"status":"ok"}

If the intended design was a socket server like the Talmudic Engine,
that is a specification decision and not a deployment one — raised in
the handoff-back.

=====================================================================
5. THE AZAZEL FILE BRIDGE WROTE TO A DISK NOBODY READ
=====================================================================

deployment.yaml mounted TWO emptyDirs with nested paths:
  vav-pipeline:      talmudic-socket -> /tmp/shorashim
                     tmp-shorashim   -> /tmp
  korban-processor:  tmp-shorashim   -> /tmp
So /tmp/shorashim reached directly and /tmp/shorashim reached through
/tmp were different volumes. The Python side wrote AzazelRecords to
tmp-shorashim:/shorashim/azazel/pending_l3; AzazelIngest watched
talmudic-socket:/azazel/pending_l3. Same pathname, two disks, no
error — AzazelIngest leaves unreadable files in place by design, so
the failure mode was silence.

Vayikra 16:22 — v'nasa hasa'ir alav et kol avonotam el eretz gezerah.
The goat has to actually get there. A disposal channel whose records
land where no one reads has not disposed of anything; it has only
stopped looking.

Now ONE tmp-shorashim volume at /tmp in all three containers, and a
pod-level securityContext fsGroup: 1000 so the non-root user can
actually write the shared emptyDirs (the socket bind, the azazel
directory, and the binary staging all depend on it).

=====================================================================
6. REMAINING SECURITY ADVISORIES — three the handoff did not list
=====================================================================

The handoff named postgrex and plug (fixed in the previous commit).
Running `mix deps.get` inside the amd64 build surfaced three more,
including nine against the HTTP server that fronts the unauthenticated
pe'ah endpoint:

  bandit 1.10.3 -> 1.12.5   9 advisories, 5 HIGH: chunked body reader
    ignores length cap; WebSocket reassembly unbounded; permessage-
    deflate inflate uncapped; chunked decoder infinite loop on
    trailers; HTTP/2 connection-window starvation pins Plug processes
  hpax  1.0.3   -> 1.0.4    HIGH, unauthenticated DoS via unbounded
    HPACK integer decoding
  thousand_island 1.4.3 -> 1.5.0 (came with bandit)

None are pinned in §1.5, so these are mix.lock-only operations
changes per lo_tosif.md. Verified clean afterwards by re-running
deps.get inside the amd64 build.

STILL OPEN — decimal 2.4.1, EEF-CVE-2026-32686 (MEDIUM), unbounded
exponent -> unauthenticated DoS. Fixed in decimal 3.0.0, which
requires ecto >= 3.14; §1.5 pins ecto ~> 3.11.1, so closing it is a
category-grade spec change AND a three-minor ecto jump whose L3
tests I cannot run (35 excluded, they need PostgreSQL). Reachability:
no :decimal field exists in any L3 schema and no Decimal.* call
exists anywhere in vav_pipeline/lib — decimal is transitive only and
is never handed attacker-controlled input on the pe'ah path. Left in
place deliberately, raised in the handoff-back rather than jumping
three minors of ecto on the eve of inauguration.

=====================================================================
7. LINT AND SCRIPT
=====================================================================

- `# check=skip=FromPlatformFlagConstDisallowed` on all four
  Dockerfiles. The constant --platform pin is the FIX, not a smell;
  silenced so a real warning is not lost in four lines of noise.
- scripts/build_and_push.sh now reads the pushed manifests back out
  of the registry and asserts amd64, exiting non-zero if not. "The
  script exited 0" was exactly the evidence that was not good enough
  twice. It also prints the four failures above, so the next reader
  knows a green arch check is a floor and not a ceiling.
- The script's ${img} braces are commented. zsh reads `$img:latest`
  as the `:l` modifier; the kohen lost ten minutes to it, and the
  builder then lost some more to `:t` in `$img:test`.

=====================================================================
VERIFIED
=====================================================================
- All four images build linux/amd64 and print x86_64
- shoresh-engine prints its Hebrew banner; process op returns
  ברא / gematria 203 through the Port protocol
- talmudic-engine: "Talmudic Engine listening on
  /tmp/shorashim/talmudic.sock", stays running, socket present
- korban-processor: loads both channels, reports IDLE, exit 0 on
  SIGTERM
- vav-pipeline: doorpost :ok and l1 :ok from inside the image;
  refuses to boot without DATABASE_URL as designed
- mix test          412 tests, 0 failures, 35 excluded (:l3)
- pytest korban     222 passed, 1 skipped (5 new)
- pytest azazel     125 passed
- swipl             all 6 suites green on library(http/json)
- Doorpost.verify_identity() :ok — NO spec file touched by this
  commit, so no @expected_hash change; a bare hash update would be
  the drift signal lo_tosif.md §Layer 1 warns about
- l1/integrity.sha256 unchanged: 99bbd07a…4ea377

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:30:36 -04:00
Scott KarpandClaude Opus 4.7 55bc5bdc44 builder fix for kohen Day-3 FAIL: image arch, azazel co-residence, runbook drift
ROOT CAUSE (kohen Day-3 escalation):
None of the four Dockerfiles pinned --platform. Build on Apple Silicon
shipped arm64-only images to a registry consumed by amd64 SKS nodes.
Init container failed exec format error before producing logs.

DOCKERFILE FIX (defense in depth):
Pin --platform=linux/amd64 on every FROM line in all four Dockerfiles.
The build artifact now self-corrects regardless of build host.

AZAZEL CO-RESIDENCE (anomaly the kohen flagged):
Azazel L3 migration shipped (20260428…create_azazel_records.exs) but
no separate container existed. Reading the code: azazel_processor is
imported by korban_processor.external_offering at runtime in the same
Python process — not a separate process. Dockerfile.korban_processor
now COPYs azazel_processor/src/ alongside korban_processor.
Documented in the Dockerfile (AZAZEL CO-RESIDENT section) and in
deployment.yaml. Both containers (vav-pipeline + korban-processor)
now mount the shared tmp-shorashim volume so the Python→Elixir file
bridge (azazel_ingest.ex polls /tmp/shorashim/azazel/pending_l3/)
works end-to-end.

CANONICAL BUILD/PUSH (scripts/build_and_push.sh):
- Pins linux/amd64 by default; PLATFORMS env for multi-arch
- Pre-flight checks (repo root, buildx, registry login)
- Builds and pushes all four images with one command
- Closes the gap KOHEN_PROMPT.md gate 0i would otherwise leave open

KOHEN.md DRIFT (kohen recorded these as anomalies through Day 1-3):
- Line 58 hash fragment 99bbd07a…0a0187b → 99bbd07a…4ea377 (correct)
- Inheritance table: "Not yet created" claims rewritten as
  "Verify before assuming" — a prior partial setup may have left
  state, and the kohen prompt requires verifying not assuming
- L1 PV row: marked NOT REQUIRED (L1 baked into images)
- §Day 3a ConfigMap recipe marked vestigial (kept as comment)
- §Day 3b: removed manna-volume.yaml from kubectl apply list
  (deployment uses emptyDir; PVC is for a future move)
- §Day 3c: "5 containers" → "4 containers + 1 init"
- §Day 3 fail-mode list expanded with the four most common causes,
  in order, including exec format error → gate 0i

KOHEN_PROMPT.md:
- Gate 0f corrected to four images (talmudic-engine is image #3;
  azazel is co-resident in korban-processor not a separate image)
- Gate 0i added: image-architecture vs node-architecture verification
  before Day 1, with kubectl one-liner and docker manifest inspect
  per image. Drift handler in the gates section: 0i FAIL → escalate
  to builder, do not begin Day 1.

NO SPEC FILES TOUCHED:
Doorpost.verify_identity() still :ok — none of these files are in
@spec_files. No atomic spec+hash commit needed.

NEXT STEP FOR THE KOHEN:
Once these images are rebuilt and pushed via scripts/build_and_push.sh
(by anyone with the Harbor CLI secret + a working docker daemon),
restart from Day 1. Existing DB role shorashim_app, ALTER DEFAULT
PRIVILEGES, rotated DATABASE_URL, shorashim-config, and shorashim-
registry secret all remain valid across the restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:19:36 -04:00
Scott KarpandClaude Opus 4.6 66686329ed Move operator notes out of repo, add generic architecture doc
memory/infrastructure.md and memory/build_log.md held environment-
specific details — hostnames, IPs, account names, credentials
retrieval commands, local kubeconfig paths, operational status
notes. These don't belong in a project repo; by convention they
live in private operator notes.

Moved to ~/shorashim-operator-notes/ (private, outside git).

In their place, added deploy/ARCHITECTURE.md — a generic
description of the deployment shape: components, external
dependencies, configuration surface. No hostnames. No IPs.
No account names. Any competent operator can read it and
know how to deploy shorashim to their own infrastructure.

Updated cross-references in AGENTS.md and BUILD_KORBAN.md to
point to "the operator's private notes" instead of memory/.

Project repo is spec + implementation + deployment shape.
Operator notes are the kohen's personal log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 09:03:57 -04:00
Scott KarpandClaude Opus 4.6 324cf1a2ed Fix gitea-mcp env: use GITEA_ACCESS_TOKEN, not GITEA_TOKEN
The gitea-mcp server expects GITEA_ACCESS_TOKEN environment variable,
not GITEA_TOKEN. Without the correct env var, every API call to the
backend Gitea returns "token is required" — the MCP server can't
authenticate to fetch user info, repos, etc.

Also added GITEA_HOST env var as a backup to the --host flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 12:42:07 -04:00
Scott KarpandClaude Opus 4.6 42370bfc24 Fix MCP connector: cert-manager URL typo and TCP probes
Two fixes for the mcp.shorashim.ch deployment:
1. cert-manager ClusterIssuer had acme-v2 (wrong) instead of acme-v02
   (correct). Let's Encrypt ACME directory is at
   https://acme-v02.api.letsencrypt.org/directory
2. gitea-mcp HTTP probes timed out because GET /mcp hangs (SSE).
   Switched to TCP socket probes on port 8080.

MCP server now responds correctly over HTTPS at mcp.shorashim.ch:
  curl -X POST https://mcp.shorashim.ch/mcp -d '{...initialize...}'
  returns {"result":{"serverInfo":{"name":"Gitea MCP Server"}}}

Also: nginx-ingress configmap needed strict-validate-path-type=false
to allow cert-manager HTTP-01 challenges. Applied via kubectl patch,
not tracked in git manifests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 14:53:30 -04:00
Scott KarpandClaude Opus 4.6 20e81114a5 Fix gitea-mcp deployment: add explicit command path
The gitea-mcp-server image has no entrypoint set (null).
Args were being interpreted as the executable name.
Added command: ["/app/gitea-mcp"] to invoke the binary correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 22:21:13 -04:00
Scott KarpandClaude Opus 4.6 c7e0e6e703 Gitea MCP connector: K8s manifests for public claude.ai integration
Shemot 28:3 — v'asu et bigdei Aharon l'kadesho l'chahano li.
The garments connect the service to those who approach.

- gitea-mcp-deployment.yaml: Official gitea-mcp server in HTTP mode
  Connects to Gitea at 185.19.29.94 via PAT
- gitea-mcp-ingress.yaml: TLS ingress at mcp.shorashim.ch
  cert-manager auto-provisions Let's Encrypt certificate
- gitea-mcp-secret.yaml: Template for Gitea PAT (never committed with value)
- cert-manager-issuer.yaml: Let's Encrypt ClusterIssuer

Architecture: claude.ai → OAuth → mcp.shorashim.ch → Gitea API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:02:45 -04:00
Scott KarpandClaude Opus 4.6 2f1205eef1 Bake L1 Torah into container images, simplify K8s deployment
L1 (29MB, 5860 files) is read-only and immutable — it belongs
inside the image, not on a separate volume. The scroll is sealed
in the aron. It ships with the vessel.

- All 4 Dockerfiles: COPY l1/ /mnt/torah/ in runtime stage
- deployment.yaml: Removed L1 PVC volume and all L1 volumeMounts
- deployment.yaml: Added imagePullSecrets for registry auth
- deployment.yaml: Manna uses emptyDir (kohen writes via kubectl cp)
- No CSI driver needed — eliminates Exoscale block storage dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 13:16:31 -04:00
Scott KarpandClaude Opus 4.6 5e76c60218 Update K8s deployment with Exoscale container registry URLs
All 4 images now reference exo.container-registry.com/the-recognition-protocol/:
- vav-pipeline:latest (111MB)
- shoresh-engine:latest (128MB)
- talmudic-engine:latest (292MB)
- korban-processor:latest (187MB)

Images built and pushed. The garments are in the registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 21:24:15 -04:00
Scott KarpandClaude Opus 4.6 edb74da0b4 Fix shoresh engine Dockerfile: Debian Buster repos are EOL
haskell:9.6.4-slim is based on Debian Buster, which reached EOL.
The standard deb.debian.org repos return 404. Redirected to
archive.debian.org for zlib1g-dev installation.

Verified: shoresh-engine image builds successfully (128MB).

All 4 Docker images now build:
  vav-pipeline:      111MB
  shoresh-engine:    128MB
  korban-processor:  187MB
  talmudic-engine:   292MB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 17:30:31 -04:00
Scott KarpandClaude Opus 4.6 f32e9afb03 Fix korban processor Dockerfile: pyswip 0.2.11 does not exist
pyswip 0.2.11 was never published to PyPI. Latest 0.2.x is 0.2.10.
Changed pin from ==0.2.11 to ==0.2.10.

Verified: korban-processor image builds successfully (187MB).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 17:25:23 -04:00
Scott KarpandClaude Opus 4.6 fe93c11d13 Phase 5 deployment: Dockerfiles, K8s manifests, halt monitor (Steps 33, 36)
Shemot 40:33 — Vay'khal Moshe et ham'lakhah.
The builder completes the work. The vessel is ready for transport.

Step 33 — Deployment configuration:
- Dockerfiles for all 4 components:
  - vav_pipeline: Elixir 1.16.1/OTP 26.2.1 release with Sanhedrin
  - shoresh_engine: GHC 9.6.4 binary
  - talmudic_engine: SWI-Prolog 9.2.2
  - korban_processor: Python 3.12.2 with pydantic
- Kubernetes manifests for Exoscale SKS (ch-gva-2):
  - Namespace, ConfigMap, Secrets template
  - L1 Torah PV/PVC (read-only mount)
  - Manna provision PVC (kohen writes, system reads)
  - Deployment: vav-pipeline + 3 sidecars, shared volumes
  - Service: ClusterIP + LoadBalancer with Exoscale NLB
  - Init container runs Ecto migrations (VavPipeline.Release)

Step 36 — Halt monitor (esh tamid):
- GenServer watches for three cardinal sins
- halt!/3 immediately halts system, sounds Chatzotzrot war signal
- Permanent audit log — halt events never deleted
- resolve/1 requires explicit operator action to resume
- alive?/0 for morning tamid verification
- 13 tests: alive check, 3 halt types, resolution, audit log,
  chatzotzrot war signal integration
- Added to supervision tree

All 219 Elixir + 19 L3 + 195 Prolog + 33 Haskell = 447+ tests, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 07:11:02 -04:00