Skip to content

example: Shen at the edge — Envoy fronting the two OpenResty apps - #54

Merged
pyrex41 merged 4 commits into
mainfrom
example/envoy-edge
Aug 11, 2026
Merged

example: Shen at the edge — Envoy fronting the two OpenResty apps#54
pyrex41 merged 4 commits into
mainfrom
example/envoy-edge

Conversation

@pyrex41

@pyrex41 pyrex41 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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:

  1. ext_authz → the authz app. Envoy forwards every API request (method + path + authorization header, 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.
  2. An Envoy Lua filter → rules.shen inside the proxy. Envoy's Lua filter embeds LuaJIT — shen-lua's primary host — so filter.lua boots 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.shen enforced on four hosts from one typed source: browser (ShenScript, shaken), edge (shen-lua on Envoy's LuaJIT), origin (shen-lua on OpenResty), and plain luajit in the selftests.

Changes to the authz example

  • app.shen: a (route "CHECK" Path ...) route speaking ext_authz's http_service protocol — same authorize-* gate, same host-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 projection check-response — total over decision, and no shape of decision places 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 content
  • filter.lua — the edge validator (only the pure typed core belongs in-proxy: per-thread Lua states, no shared dicts/cosockets, httpCall only)
  • authz.conf — the authz app as ext_authz backend on :8081, seeding a guestbook resource
  • selftest.lua — the whole three-layer pipeline under plain luajit, in Envoy's exact filter order, asserting which layer answers each request
  • README.md — architecture, run instructions, and verified deployment mechanics

Testing

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):

  • proof-chain denials at the edge with discharge reports: no token / tok-carol (not a member) / tok-bob POST (viewer needs editor) → 403 from ext_authz
  • typed rules inside the proxy: tok-alice with a bad body → 400 {"errors":["name: is required"]} with x-shen-edge: rejected + server: envoy (proving the origin was never touched); blank-message and broken-JSON variants likewise
  • the valid entry survives both gates → 201, stored in the real guestbook, read back
  • revocation via the admin API flips the edge's answer to access to this resource was revoked
  • static route bypass (per-route filter disable) serves the front end
  • the durable audit log on :8081 contains every edge decision
  • steady-state latency through the full chain: 3–6 ms per request; ~3 s to serving with a warm cache

Two 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 local luajit otherwise rewrite each other's shared default cache.
  • SHEN_JIT=off on 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

Reuben Brooks and others added 3 commits August 11, 2026 07:57
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>
@pyrex41

pyrex41 commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

The two operational findings are now fixed in boot.lua rather than documented as env-var workarounds (commit 2ef93b7):

  1. Per-build bytecode caches. The default cache path is now .shen-kernel-cache.<build-hash>.bin, hashed from jit.version .. jit.arch — the same fingerprint the content key already used. Local luajit, OpenResty's LuaJIT, and Envoy's LuaJIT each keep their own warm cache instead of invalidating each other's; writing a new default-path cache removes the obsolete shared-path file. Verified with three different LuaJIT builds coexisting in one cwd.

  2. Auto-fallback when the JIT can't compile. Boot now probes the JIT directly — compile one throwaway hot loop under a jit.attach trace watcher — and turns the JIT off (with a stderr note) if no trace materializes. This catches hosts that report jit.status() == true but are denied executable memory (macOS hardened-runtime binaries embedding LuaJIT, like Envoy). SHEN_JIT=on skips the probe; healthy hosts compile the one probe trace and are unchanged.

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 luajit, whose JITs are healthy. Kernel test suite ok, all three example selftests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pyrex41
pyrex41 merged commit 12fab4b into main Aug 11, 2026
2 checks passed
@pyrex41
pyrex41 deleted the example/envoy-edge branch August 11, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant