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>
This commit is contained in:
1 parent
0572a2cffd
commit
8c221c2bcb
17 files changed
+672
-39
No files matched your search
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
The container entry point — `python -m korban_processor`.
|
||||
|
||||
deploy/k8s/deployment.yaml runs the korban-processor container with no
|
||||
`command:` override, so the image's CMD is what the cluster executes.
|
||||
Before korban_processor/src/korban_processor/__main__.py existed, that
|
||||
CMD exited immediately ("No module named korban_processor.__main__")
|
||||
and took the whole pod into CrashLoopBackOff. These tests hold the
|
||||
entry point to the three things it actually promises.
|
||||
|
||||
Vayikra 16:8 is the load-bearing one: goral echad l'YHVH v'goral echad
|
||||
la'Azazel. The two goats are provisioned together, from the same pool.
|
||||
A korban channel that comes up WITHOUT its disposal channel can accept
|
||||
an offering it has no lawful way to refuse, so the entry point must
|
||||
refuse to come up at all rather than serve half the architecture.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
KORBAN_SRC = REPO_ROOT / "korban_processor" / "src"
|
||||
AZAZEL_SRC = REPO_ROOT / "azazel_processor" / "src"
|
||||
|
||||
|
||||
def _run_entrypoint(pythonpath, stop_after=6.0):
|
||||
"""
|
||||
Launch `python -m korban_processor` with an explicit PYTHONPATH.
|
||||
|
||||
Returns (returncode, combined_output). If the process is still
|
||||
running when we are done waiting, it is SIGTERMed — which is also
|
||||
how Kubernetes stops it, so the shutdown path is exercised here
|
||||
rather than only in production.
|
||||
"""
|
||||
env = {**os.environ, "PYTHONPATH": os.pathsep.join(str(p) for p in pythonpath)}
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "korban_processor"],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
cwd=str(REPO_ROOT),
|
||||
)
|
||||
try:
|
||||
out = proc.communicate(timeout=2.0)[0]
|
||||
return proc.returncode, out
|
||||
except subprocess.TimeoutExpired:
|
||||
# Still up — that is the healthy idle case. Stop it the way
|
||||
# the cluster would.
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
out = proc.communicate(timeout=stop_after)[0]
|
||||
return proc.returncode, out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# It comes up, stays up, and stops clean
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_entrypoint_stays_up_and_exits_zero_on_sigterm():
|
||||
code, out = _run_entrypoint([KORBAN_SRC, AZAZEL_SRC])
|
||||
|
||||
assert code == 0, f"expected clean exit on SIGTERM, got {code}:\n{out}"
|
||||
assert "SIGTERM" in out
|
||||
assert "stopped" in out
|
||||
|
||||
|
||||
def test_entrypoint_reports_itself_idle_rather_than_ready_to_serve():
|
||||
"""
|
||||
korban_processor is a library. Until Architecture Step 22 wires an
|
||||
external-model consumer, the honest report is IDLE. A container
|
||||
that claims to be serving when nothing can reach it is the failure
|
||||
this test exists to prevent.
|
||||
"""
|
||||
_, out = _run_entrypoint([KORBAN_SRC, AZAZEL_SRC])
|
||||
|
||||
assert "IDLE" in out
|
||||
assert "Step 22" in out
|
||||
|
||||
|
||||
def test_entrypoint_names_the_offering_protocol_in_order():
|
||||
_, out = _run_entrypoint([KORBAN_SRC, AZAZEL_SRC])
|
||||
|
||||
order = ["semichah", "shechitah", "zerikah", "haktarah", "route"]
|
||||
positions = [out.index(stage) for stage in order]
|
||||
assert positions == sorted(positions), f"protocol stages out of order:\n{out}"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Vayikra 16:8 — it refuses to run without the disposal channel
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_entrypoint_refuses_to_start_without_azazel_processor():
|
||||
code, out = _run_entrypoint([KORBAN_SRC]) # azazel deliberately absent
|
||||
|
||||
assert code == 1, f"expected exit 1 without azazel_processor, got {code}:\n{out}"
|
||||
assert "FAILED TO START" in out
|
||||
assert "azazel_processor" in out
|
||||
assert "BUILD_AZAZEL.md" in out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Form 1 — the entry point opens no bypass to the external model
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_entrypoint_does_not_reach_the_model_interface():
|
||||
"""
|
||||
Companion to test_form_1_architecture.py invariant 3. The entry
|
||||
point runs before anything else in the container and must not be
|
||||
the module that hands a caller call_claude without the offering
|
||||
protocol.
|
||||
"""
|
||||
source = (KORBAN_SRC / "korban_processor" / "__main__.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "model_interface" not in source
|
||||
assert "call_claude" not in source
|
||||
Reference in new issue
Block a user