example: Shen at the edge — Envoy fronting the two OpenResty apps - #54
Conversation
examples/envoy/ puts Envoy in front of the guestbook and authz examples, using both of its integration seams: - ext_authz -> the authz app: every edge request runs the Prolog proof chain before routing; denials return the discharge report and every edge decision lands in the same durable audit log. New in the authz app: a (route "CHECK" Path ...) route speaking Envoy's ext_authz http_service protocol, and a typed check-response projection in authz.shen (total over `decision`, structurally unable to include document content). - an Envoy Lua filter -> rules.shen inside the proxy: filter.lua boots shen-lua once per worker thread on Envoy's LuaJIT and rejects malformed guestbook POSTs at the edge with the origin's exact typed error strings. One rules.shen, four hosts (browser / edge / origin / selftests). selftest.lua drives the whole pipeline under plain luajit in Envoy's exact filter order (a faithful fake of Envoy's request_handle drives the real filter.lua; the real authz app decides; only the guestbook upstream is a stand-in). The authz selftest gains CHECK cases, still decision-for-decision identical across file/lmdb/cosocket substrates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stood the stack up against Homebrew Envoy 1.39 on an arm64 Mac and folded the findings into the docs: - SHEN_KERNEL_CACHE: the bytecode cache is keyed to the exact LuaJIT build, so Envoy's LuaJIT and the local luajit rewrite each other's cache under the shared default path; a dedicated path fixes it. - SHEN_JIT=off on macOS: the hardened runtime denies Envoy's binary executable trace memory, so LuaJIT thrashes on failed trace compiles (measured ~550x slowdown, 40-66 s kernel boots). Interpreted, the edge boots in ~3 s warm and serves in 3-6 ms. - FFI confirmed present in Envoy's LuaJIT (soa32 engine runs as-is). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mpile Fixes the two issues surfaced by running shen-lua inside Envoy's Lua filter (examples/envoy), both previously documented as manual env-var workarounds: 1. The default kernel cache path is now per Lua build (.shen-kernel-cache.<fnv1a(jit.version..jit.arch)>.bin). Bytecode is only portable within the exact build, so two hosts sharing the old single path (local luajit vs OpenResty's or Envoy's embedded LuaJIT) invalidated and rewrote each other's cache on every alternation, recompiling the kernel every time. Each build now keeps its own warm cache; writing a default-path cache removes the obsolete shared-path file; SHEN_KERNEL_CACHE still relocates or disables. Verified: three different LuaJIT builds coexisting, each warm-booting from its own file. 2. boot.lua now probes whether the JIT can actually materialize a trace (compile one throwaway hot loop under a jit.attach trace watcher). Hosts that report jit.status() == true but are denied executable memory — macOS hardened-runtime binaries embedding LuaJIT, e.g. Envoy — otherwise thrash on "failed to allocate mcode memory" re-recording aborts (measured ~550x on a probe loop; 40-66 s kernel boots). On a failed probe the JIT is turned off with a stderr note: measured 2 s cold / 1 s warm to serving, 1.5-3 ms edge requests. Healthy hosts (local luajit, OpenResty nginx) compile the one probe trace and are unchanged. SHEN_JIT=on skips the probe; SHEN_JIT=off still forces the interpreter. Kernel test suite: ok. All three example selftests pass. Envoy example re-verified live with NO env vars. Docs updated to match (the envoy README's env-var guidance now describes the automatic behavior). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The two operational findings are now fixed in
Re-verified live with no environment variables: Envoy cold-boots to serving in 2 s (was 40–66 s), warm-restarts in 1 s, requests at 1.5–3 ms; the probe fired inside Envoy (4 stderr notes, one per Lua state) and stayed silent under OpenResty and local |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
A new
examples/envoy/— the third piece of the web trilogy. Envoy (the standard edge/mesh proxy) goes in front of the guestbook (examples/openresty) and the authz app (examples/openresty-authz), using each of Envoy's two integration seams for what it's good at:ext_authz→ the authz app. Envoy forwards every API request (method + path +authorizationheader, no body) to the authz service before routing it. The Prolog proof chain decides; denials return the discharge report to the client verbatim; and every edge decision lands in the same durable audit log as a direct API call. No authorization code lives in the proxy.rules.sheninside the proxy. Envoy's Lua filter embeds LuaJIT — shen-lua's primary host — sofilter.luaboots the kernel once per worker thread and runs the guestbook's typed field rules in the proxy: malformed POSTs get their 400 at the edge, with the origin's exact typed error strings.That makes it one
rules.shenenforced on four hosts from one typed source: browser (ShenScript, shaken), edge (shen-lua on Envoy's LuaJIT), origin (shen-lua on OpenResty), and plainluajitin the selftests.Changes to the authz example
app.shen: a(route "CHECK" Path ...)route speaking ext_authz'shttp_serviceprotocol — sameauthorize-*gate, samehost-log, so edge decisions run the same proof chain and are durably audited. Unmapped paths fail closed (resource "" → denied "unknown resource").authz.shen: a new typed projectioncheck-response— total overdecision, and no shape ofdecisionplaces document content in a check response, so the gateway structurally cannot become a data leak (render-doc's dual).app.lua/nginx.conf: the glue maps/authz/<path>+Authorization: Bearer <tok>to the CHECK dispatch; a/authz/location.New files
envoy.yaml— ext_authz (fail-closed) + Lua filter + router; per-route disable for static contentfilter.lua— the edge validator (only the pure typed core belongs in-proxy: per-thread Lua states, no shared dicts/cosockets,httpCallonly)authz.conf— the authz app as ext_authz backend on :8081, seeding aguestbookresourceselftest.lua— the whole three-layer pipeline under plain luajit, in Envoy's exact filter order, asserting which layer answers each requestREADME.md— architecture, run instructions, and verified deployment mechanicsTesting
Selftests (plain luajit):
luajit examples/envoy/selftest.lua— passes (ext_authz + Lua filter + upstream; edge 400s match the origin's typed strings; all 13 edge decisions in the audit log)luajit examples/openresty-authz/selftest.lua— passes with new CHECK cases, decision-for-decision identical across file/lmdb/cosocket substrates (16 decisions)luajit examples/openresty/selftest.lua— passes (unchanged)Live, end to end (Envoy 1.39.0 via Homebrew + OpenResty, arm64 macOS — the full three-process setup from the README):
tok-carol(not a member) /tok-bobPOST (viewer needs editor) → 403 from ext_authztok-alicewith a bad body →400 {"errors":["name: is required"]}withx-shen-edge: rejected+server: envoy(proving the origin was never touched); blank-message and broken-JSON variants likewiseaccess to this resource was revokedTwo operational findings from the live run, now documented in the README and set in its run command:
SHEN_KERNEL_CACHE=<dedicated path>: the bytecode cache is keyed to the exact LuaJIT build, so Envoy's LuaJIT and the localluajitotherwise rewrite each other's shared default cache.SHEN_JIT=offon macOS: the hardened runtime denies Envoy's binary executable trace memory, so LuaJIT thrashes on failed trace compiles (measured ~550× on a probe loop; 40–66 s kernel boots). Interpreted: ~3 s boots, same single-digit-ms requests. Linux builds generally don't need it.🤖 Generated with Claude Code