Security hardening and release readiness

ober

43f0fef381fd099b3b19c4fe3b9b33c1ee7596f7

diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..ac2dfef
--- /dev/null
+++ b/.github/workflows/security-baseline.yml
@@ -0,0 +1,39 @@
+name: CI
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  verify:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install build tools
+        run: sudo apt-get update && sudo apt-get install -y build-essential curl ca-certificates git ripgrep pkg-config
+
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Install cargo-audit
+        run: cargo install cargo-audit --locked
+
+      - name: Install jerbuild
+        run: |
+          set -eux
+          curl -fsSL "https://github.com/jerboa-lang/jerboa/releases/download/${JERBOA_VERSION}/jerbuild-linux-x86_64" -o /usr/local/bin/jerbuild
+          chmod +x /usr/local/bin/jerbuild
+        env:
+          JERBOA_VERSION: v0.2.3
+
+      - name: Verify
+        run: JERBUILD=/usr/local/bin/jerbuild make verify
+
+      - name: Release evidence
+        run: JERBUILD=/usr/local/bin/jerbuild make release-evidence
diff --git a/.gitignore b/.gitignore
index 1cdf515..03e1c73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,15 @@
 # Build artifacts (jerbuild build)
 /jerboa-edge
+dist/
+.build/
 vendor/
+/.jerboa/jerboa-native-rs/
 /*-main.c
 support/ffi-symbols.gen
 *.so
 *.wpo
+*.o
+*.a
 *.boot
 petite_boot.h
 scheme_boot.h
diff --git a/.jerboa/security.json b/.jerboa/security.json
index 055c2df..7c0f6c2 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -3,20 +3,28 @@
   "repo": "jerboa-edge",
   "extends": ["jerboa:daemon", "jerboa:ffi", "jerboa:network-service"],
   "paths": {
-    "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "src/**/*.{ss,sls,c,h,rs}", "native/**/*.{c,h,rs}", "Makefile"],
+    "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "src/**/*.{ss,sls,c,h,rs}", "native/**/*.{c,h,rs}", "support/**/*.{c,h,ss,sh}", "Makefile", ".jerbuild"],
     "tests": ["test/**", "tests/**", "**/*-test.ss"],
     "generated": ["build/**", "dist/**", "target/**", "*.so", "*.dylib", "*.wpo"],
     "vendor": ["vendor/**", "third_party/**"],
     "docs": ["README.md", "docs/**", "*.md"]
   },
   "policy": {
-    "failOn": ["critical", "high"],
-    "imports": { "directChezscheme": "allow-in-ffi-boundaries" },
-    "ffi": { "allowed": true, "requireDynamicWindCleanup": true },
+    "failOn": ["critical", "high", "medium"],
+    "imports": { "directChezscheme": "deny-in-project-source" },
+    "ffi": { "allowed": true, "nativeFeatures": ["crypto", "wasm"], "denyFeatures": ["tls"], "requireDynamicWindCleanup": true },
     "process": { "shellInterpolation": "deny" },
-    "network": { "requireTimeouts": true, "requireFailClosedSandbox": true, "requireMtlsForAdmin": true },
+    "filesystem": { "allowedWritePaths": ["EDGE_DB_PATH", "temporary embedded-program file"], "requireDocumentedPaths": true },
+    "network": { "ingress": "plain-http-behind-external-mtls", "requireTimeouts": true, "requireFailClosedSandbox": true, "requireMtlsForAdmin": true },
+    "releaseEvidence": {
+      "linuxRuntime": "current-smoke-recorded-or-target-proof-required",
+      "sustainedLoad": "release-host-sustained-recorded-or-target-proof-required",
+      "proofFiles": ["JEDGE_TARGET_RUNTIME_PROOF_FILE", "JEDGE_TARGET_LOAD_PROOF_FILE"],
+      "failClosedProofFlags": ["JEDGE_REQUIRE_TARGET_RUNTIME_PROOF", "JEDGE_REQUIRE_TARGET_LOAD_PROOF"]
+    },
     "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false },
-    "auth": { "rejectUnsignedWebhooks": true }
+    "auth": { "rejectUnsignedWebhooks": true, "allowUnsignedOnlyWithExplicitDevFlag": "EDGE_ALLOW_UNSIGNED" }
   },
+  "runtimeTargets": ["linux"],
   "suppressions": []
 }
diff --git a/.jerbuild b/.jerbuild
index 897032f..ab83aa6 100644
--- a/.jerbuild
+++ b/.jerbuild
@@ -1,8 +1,10 @@
 ;; Build jerboa-edge with a standalone jerbuild binary: `jerbuild build`.
 ;;
-;; jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust
-;; crate, so this needs only jerbuild + a C compiler + cargo — no jerboa
-;; source checkout. jerboa-native provides rustls TLS and crypto.
+;; The Makefile copies jerbuild's bundled jerboa-native-rs source into
+;; .jerboa/jerboa-native-rs before invoking `jerbuild build`; the crate then
+;; builds under this repo instead of the shared jerbuild cache target.
+;; jerboa-native provides HMAC/constant-time crypto and WASM-backed restricted
+;; execution.
 
 (entry "edge.ss")
 (output "jerboa-edge")
@@ -14,6 +16,6 @@
 (main-c "support/static-main.c")
 (ffi-symbols "support/ffi-symbols.gen")
 (rust-crates
-  ("@bundle/jerboa-native-rs/Cargo.toml"
-   features: "tls,crypto"
+  (".jerboa/jerboa-native-rs/Cargo.toml"
+   features: "crypto,wasm"
    no-default-features: #t))
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
index ac69af8..a7a398e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,31 @@
 # jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust
-# crate, so building jerboa-edge needs only `jerbuild`, a C compiler, and
-# cargo — no jerboa source checkout and no separately-built Chez/native lib.
+# crate. The Makefile copies that native crate into .jerboa/jerboa-native-rs
+# before building so the daemon's native archive is repo-local and feature
+# isolated.
 JERBUILD ?= jerbuild
 JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
 ifeq ($(JH),)
 $(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild)
 endif
 
-NATIVE_A := $(JH)/jerboa-native-rs/target/release/libjerboa_native.a
+JERBOA_NATIVE_BUNDLE_DIR := $(JH)/jerboa-native-rs
+NATIVE_DIR ?= $(CURDIR)/.jerboa/jerboa-native-rs
+NATIVE_A ?= $(NATIVE_DIR)/target/release/libjerboa_native.a
+NATIVE_FEATURES ?= crypto,wasm
+NATIVE_NO_DEFAULT_FEATURES ?= 1
 BIN      := jerboa-edge
 BIN_DIR  := $(HOME)/.local/bin
 VENDOR   := $(CURDIR)/vendor
 JSQLITE_REPO ?= $(VENDOR)/jsqlite
 JSQLITE_URL  ?= https://git.sr.ht/~lisp/jsqlite
+LIBDIRS  := .:$(JH)/lib:$(JSQLITE_REPO)/src
+EVIDENCE_DIR ?= dist/release-evidence
+SBOM_DIR ?= dist/sbom
+REPRO_DIR ?= dist/reproducibility
+SOAK_DIR ?= dist/soak
+SMOKE_PORT ?= $(shell python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()' 2>/dev/null || echo 18080)
 
-.PHONY: all build binary install clean ensure-jsqlite
+.PHONY: all build binary run install clean clean-generated ensure-jsqlite ensure-native-src import-check security audit smoke sbom reproducibility-report soak-evidence verify release-evidence
 
 all: binary
 
@@ -25,24 +36,135 @@ ensure-jsqlite:
 	fi
 	@test -f "$(JSQLITE_REPO)/src/jsqlite/api.ss"
 
+ensure-native-src:
+	@test -f "$(JERBOA_NATIVE_BUNDLE_DIR)/Cargo.toml" || { \
+	  echo "ERROR: bundled jerboa-native-rs missing at $(JERBOA_NATIVE_BUNDLE_DIR)" >&2; \
+	  exit 1; \
+	}
+	rm -rf "$(NATIVE_DIR)"
+	mkdir -p "$(dir $(NATIVE_DIR))"
+	(cd "$(JH)" && tar --exclude='jerboa-native-rs/target' --exclude='jerboa-native-rs/.git' -cf - jerboa-native-rs) | tar -xf - -C "$(dir $(NATIVE_DIR))"
+
 # Standalone native binary via .jerbuild (entry edge.ss -> jerboa-edge).
-# Two passes: the first cargo-builds jerboa-native into jerbuild's cache; we
-# then regenerate the FFI symbol list from that archive (so it matches this
+# Two passes: the first cargo-builds the repo-local jerboa-native copy; we then
+# regenerate the FFI symbol list from that archive (so it matches this
 # platform — e.g. epoll on Linux) and relink. A static main.c sets
 # JERBOA_STATIC=1 so the std modules use the registered symbols, not dlopen.
-binary: ensure-jsqlite
+binary: ensure-jsqlite ensure-native-src
 	@: > support/ffi-symbols.gen
 	$(JERBUILD) build
-	sh support/gen-ffi-symbols.sh
+	JERBUILD="$(JERBUILD)" NATIVE_A="$(NATIVE_A)" sh support/gen-ffi-symbols.sh
 	$(JERBUILD) build
 
 build: binary
 
+run: binary
+	./$(BIN)
+
 install: binary
 	mkdir -p $(BIN_DIR)
 	install -m 0755 $(BIN) $(BIN_DIR)/$(BIN)
 	@echo "Installed $(BIN) to $(BIN_DIR)/$(BIN)"
 
+import-check: ensure-jsqlite
+	$(JERBUILD) exec --libdirs "$(LIBDIRS)" support/import-check.ss
+
+security: clean-generated
+	bash scripts/security-check.sh
+
+audit: security ensure-native-src
+	@echo "==> jerbuild"
+	@$(JERBUILD) --version
+	@echo "==> C compiler"
+	@cc --version 2>/dev/null | head -1 || cc -v 2>&1 | head -1
+	@echo "==> Rust native bundle"
+	@if [ -f "$(NATIVE_DIR)/Cargo.toml" ]; then \
+		cargo metadata --format-version 1 --manifest-path "$(NATIVE_DIR)/Cargo.toml" >/dev/null; \
+	else \
+		echo "ERROR: native bundle missing at $(NATIVE_DIR); run the binary target before audit." >&2; \
+		exit 1; \
+	fi
+	@if cargo audit --version >/dev/null 2>&1; then \
+		cargo audit --file "$(NATIVE_DIR)/Cargo.lock" -D warnings; \
+	else \
+		echo "ERROR: cargo audit is not installed; install cargo-audit for the native dependency audit gate." >&2; \
+		exit 1; \
+	fi
+
+smoke: binary
+	@set -eu; \
+	if [ "$$(uname -s)" != "Linux" ]; then \
+		echo "smoke: skipped on $$(uname -s); static fiber HTTP runtime requires Linux epoll symbols and runs in CI/release Linux"; \
+		exit 0; \
+	fi; \
+	log="dist/jerboa-edge-smoke.log"; \
+	mkdir -p dist; \
+	EDGE_PORT="$(SMOKE_PORT)" EDGE_ALLOW_UNSIGNED=1 ./$(BIN) > "$$log" 2>&1 & \
+	pid=$$!; \
+	trap 'kill $$pid >/dev/null 2>&1 || true; wait $$pid >/dev/null 2>&1 || true' EXIT INT TERM; \
+	ok=0; \
+	for i in $$(seq 1 50); do \
+		if curl -fsS "http://127.0.0.1:$(SMOKE_PORT)/health" >/dev/null 2>&1; then ok=1; break; fi; \
+		sleep 0.2; \
+	done; \
+	test "$$ok" = 1; \
+	curl -fsS "http://127.0.0.1:$(SMOKE_PORT)/health" | grep -q '"status":"ok"'; \
+	code=$$(curl -sS -o /dev/null -w '%{http_code}' -X POST "http://127.0.0.1:$(SMOKE_PORT)/hooks/smoke" -H 'Content-Type: application/json' -d '{"id":"smoke_1","amount":1}'); \
+	test "$$code" = 202; \
+	echo "smoke: ok"
+
+sbom: binary
+	@REPO_ROOT="$(CURDIR)" \
+	SBOM_DIR="$(SBOM_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	JERBOA_HOME="$(JH)" \
+	NATIVE_DIR="$(NATIVE_DIR)" \
+	NATIVE_A="$(NATIVE_A)" \
+	NATIVE_FEATURES="$(NATIVE_FEATURES)" \
+	NATIVE_NO_DEFAULT_FEATURES="$(NATIVE_NO_DEFAULT_FEATURES)" \
+	BIN="$(BIN)" \
+	sh scripts/sbom.sh
+
+reproducibility-report: ensure-jsqlite
+	@REPO_ROOT="$(CURDIR)" \
+	REPRO_DIR="$(REPRO_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	MAKE="$(MAKE)" \
+	BIN="$(BIN)" \
+	NATIVE_A="$(NATIVE_A)" \
+	sh scripts/reproducibility-report.sh
+
+soak-evidence:
+	@REPO_ROOT="$(CURDIR)" \
+	SOAK_DIR="$(SOAK_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	MAKE="$(MAKE)" \
+	SMOKE_PORT="$(SMOKE_PORT)" \
+	sh scripts/soak-evidence.sh
+
+verify: security audit import-check smoke
+
+release-evidence:
+	@REPO_ROOT="$(CURDIR)" \
+	EVIDENCE_DIR="$(EVIDENCE_DIR)" \
+	SBOM_DIR="$(SBOM_DIR)" \
+	REPRO_DIR="$(REPRO_DIR)" \
+	SOAK_DIR="$(SOAK_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	JERBOA_HOME="$(JH)" \
+	NATIVE_DIR="$(NATIVE_DIR)" \
+	NATIVE_A="$(NATIVE_A)" \
+	NATIVE_FEATURES="$(NATIVE_FEATURES)" \
+	NATIVE_NO_DEFAULT_FEATURES="$(NATIVE_NO_DEFAULT_FEATURES)" \
+	sh scripts/release-evidence.sh
+
+clean-generated:
+	rm -f $(BIN) $(BIN)-main.c support/ffi-symbols.gen
+	rm -f dist/jerboa-edge-smoke.log
+	find . \( -name '*.so' -o -name '*.wpo' -o -name '*.o' -o -name '*.a' \) -delete 2>/dev/null || true
+
 clean:
-	rm -f $(BIN) $(BIN)-main.c
-	find . \( -name '*.so' -o -name '*.wpo' \) -delete 2>/dev/null || true
+	rm -rf dist
+	rm -rf .jerboa/jerboa-native-rs
+	rm -f $(BIN) $(BIN)-main.c support/ffi-symbols.gen
+	find . \( -name '*.so' -o -name '*.wpo' -o -name '*.o' -o -name '*.a' \) -delete 2>/dev/null || true
diff --git a/README.md b/README.md
index bfe8ce2..aeea925 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,77 @@
 # jerboa-edge
+
+`jerboa-edge` is a Jerboa webhook processing daemon. It accepts plain HTTP
+from a trusted local or private-network ingress, verifies webhook HMACs, runs
+event filters and hot handlers in Jerboa restricted execution, persists optional
+state through jsqlite, and exposes health, metrics, event query, dead-letter,
+and dashboard endpoints.
+
+This daemon is not its own internet TLS endpoint. Production deployments must
+put it behind an external mTLS terminator or service mesh and expose the daemon
+only on a private listener.
+
+## Build
+
+```sh
+JERBUILD=/path/to/jerbuild make build
+```
+
+The static binary is written to `./jerboa-edge`. The build uses Jerboa native
+features `crypto,wasm`; TLS native code is intentionally not linked.
+
+Production runtime target is Linux. The static fiber HTTP server uses Jerboa's
+Linux epoll-native backend; macOS can build the binary but does not provide the
+runtime epoll symbols needed to execute it.
+
+## Run
+
+```sh
+EDGE_SECRET='replace-with-webhook-secret' \
+EDGE_PORT=8080 \
+EDGE_DB_PATH=/var/lib/jerboa-edge/events.db \
+./jerboa-edge
+```
+
+`EDGE_SECRET` is required for production webhook ingestion. If it is missing,
+unsigned webhook requests fail closed. Local development can opt in to unsigned
+requests with `EDGE_ALLOW_UNSIGNED=1`.
+
+Useful endpoints:
+
+- `POST /hooks/:type`
+- `GET /health`
+- `GET /metrics`
+- `GET /api/stats`
+- `GET /api/events/:id`
+- `GET /api/dead-letter`
+- `POST /api/dead-letter/:id/replay`
+- `GET /dashboard`
+
+## Security
+
+Release gates:
+
+```sh
+JERBUILD=/path/to/jerbuild make verify
+JERBUILD=/path/to/jerbuild make release-evidence
+```
+
+Release evidence includes Linux runtime/load status under
+`dist/release-evidence/soak/`. On non-Linux hosts this records the runtime and
+load gates as blocked; production release requires Linux release-host smoke and
+sustained load evidence. A Linux release host can record sustained load with
+`JEDGE_RUN_RELEASE_SOAK=1 JEDGE_RUN_PRODUCTION_LOAD=1`; externally captured
+runtime/load proof can be attached with `JEDGE_TARGET_RUNTIME_PROOF_FILE` and
+`JEDGE_TARGET_LOAD_PROOF_FILE`.
+
+The security posture is documented in:
+
+- `SECURITY.md`
+- `docs/threat-model.md`
+- `docs/deployment-security.md`
+- `docs/ffi-boundary.md`
+- `docs/release-evidence.md`
+
+## License
+
+Apache-2.0, matching the Jerboa and Chez Scheme license posture. See `LICENSE`.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..4da8883
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,43 @@
+# Security Policy
+
+`jerboa-edge` is a network daemon and must be treated as hostile-input-facing
+software. Production releases require the gates in `~/Release-plan.md`,
+`~/mine/jerboa-production-readiness.md`, and this repository to pass.
+
+## Supported Status
+
+No public production-support commitment exists until a release is cut with
+`make release-evidence` from a clean checkout and the generated evidence is
+reviewed.
+
+Required gates:
+
+- `make verify` passes with the intended `JERBUILD`.
+- Medium-or-higher Jerboa security scanner findings are zero.
+- `EDGE_SECRET` is configured in production.
+- The daemon is reachable only behind external mTLS or an equivalent authenticated ingress.
+- `EDGE_ALLOW_UNSIGNED` is unset in production.
+- Persistence paths, credentials, and service account permissions are documented by the deployer.
+- Dependency and native-code audit output is clean, or accepted risk is recorded in release notes.
+- Release evidence includes explicit Linux runtime and sustained load status.
+  Production evidence must record `linux_runtime_status=current-smoke-recorded`
+  from a Linux host or a required `target_runtime_proof_status=present` proof
+  file, and `load_status=release-host-sustained-recorded` or a required
+  `target_load_proof_status=present` proof file. Any `blocked-*`, `failed`, or
+  `blocked-not-run` runtime/load status is a production blocker.
+
+## Production Rules
+
+- Do not expose `jerboa-edge` directly to the public internet.
+- Do not enable the development unsigned-webhook mode in production.
+- Do not commit secrets, private keys, tokens, production `.env` files, hostnames, customer data, or database files.
+- Do not add in-process TLS without a threat model update, scanner coverage, and cleanup tests for native handles.
+- Do not add broad direct `(chezscheme)` imports in project source.
+- Do not add string-built SQL, shell interpolation, or bare Scheme `read` on untrusted input.
+- Keep restricted user filters and hot handlers behind Jerboa restricted execution and timeouts.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After a
+public release, replace this section with a dedicated advisory contact,
+supported versions, and disclosure window.
diff --git a/docs/deployment-security.md b/docs/deployment-security.md
new file mode 100644
index 0000000..6251c8b
--- /dev/null
+++ b/docs/deployment-security.md
@@ -0,0 +1,63 @@
+# Deployment Security
+
+## Network
+
+Run `jerboa-edge` behind an external mTLS terminator, service mesh sidecar, or
+reverse proxy that enforces client authentication, request size limits, rate
+limits, access logging, and TLS policy. Bind the daemon to a private interface
+or loopback listener.
+
+The daemon intentionally does not link Jerboa native TLS support. This avoids a
+second in-process HTTP/TLS parser and keeps TLS certificate/key material out of
+the Scheme process.
+
+Production runtime target is Linux. The static fiber HTTP server uses Jerboa's
+epoll-native event loop, so Linux release/CI smoke tests are required before
+promotion. Sustained load evidence must also be recorded for the release host
+with `JEDGE_RUN_RELEASE_SOAK=1 JEDGE_RUN_PRODUCTION_LOAD=1`, or attached as
+reviewable target proof with `JEDGE_TARGET_RUNTIME_PROOF_FILE` and
+`JEDGE_TARGET_LOAD_PROOF_FILE`. Local non-Linux evidence keeps the runtime and
+load gates blocked unless target proof files are supplied and hashed into the
+bundle.
+
+## Authentication
+
+Set `EDGE_SECRET` in production. Webhook requests without a valid `x-signature`
+are rejected when a secret is configured. If `EDGE_SECRET` is absent, unsigned
+webhooks are rejected unless `EDGE_ALLOW_UNSIGNED=1` is explicitly set for local
+development.
+
+Do not set `EDGE_ALLOW_UNSIGNED` in production.
+
+## Persistence
+
+`EDGE_DB_PATH` enables jsqlite persistence. The deployer should place this path
+on a dedicated directory owned by the service account with no world-readable
+permissions. Backups and logs containing payload data should follow the same
+data handling policy as webhook payloads.
+
+## Process Sandboxing
+
+Use a dedicated service account. For systemd, prefer:
+
+- `NoNewPrivileges=true`
+- `PrivateTmp=true`
+- `ProtectSystem=strict`
+- `ProtectHome=true`
+- `ReadWritePaths=` limited to the `EDGE_DB_PATH` directory
+- `RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX`
+- memory and file-descriptor limits sized for expected load
+
+## Operational Checks
+
+Before promoting a build:
+
+```sh
+JERBUILD=/path/to/jerbuild make verify
+JERBUILD=/path/to/jerbuild make release-evidence
+```
+
+Keep generated `dist/release-evidence` with the release record.
+The bundle must include `dist/release-evidence/soak/status.txt`. A
+`load_status=blocked-not-run`, `linux_runtime_status=blocked-*`, or
+`status=blocked-target-proof` entry is a production blocker.
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
new file mode 100644
index 0000000..03d5fb8
--- /dev/null
+++ b/docs/ffi-boundary.md
@@ -0,0 +1,37 @@
+# FFI Boundary
+
+`jerboa-edge` builds a standalone jerbuild binary with a custom C launcher and
+the bundled Jerboa native Rust archive.
+
+## Native Features
+
+The `.jerbuild` configuration enables only:
+
+- `crypto`: HMAC-SHA256 and constant-time comparison.
+- `wasm`: Jerboa restricted execution support.
+
+The TLS feature is intentionally disabled. Production TLS and client
+authentication must be supplied by external ingress.
+
+## Static Launcher
+
+`support/static-main.c` embeds the Scheme program boot data in a temporary file
+and invokes Chez through `Sscheme_program`. The launcher:
+
+- sets `JERBOA_STATIC=1` so Jerboa uses linked native symbols,
+- uses `mkstemp` for the temporary program path,
+- writes boot data with a checked `fwrite_all` loop,
+- closes with checked `fclose`,
+- unlinks the temporary file after the Scheme program starts.
+
+## FFI Symbol List
+
+`support/gen-ffi-symbols.sh` regenerates `support/ffi-symbols.gen` from the
+actual platform archive after the first jerbuild pass. The file is ignored
+because it is generated and platform-specific.
+
+## SQLite Boundary
+
+`jsqlite` is vendored under `vendor/` during builds. Prepared statements are
+finalized with `dynamic-wind`, and database handles are closed on failed open
+and graceful shutdown.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..e0ece00
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,74 @@
+# Release Evidence
+
+`make release-evidence` writes evidence to `dist/release-evidence`.
+
+The directory includes:
+
+- git commit, status, and diff summary,
+- build environment and native feature summary,
+- security, audit, import-check, and smoke-test logs,
+- Cargo metadata for the bundled Jerboa native crate when available,
+- SBOM/dependency manifests for source, support scripts, vendored jsqlite,
+  the repo-local no-default `crypto,wasm` native crate copy, binary linkage,
+  and native artifact hashes,
+- repeated clean-build reproducibility reports for the standalone binary,
+  generated FFI symbol list, native archive, and source manifest,
+- `soak/` with Linux runtime smoke, sustained request-load status, optional
+  target proof files, and SHA-256 hashes of attached proof files. Local
+  non-Linux evidence records the Linux runtime smoke and load gates as blocked
+  instead of treating the macOS smoke skip as production-ready evidence,
+- binary linkage information,
+- SHA-256 hashes for the binary and source files.
+
+Release evidence is generated from the same gates used by CI:
+
+```sh
+JERBUILD=/path/to/jerbuild make verify
+JERBUILD=/path/to/jerbuild make release-evidence
+```
+
+On non-Linux developer hosts the smoke target records a skip because the static
+fiber HTTP runtime requires Linux epoll-native symbols. Production release
+evidence must include the Linux CI/release-host smoke result.
+
+To refresh runtime smoke evidence on a Linux release host:
+
+```sh
+JEDGE_RUN_RELEASE_SOAK=1 JERBUILD=/path/to/jerbuild make soak-evidence
+```
+
+That records `status=short-smoke-not-production` when the Linux smoke passes.
+To record sustained request-load evidence on that Linux host:
+
+```sh
+JEDGE_RUN_RELEASE_SOAK=1 \
+JEDGE_RUN_PRODUCTION_LOAD=1 \
+JEDGE_PRODUCTION_SOAK_MIN_REQUESTS=10000 \
+JERBUILD=/path/to/jerbuild \
+make soak-evidence
+```
+
+That records `load_status=release-host-sustained-recorded` only after the
+daemon accepts at least `JEDGE_PRODUCTION_SOAK_MIN_REQUESTS` sequential webhook
+requests with zero failed requests.
+
+If the Linux smoke or sustained load run is captured by another release-host
+job, attach that evidence to the bundle with:
+
+```sh
+JEDGE_TARGET_RUNTIME_PROOF_FILE=/path/to/linux-runtime-status.txt \
+JEDGE_TARGET_LOAD_PROOF_FILE=/path/to/linux-load-status.txt \
+JEDGE_REQUIRE_TARGET_RUNTIME_PROOF=1 \
+JEDGE_REQUIRE_TARGET_LOAD_PROOF=1 \
+make soak-evidence
+```
+
+The runtime proof must contain `linux_runtime_status=current-smoke-recorded`.
+The load proof must contain `load_status=release-host-sustained-recorded`.
+Supplied proof files are copied into `dist/soak/` and hashed. Missing, empty, or
+marker-mismatched required proof files fail closed with
+`status=blocked-target-proof`.
+
+The target writes evidence before returning failure. A nonzero exit with
+`release_evidence_status=blocked` is a release blocker, not an incomplete
+evidence bundle.
diff --git a/docs/threat-model.md b/docs/threat-model.md
new file mode 100644
index 0000000..8d098fe
--- /dev/null
+++ b/docs/threat-model.md
@@ -0,0 +1,46 @@
+# jerboa-edge Threat Model
+
+## Scope
+
+`jerboa-edge` receives webhooks from a trusted ingress, queues events, runs
+filters and handlers, stores optional SQLite-backed state, and exposes query,
+metrics, health, dead-letter, and dashboard endpoints.
+
+## Assets
+
+- Webhook payloads and event metadata.
+- `EDGE_SECRET` and any deployer-managed ingress credentials.
+- SQLite state at `EDGE_DB_PATH`.
+- Restricted filter and hot-handler code submitted through API endpoints.
+- Service availability and worker supervision state.
+
+## Trust Boundaries
+
+- External clients terminate at an mTLS ingress before reaching this daemon.
+- `jerboa-edge` listens on plain HTTP only inside the trusted runtime boundary.
+- HMAC verification authenticates webhook payloads when `EDGE_SECRET` is set.
+- User-provided filter and handler code crosses into Jerboa restricted execution.
+- jsqlite persistence crosses into a local filesystem/database boundary.
+- Jerboa native FFI is limited to `crypto,wasm` for this daemon.
+
+## Primary Threats And Controls
+
+- Unauthorized webhook submission: production requires `EDGE_SECRET`; missing secrets fail closed.
+- Accidental unsigned mode: `EDGE_ALLOW_UNSIGNED=1` is explicit development-only behavior and is checked by the security gate.
+- Public daemon exposure: production requires external mTLS or equivalent authenticated ingress.
+- Code execution through filters or hot handlers: code runs through `restricted-eval-string` with Jerboa restricted bindings and timeouts.
+- SQL injection: SQL statements are literals and event values are bound parameters.
+- SQLite handle leaks: prepared statements are finalized with `dynamic-wind`; database handles close on failed open and shutdown.
+- Native handle leaks: in-process TLS is removed; the remaining static launcher uses checked stdio writes and closes.
+- Worker crash loss: worker actors run under one-for-one supervision and failed events retry before dead-lettering.
+
+## Residual Risk
+
+- The daemon depends on the deployer to enforce mTLS, network ACLs, request size limits, rate limits, and process sandboxing.
+- Restricted execution is still hostile-input-facing and must be reviewed whenever new bindings are added.
+- The smoke test validates basic runtime behavior on Linux, not sustained load,
+  fuzzing, or full ingress policy. Release evidence must record explicit
+  Linux runtime status and sustained request-load status; target proof files
+  can be attached and hashed, but `load_status=blocked-not-run`,
+  `linux_runtime_status=blocked-*`, or `status=blocked-target-proof` remains a
+  production blocker.
diff --git a/edge.ss b/edge.ss
index 5c2c630..6c546fc 100644
--- a/edge.ss
+++ b/edge.ss
@@ -12,7 +12,7 @@
 ;;;   - Structured JSON logging (greppable by Loki/Splunk)
 ;;;   - Graceful SIGTERM shutdown (drain, flush jsqlite WAL)
 ;;;   - Optional jsqlite persistence (EDGE_DB_PATH)
-;;;   - Optional TLS termination (EDGE_TLS_CERT + EDGE_TLS_KEY)
+;;;   - Production deployment behind an external mTLS terminator
 ;;;
 ;;; Zero external dependencies. Everything is Jerboa stdlib.
 ;;;
@@ -22,25 +22,17 @@
 ;;;        curl localhost:8080/api/stats
 ;;;        curl localhost:8080/metrics
 
-(import (except (chezscheme) merge
-          make-hash-table hash-table?
-          sort sort! format printf fprintf
-          iota 1+ 1-
-          path-extension path-absolute?
-          with-input-from-string with-output-to-string
-          make-date partition
-          atom? meta)
-        (except (jerboa prelude)
-          make-time split cpu-count or-else
+(import (except (jerboa prelude)
+          cpu-count or-else
           json-object->string string->json-object write-json read-json
           tcp-write-string tcp-write tcp-read
           tcp-close tcp-accept tcp-listen tcp-connect
-          sqlite-open sqlite-close sqlite-exec sqlite-execute sqlite-query
-          sqlite-prepare sqlite-finalize sqlite-step sqlite-bind)
+          sqlite-open sqlite-close sqlite-finalize sqlite-exec sqlite-execute sqlite-query
+          sqlite-prepare sqlite-step sqlite-bind)
         (std net fiber-httpd)
         (std net fiber-ws)
         (except (std csp) go go-named)
-        (std csp clj)
+        (except (std csp clj) merge split)
         (std actor)
         (std stm)
         (std transducer)
@@ -51,16 +43,17 @@
         (std misc timeout)
         (std metrics)
         (std os signal)
+        (only (std error conditions) make-supervision-failure)
         (prefix (jsqlite api) japi:)
-        (prefix (jsqlite constants) jconst:)
-        (std net tls-rustls)
-        (std net tcp-raw))
+        (prefix (jsqlite constants) jconst:))
 
 (def sqlite-open japi:sqlite-open)
-(def sqlite-close japi:sqlite-close)
 (def sqlite-exec japi:sqlite-exec)
 (def sqlite-execute japi:sqlite-exec)
 
+(def (db-close-handle! db)
+  (japi:sqlite-close db)) ;; sqlite-finalize is handled before connection close
+
 (def (bind-sqlite-args! stmt vals)
   (let loop ([i 1] [vs vals])
     (unless (null? vs)
@@ -68,20 +61,26 @@
       (loop (+ i 1) (cdr vs)))))
 
 (def (sqlite-query db sql . vals)
-  (let ([stmt (japi:sqlite-prepare db sql)])
-    (bind-sqlite-args! stmt vals)
-    (let ([names (japi:sqlite-columns stmt)])
-      (let loop ([acc '()])
-        (let ([rc (japi:sqlite-step stmt)])
-          (cond
-            [(= rc jconst:SQLITE_ROW)
-             (loop (cons (map cons names (japi:sqlite-row stmt)) acc))]
-            [(= rc jconst:SQLITE_DONE)
-             (japi:sqlite-finalize stmt)
-             (reverse acc)]
-            [else
-             (japi:sqlite-finalize stmt)
-             (reverse acc)]))))))
+  (let ([stmt #f])
+    (dynamic-wind
+      (lambda () (void))
+      (lambda ()
+        (set! stmt (japi:sqlite-prepare db sql))
+        (bind-sqlite-args! stmt vals)
+        (let ([names (japi:sqlite-columns stmt)])
+          (let loop ([acc '()])
+            (let ([rc (japi:sqlite-step stmt)])
+              (cond
+                [(= rc jconst:SQLITE_ROW)
+                 (loop (cons (map cons names (japi:sqlite-row stmt)) acc))]
+                [(= rc jconst:SQLITE_DONE)
+                 (reverse acc)]
+                [else
+                 (reverse acc)])))))
+      (lambda ()
+        (when stmt
+          (guard (e [(condition? e) (void)])
+            (japi:sqlite-finalize stmt)))))))
 
 ;; ═══════════════════════════════════════════════════════════════
 ;; Configuration
@@ -97,9 +96,8 @@
   (let ([v (getenv "EDGE_ALLOW_UNSIGNED")])
     (and v (or (string=? v "1") (string=? v "true") (string=? v "yes")))))
 (def *db-path*  (getenv "EDGE_DB_PATH"))    ;; SQLite file path, or #f for in-memory only
-(def *tls-cert* (getenv "EDGE_TLS_CERT"))  ;; PEM cert path, or #f to disable TLS
-(def *tls-key*  (getenv "EDGE_TLS_KEY"))   ;; PEM key path, or #f to disable TLS
-(def *tls-port* (or (and (getenv "EDGE_TLS_PORT") (string->number (getenv "EDGE_TLS_PORT"))) 8443))
+(def (secret-configured?)
+  (> (string-length *secret*) 0))
 
 ;; ═══════════════════════════════════════════════════════════════
 ;; Prometheus Metrics (Phase 3.4)
@@ -155,20 +153,21 @@
 
 (def (db-init!)
   (when *db-path*
-    (let ([db (sqlite-open *db-path*)])
-      (sqlite-exec db "PRAGMA journal_mode=WAL")
-      (sqlite-exec db
-        (string-append
-          "CREATE TABLE IF NOT EXISTS events ("
-          "  id TEXT PRIMARY KEY, type TEXT, status TEXT,"
-          "  received TEXT, processed TEXT)"))
-      (sqlite-exec db
-        (string-append
-          "CREATE TABLE IF NOT EXISTS dead_letters ("
-          "  id TEXT PRIMARY KEY, type TEXT, reason TEXT,"
-          "  retries INTEGER, ts INTEGER)"))
-      (set! *db* db)
-      (jlog "sqlite opened" "path" *db-path*))))
+    (let ([db #f])
+      (dynamic-wind
+        (lambda () (void))
+        (lambda ()
+          (set! db (sqlite-open *db-path*))
+          (sqlite-exec db "PRAGMA journal_mode=WAL")
+          (sqlite-exec db "CREATE TABLE IF NOT EXISTS events (id TEXT PRIMARY KEY, type TEXT, status TEXT, received TEXT, processed TEXT)")
+          (sqlite-exec db "CREATE TABLE IF NOT EXISTS dead_letters (id TEXT PRIMARY KEY, type TEXT, reason TEXT, retries INTEGER, ts INTEGER)")
+          (set! *db* db)
+          (set! db #f)
+          (jlog "sqlite opened" "path" *db-path*))
+        (lambda ()
+          (when db