Security hardening and release readiness

ober

ed479a07007b3c840861c6316a6bfe35e1209fd4

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..632c50f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,33 @@
+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 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 8d6f477..5b8ce4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
 build/
 dist/
 *.so
+*.dylib
 *.wpo
 *.o
+*.a
+*.boot
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..f508b39
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,35 @@
+{
+  "version": 1,
+  "repo": "jerboa-sinatra",
+  "extends": ["jerboa:web-framework", "jerboa:network-service"],
+  "paths": {
+    "production": ["*.ss", "sinatra/**/*.ss", "scripts/**", "support/**", "Makefile"],
+    "tests": ["**/*-test.ss", "test-runner.ss", "sinatra-test.ss"],
+    "generated": ["build/**", "dist/**", "*.so", "*.dylib", "*.wpo", "*.o", "*.a"],
+    "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"]
+  },
+  "policy": {
+    "failOn": ["critical", "high", "medium"],
+    "imports": { "directChezscheme": "deny" },
+    "templates": { "defaultEscaping": "html", "rawRenderingRequiresExplicitApi": "render-raw-string" },
+    "headers": { "rejectCrlf": true, "validateCookieFields": true },
+    "sessions": { "signed": true, "constantTimeSignatureCheck": true, "secureCookieDefault": true },
+    "staticFiles": { "requireDirectoryContainment": true, "defaultMaxBytes": 8388608 },
+    "network": { "daemonUse": "framework-only; production apps require deployment threat model", "rejectSmugglingShape": true },
+    "eval": { "stringEval": "deny", "bareRead": "deny" },
+    "releaseEvidence": {
+      "requireTargetProofForProduction": true,
+      "sensitiveArtifactPolicy": "no-secrets-tokens-logs-request-bodies-or-private-host-data"
+    }
+  },
+  "runtimeTargets": ["linux", "darwin"],
+  "releaseGates": [
+    "make verify",
+    "make web-security-smoke",
+    "make target-evidence",
+    "make sbom",
+    "make reproducibility-report",
+    "make release-evidence"
+  ],
+  "suppressions": []
+}
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 0dd93a7..c98dab5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,22 @@
-JERBOA_HOME ?= $(HOME)/mine/jerboa
-JERBOA ?= jerboa
+JERBOA_SOURCE_HOME ?= $(abspath $(CURDIR)/../jerboa)
+JERBOA_DIST ?= $(JERBOA_SOURCE_HOME)/dist/jerboa
+JERBOA ?= $(if $(wildcard $(JERBOA_DIST)),$(JERBOA_DIST),jerboa)
 JERBUILD ?= $(JERBOA) jerbuild
+JERBOA_HOME ?= $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
+ifeq ($(JERBOA_HOME),)
+JERBOA_HOME := $(HOME)/mine/jerboa
+endif
+JERBOA_SOURCE_LIB := $(JERBOA_SOURCE_HOME)/lib
 UNAME_S := $(shell uname -s)
 
 BUILD_DIR ?= build
 SRC_STAGE := $(BUILD_DIR)/src
 LIB_STAGE := $(BUILD_DIR)/lib
-LIBDIRS := $(LIB_STAGE):$(JERBOA_HOME)/lib
+LIBDIRS := $(LIB_STAGE):$(if $(wildcard $(JERBOA_SOURCE_LIB)),$(JERBOA_SOURCE_LIB):)$(JERBOA_HOME)/lib
 ENTRY ?= example.ss
 BINARY_OUTPUT ?= dist/jerboa-sinatra-example
 STATIC_BINARY_OUTPUT ?= dist/jerboa-sinatra-example-linux-amd64
-PROJECT_LIBDIRS := $(abspath $(LIB_STAGE))
+PROJECT_LIBDIRS := $(abspath $(LIB_STAGE))$(if $(wildcard $(JERBOA_SOURCE_LIB)),:$(JERBOA_SOURCE_LIB),)
 BINARY_NATIVE_LDFLAGS ?= $(if $(filter Darwin,$(UNAME_S)),-lc++,)
 STATIC_TARGET_MACHINE ?= ta6le
 STATIC_RUST_TARGET ?= x86_64-unknown-linux-musl
@@ -19,8 +25,13 @@ STATIC_CSV_DIR ?= $(firstword $(wildcard $(STATIC_CHEZ_PREFIX)/lib/csv*/$(STATIC
 STATIC_XPATCH ?= $(JERBOA_HOME)/build/chez/xc-$(STATIC_TARGET_MACHINE)/s/xpatch
 STATIC_MUSL_CC ?= x86_64-linux-musl-gcc
 STATIC_OS_LIBS ?= -lm -ldl -lpthread -static -no-pie -lstdc++
+EVIDENCE_DIR ?= dist/release-evidence
+SBOM_DIR ?= dist/sbom
+REPRO_DIR ?= dist/reproducibility
+SOAK_EVIDENCE_DIR ?= dist/soak-evidence
+TARGET_EVIDENCE_DIR ?= dist/target-evidence
 
-.PHONY: build test example binary static-binary clean distclean
+.PHONY: build test example binary static-binary security import-check web-security-smoke web-security-corpus soak-evidence target-evidence audit verify sbom reproducibility-report release-evidence clean distclean
 
 build:
 	rm -rf $(SRC_STAGE) $(LIB_STAGE)
@@ -32,6 +43,76 @@ build:
 test: build
 	$(JERBUILD) exec --libdirs $(LIBDIRS) test-runner.ss
 
+security:
+	bash scripts/security-check.sh
+
+import-check: build
+	$(JERBUILD) exec --libdirs $(LIBDIRS) support/import-check.ss
+
+web-security-smoke: build scripts/web-security-smoke.sh support/web-security-smoke.ss
+	REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" sh scripts/web-security-smoke.sh
+
+web-security-corpus: build scripts/web-security-corpus.sh support/web-security-corpus.ss
+	REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" sh scripts/web-security-corpus.sh
+
+soak-evidence: build scripts/soak-evidence.sh support/soak-evidence.ss
+	REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" SOAK_EVIDENCE_DIR="$(SOAK_EVIDENCE_DIR)" sh scripts/soak-evidence.sh
+
+target-evidence: scripts/target-evidence.sh
+	REPO_ROOT="$(CURDIR)" TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" sh scripts/target-evidence.sh
+
+audit:
+	@echo "==> jerbuild"
+	@$(JERBUILD) --version
+	@echo "==> C compiler"
+	@cc --version 2>/dev/null | head -1 || cc -v 2>&1 | head -1
+	@echo "==> dependency audit"
+	@if [ -f Cargo.lock ] || [ -f package-lock.json ] || [ -f pnpm-lock.yaml ] || [ -f yarn.lock ]; then \
+		echo "unexpected external package-manager lockfile present; document and audit it before release" >&2; \
+		exit 1; \
+	else \
+		echo "no repo-local Rust or Node dependency lockfiles"; \
+	fi
+
+verify: security test import-check web-security-smoke web-security-corpus soak-evidence target-evidence binary audit
+
+sbom: scripts/sbom.sh
+	REPO_ROOT="$(CURDIR)" SBOM_DIR="$(SBOM_DIR)" JERBUILD="$(JERBUILD)" JERBOA_HOME="$(JERBOA_HOME)" JERBOA_SOURCE_HOME="$(JERBOA_SOURCE_HOME)" sh scripts/sbom.sh
+
+reproducibility-report: scripts/reproducibility-report.sh
+	REPO_ROOT="$(CURDIR)" REPRO_DIR="$(REPRO_DIR)" MAKE="$(MAKE)" sh scripts/reproducibility-report.sh
+
+release-evidence: verify
+	$(MAKE) reproducibility-report
+	$(MAKE) sbom
+	rm -rf $(EVIDENCE_DIR)
+	mkdir -p $(EVIDENCE_DIR)
+	{ git rev-parse HEAD 2>/dev/null || true; } > $(EVIDENCE_DIR)/git-commit.txt
+	git status --short > $(EVIDENCE_DIR)/git-status.txt
+	git diff --stat > $(EVIDENCE_DIR)/diff-stat.txt
+	{ printf 'JERBUILD=%s\n' '$(JERBUILD)'; printf 'JERBOA_HOME=%s\n' '$(JERBOA_HOME)'; printf 'JERBOA_SOURCE_HOME=%s\n' '$(JERBOA_SOURCE_HOME)'; printf 'ENTRY=%s\n' '$(ENTRY)'; } > $(EVIDENCE_DIR)/build-env.txt
+	$(MAKE) security > $(EVIDENCE_DIR)/security.log 2>&1
+	$(MAKE) test > $(EVIDENCE_DIR)/test.log 2>&1
+	$(MAKE) import-check > $(EVIDENCE_DIR)/import-check.log 2>&1
+	$(MAKE) web-security-smoke > $(EVIDENCE_DIR)/web-security-smoke.log 2>&1
+	$(MAKE) web-security-corpus > $(EVIDENCE_DIR)/web-security-corpus.log 2>&1
+	$(MAKE) soak-evidence > $(EVIDENCE_DIR)/soak-evidence.log 2>&1
+	$(MAKE) target-evidence > $(EVIDENCE_DIR)/target-evidence.log 2>&1
+	$(MAKE) binary > $(EVIDENCE_DIR)/binary.log 2>&1
+	$(MAKE) audit > $(EVIDENCE_DIR)/audit.log 2>&1
+	@if command -v otool >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then otool -L "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; elif command -v ldd >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then ldd "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; fi
+	@if [ -f "$(BINARY_OUTPUT)" ]; then shasum -a 256 "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-sha256.txt; fi
+	{ find . \( -path './.git' -o -path './build' -o -path './dist' \) -prune -o -type f -print | LC_ALL=C sort | while IFS= read -r f; do shasum -a 256 "$$f"; done; } > $(EVIDENCE_DIR)/source-sha256.txt
+	rm -rf $(EVIDENCE_DIR)/sbom $(EVIDENCE_DIR)/reproducibility $(EVIDENCE_DIR)/web-security-smoke $(EVIDENCE_DIR)/web-security-corpus $(EVIDENCE_DIR)/soak-evidence $(EVIDENCE_DIR)/target-evidence
+	cp -R $(SBOM_DIR) $(EVIDENCE_DIR)/sbom
+	cp -R $(REPRO_DIR) $(EVIDENCE_DIR)/reproducibility
+	cp -R dist/web-security-smoke $(EVIDENCE_DIR)/web-security-smoke
+	cp -R dist/web-security-corpus $(EVIDENCE_DIR)/web-security-corpus
+	cp -R $(SOAK_EVIDENCE_DIR) $(EVIDENCE_DIR)/soak-evidence
+	cp -R $(TARGET_EVIDENCE_DIR) $(EVIDENCE_DIR)/target-evidence
+	test "$$(grep '^status=' "$(EVIDENCE_DIR)/reproducibility/report.txt" | cut -d= -f2)" = "match"
+	grep -q '^target_evidence_status=present$$' "$(EVIDENCE_DIR)/target-evidence/status.txt"
+
 binary: build
 	mkdir -p $(dir $(BINARY_OUTPUT))
 	JERBOA_HOME="$(JERBOA_HOME)" $(JERBUILD) binary --static-native $(foreach flag,$(BINARY_NATIVE_LDFLAGS),--extra-ldflag $(flag)) --libdirs "$(PROJECT_LIBDIRS)" $(ENTRY) $(BINARY_OUTPUT)
diff --git a/README.md b/README.md
index 0881898..db091ca 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,10 @@
 
 A Sinatra-style web framework ported to Jerboa.
 
+Status: experimental Lane B web framework. Do not use it for a public
+production application until the remaining release gates in `SECURITY.md` and
+`docs/threat-model.md` are closed for the application that embeds it.
+
 ## Build
 
 ```sh
@@ -14,13 +18,43 @@ make build
 make test
 ```
 
-The Makefile uses `~/mine/jerboa` by default. Override with `JERBOA_HOME=/path/to/jerboa` if needed.
+The Makefile derives `JERBOA_HOME` from the selected `jerbuild` when possible.
+Override with `JERBOA_HOME=/path/to/jerboa` or `JERBUILD=/path/to/jerbuild` if
+needed.
+
+## Security Verification
+
+```sh
+make verify
+make target-evidence
+make release-evidence
+```
+
+`make verify` runs the static security gate, test suite, public module import
+check, web security smoke tests, deterministic web security corpus, bounded
+handler soak evidence, example binary build, and local dependency/toolchain
+audit. `make target-evidence` records the production-only hosted load, proxy/TLS,
+fuzzing, deployment, and external-review gates. `make release-evidence` adds
+SBOM/toolchain provenance and repeated clean-build reproducibility evidence under
+`dist/release-evidence/`.
+
+When `../jerboa/dist/jerboa` is present, the Makefile uses it so release checks
+exercise current Jerboa binary hardening and deterministic-build features. It
+falls back to `jerboa` on hosts that do not have an adjacent source checkout.
+
+The framework now HTML-escapes `render-string` template substitutions by
+default, rejects CR/LF in generated headers and cookies, signs sessions with
+constant-work comparison, uses secure session cookies by default, bounds static
+file reads, rejects static traversal paths, rejects `Transfer-Encoding` plus
+`Content-Length`, rejects unsupported method overrides, and records a 512-case
+route/cookie/template/static security corpus plus a 2048-request local bounded
+handler soak.
 
 ## Example Binary
 
 ```sh
 make binary
-./dist/jerboa-sinatra-example
+JERBOA_SINATRA_SESSION_SECRET="$(openssl rand -hex 32)" ./dist/jerboa-sinatra-example
 ```
 
 For a fully static Linux amd64 example binary:
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..0d138af
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,53 @@
+# jerboa-sinatra Security Policy
+
+This repository is part of the Jerboa ecosystem. Treat it as an experimental web
+framework until the release gates below are complete for both the framework and
+the application that embeds it.
+
+Production claims require the release gates tracked in `~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` to be complete for this repository.
+
+## Supported Status
+
+No public production-support commitment exists yet. Security-sensitive releases must be cut from a clean checkout after:
+
+- `git status --short` shows only intentional release changes.
+- A secret scan is clean for the working tree, and history has been reviewed or
+  intentionally reset before first public release. The June 21, 2026
+  high-confidence scan recorded `jerboa-sinatra` current/history as clean.
+- `make verify` and `make release-evidence` pass.
+- `make target-evidence` records target production blockers locally, and
+  `JSINATRA_REQUIRE_TARGET_PROOF=1` passes with a marker-complete
+  `JSINATRA_TARGET_PROOF_FILE` before any production-support claim.
+- Dependency, toolchain, and native-code audits are clean, or accepted risks are documented.
+- Web framework behavior is documented in `docs/threat-model.md` and `docs/web-security.md`.
+
+## Security Posture
+
+- Template substitution through `render-string` and `render-file-template` is HTML-escaped by default. `render-raw-string` is explicit trusted-content behavior.
+- Response headers and generated cookies reject CR/LF.
+- Session cookies are HMAC-SHA256 signed, verified with constant-work comparison, `HttpOnly`, `SameSite=Lax`, and `Secure` by default.
+- Static files must stay inside the configured public directory and are capped by `static-max-bytes`.
+- Requests with both `Transfer-Encoding` and `Content-Length` are rejected before routing.
+- Method override accepts only `PUT`, `PATCH`, and `DELETE`.
+- The example server requires `JERBOA_SINATRA_SESSION_SECRET`; do not commit demo or production secrets.
+
+## Remaining Blockers
+
+- SBOM/toolchain provenance and repeated clean-build comparison are generated by
+  `make release-evidence`; keep these artifacts attached to any release
+  candidate.
+- Local bounded handler soak evidence is recorded by `make soak-evidence` for
+  route parsing, form bodies, sessions, templates, static serving, production
+  error handling, request-smuggling rejection, and method override. Sustained
+  hosted load/soak evidence for a production-style application is still
+  required before public production support.
+- No coverage-guided fuzzing has been recorded beyond the local deterministic
+  web security corpus.
+- Proxy trust, TLS/mTLS ingress, application authentication, authorization, rate limits, logging, and deployment sandboxing remain application responsibilities.
+- External review is still required before public production support.
+- Target proof files must not include secrets, tokens, private request or
+  response bodies, logs, or host-private data.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After public release, replace this section with a dedicated advisory contact, supported versions, and disclosure window.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..bd1101a
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,81 @@
+# Release Evidence
+
+Generate release evidence with:
+
+```sh
+make release-evidence
+```
+
+The target writes `dist/release-evidence/` with:
+
+- `security.log` from `scripts/security-check.sh`.
+- `test.log` from the framework test suite.
+- `import-check.log` from loading all public modules.
+- `web-security-smoke.log` and `web-security-smoke/result.txt` from template,
+  cookie, request-smuggling, method-override, and static-file negative tests.
+- `web-security-corpus.log` and `web-security-corpus/result.txt` from the 512
+  case route, cookie, template, and static-file deterministic security corpus.
+- `soak-evidence.log` and `soak-evidence/result.txt` from the 2048 request
+  local bounded handler soak across route, form, session-cookie, template,
+  static, HEAD, production-error, smuggling-reject, and method-override paths.
+- `target-evidence.log` and `target-evidence/status.txt` from the production
+  proof gate for hosted load/soak, proxy/TLS deployment, coverage-guided fuzzing,
+  production app auth/rate-limit review, logging redaction, deployment
+  sandboxing, and external web-framework review.
+- `binary.log`, `binary-sha256.txt`, and `binary-linkage.txt` for the example
+  binary build where the host supports linkage inspection.
+- `audit.log` with Jerboa and C compiler identity plus dependency-lockfile
+  status.
+- `sbom/` with component, license, adjacent Jerboa source, toolchain, and
+  release-input manifests.
+- `reproducibility/` with a repeated clean-build comparison of the example
+  binary, generated Jerboa library stage, generated binary inputs, and source
+  manifest, plus repeated web security corpus output. The binary comparison
+  opts into Jerboa's WPO gensym-prefix canonicalization and records
+  `jerboa_binary_canonicalize_wpo=1`; it also prewarms and restores a fixed
+  generated-library object snapshot so both measured builds use identical
+  binary inputs, then discards one warm measured build before comparing the
+  next two builds.
+- `git-commit.txt`, `git-status.txt`, `diff-stat.txt`, `build-env.txt`, and
+  `source-sha256.txt`.
+
+This evidence is local release input only. The June 21, 2026 high-confidence
+history/current secret scan recorded `jerboa-sinatra` as clean. First public
+release still requires coverage-guided fuzzing beyond the smoke tests,
+hosted load/soak evidence for at least one production-style application,
+proxy/TLS deployment review, and external review of the web API and deployment
+assumptions.
+
+`make target-evidence` writes `dist/target-evidence/status.txt`. Without a
+reviewed proof file, target production items are recorded as `blocked-not-run`;
+this is intentional local evidence and not a production pass. Set
+`JSINATRA_REQUIRE_TARGET_PROOF=1` and
+`JSINATRA_TARGET_PROOF_FILE=/path/to/proof.txt` to fail closed unless the proof
+file contains all required review markers. Accepted proof files are copied and
+hashed into the evidence bundle.
+
+Proof files must not contain secrets, tokens, private request or response
+bodies, logs, or host-private data.
+
+Current local web-security corpus evidence records `cases=512`,
+`route_cases=128`, `cookie_cases=128`, `template_cases=128`,
+`static_cases=128`, `accepted=336`, `rejected=176`,
+`invariant_failures=0`, and `status=pass`.
+
+web_security_corpus_status: documented
+web_security_corpus_cases_minimum: 512
+web_security_corpus_scope_status: route-cookie-template-static
+
+Current local soak evidence records `mode=local-bounded-handler-soak`,
+`requests=2048`, `passed=2048`, `failed=0`, `status_2xx=1420`,
+`status_4xx=471`, `status_5xx=157`, `status_other=0`, and `status=pass`.
+This is deterministic handler-level release evidence, not a substitute for
+hosted target-environment load testing.
+
+soak_evidence_status: local-bounded-handler-soak-recorded
+soak_evidence_requests_minimum: 2048
+soak_evidence_scope_status: route-form-session-static-template-error-security
+hosted_soak_status: blocked-not-run
+target_evidence_status: local-production-proof-recorded
+target_proof_status: not-run
+production_web_status: blocked-not-run
diff --git a/docs/threat-model.md b/docs/threat-model.md
new file mode 100644
index 0000000..17d182e
--- /dev/null
+++ b/docs/threat-model.md
@@ -0,0 +1,58 @@
+# jerboa-sinatra Threat Model
+
+`jerboa-sinatra` is a web framework and example server. The library is not a
+privilege boundary by itself; production applications must still define their
+own authentication, authorization, rate limits, deployment sandbox, TLS/mTLS
+termination, logging policy, and data-retention policy.
+
+## Assets
+
+- Session cookies and session signing secrets.
+- Request headers, query strings, form bodies, JSON bodies, and uploaded/static
+  content handled by applications built on the framework.
+- Application templates, static files under the configured public directory, and
+  response headers emitted by handlers or middleware.
+- Telemetry emitted through `std secmon telemetry` when configured by the
+  deployment environment.
+
+## Trust Boundaries
+
+- All request paths, query strings, headers, cookies, and bodies are hostile.
+- Reverse-proxy headers such as `X-Forwarded-Proto` are trustworthy only when a
+  trusted ingress strips and re-adds them.
+- Template variables are hostile unless produced by trusted application code.
+- Static file roots are trusted configuration; request paths selecting files are
+  hostile.
+- The built-in TLS helper delegates to Jerboa `std net httpsd`. Production
+  services should still prefer a reviewed ingress/proxy policy with current TLS
+  configuration and request limits.
+
+## Required Release Posture
+
+- Run `make verify` and `make release-evidence` from a clean checkout.
+- Keep `render-string` HTML escaping enabled by default; use `render-raw-string`
+  only for explicitly trusted, pre-sanitized HTML.
+- Keep session cookies signed, `HttpOnly`, and `Secure` by default. Local HTTP
+  examples may set `session-secure` to `#f`, but production applications should
+  not.
+- Reject CR/LF in response headers and generated cookies.
+- Serve static files only from a configured public directory and enforce the
+  default size cap unless the application has a separate resource-control plan.
+- Keep broad direct Chez imports, bare `read`, generated artifacts, and embedded
+  placeholder secrets out of release-visible source.
+- Keep the deterministic route, cookie, template, and static-file corpus in
+  `make verify`; add longer hosted fuzzing evidence before a high-risk public
+  application release.
+- Keep the local bounded handler soak in `make verify`; add sustained hosted
+  load testing for the specific production application and ingress path before
+  public production support.
+
+## Remaining Production Blockers
+
+- Local bounded handler soak evidence is recorded, but no sustained hosted
+  load/soak evidence has been recorded for a real production application using
+  this framework.
+- No longer-running coverage-guided fuzzing has been recorded beyond the local
+  deterministic web security corpus.
+- No external review has been completed for the request/response API, proxy
+  trust behavior, TLS helper, or session-cookie policy.
diff --git a/docs/web-security.md b/docs/web-security.md
new file mode 100644
index 0000000..2a35a47
--- /dev/null
+++ b/docs/web-security.md
@@ -0,0 +1,55 @@
+# Web Security Notes
+
+## Templates
+
+`render-string` and `render-file-template` HTML-escape substituted variables by
+default. Use `render-raw-string` only for content that has already been sanitized
+by the application. Route handlers that return arbitrary strings are still
+responsible for choosing the correct content type and escaping policy.
+
+## Headers And Cookies
+
+Response header names and values reject CR/LF to prevent response splitting.
+Generated cookies reject CR/LF, invalid cookie names, semicolon injection in
+attributes, and negative `Max-Age` values.
+
+Session cookies are signed with HMAC-SHA256 and verified with a fixed-work
+string comparison. They are `HttpOnly`, `SameSite=Lax`, and `Secure` by default.
+Applications running local HTTP demos may set `session-secure` to `#f`; production
+deployments should leave it enabled and serve through HTTPS.
+
+## Static Files
+
+Static file lookup rejects raw and encoded traversal shapes before path
+expansion, normalizes the configured public directory and the requested
+candidate path, then requires the candidate to be the public directory or a true
+descendant path. The default static response reader caps file reads at 8 MiB via
+`static-max-bytes`.
+
+## Proxy And TLS Trust
+
+`sinatra-request-secure?` uses `X-Forwarded-Proto`. Production ingress must strip
+untrusted client-supplied forwarding headers and set its own values. The built-in
+HTTPS helper is useful for local or controlled deployments, but production
+services should document ingress TLS/mTLS, timeouts, body limits, and proxy
+header policy before promotion.
+
+## Method Override And Smuggling
+
+`method-override` accepts only `PUT`, `PATCH`, and `DELETE` overrides on `POST`
+requests. Requests containing both `Transfer-Encoding` and `Content-Length` are
+rejected before routing.
+
+## Local Security Corpus
+
+`make web-security-corpus` runs 512 deterministic hostile-input cases across
+route matching, cookie parsing/generation, template rendering, and static-file
+lookup/serving. `make verify` and `make release-evidence` run this corpus and
+record the output for release review.
+
+`make soak-evidence` runs 2048 deterministic handler-level requests across
+normal routing, named params, form bodies, signed session cookies, static
+serving, HEAD handling, production error hiding, request-smuggling rejection,
+and method override. This is local bounded release evidence; production
+applications still need sustained hosted load testing for their deployed ingress
+and configuration.
diff --git a/example.ss b/example.ss
index 9aac67c..fc43ba7 100644
--- a/example.ss
+++ b/example.ss
@@ -4,13 +4,28 @@
 ;;; Then visit: http://127.0.0.1:4567/
 
 (import (jerboa runtime)
+        (only (std misc thread) thread-sleep!)
         (sinatra))
 
+(define (required-env name)
+  (let ((value (getenv name)))
+    (unless (and value (> (string-length value) 0))
+      (error "Missing required environment variable" name))
+    value))
+
+(define (example-hash pairs)
+  (let ((table (make-hash-table)))
+    (for-each (lambda (pair) (hash-put! table (car pair) (cdr pair))) pairs)
+    table))
+
 ;; Configuration
 (configure
   (set-option! "port" 4567)
   (enable! 'sessions)
-  (set-option! "session-secret" "super-secret-key-change-in-production"))
+  (set-option! "session-secret" (required-env "JERBOA_SINATRA_SESSION_SECRET"))
+  ;; The example binds to localhost over HTTP. Production deployments should
+  ;; leave secure session cookies enabled and terminate HTTPS before the app.
+  (set-option! "session-secure" #f))
 
 ;; Before filter - runs before every request
 (before
@@ -31,13 +46,15 @@
 ;; ---- JSON API ----
 
 (GET "/api/users"
-  (json (hash ("users" (list
-    (hash ("id" 1) ("name" "Alice"))
-    (hash ("id" 2) ("name" "Bob")))))))
+  (json (example-hash
+          (list (cons "users"
+                  (list
+                    (example-hash (list (cons "id" 1) (cons "name" "Alice")))
+                    (example-hash (list (cons "id" 2) (cons "name" "Bob")))))))))
 
 (GET "/api/users/:id"
   (let ((id (param "id")))
-    (json (hash ("id" id) ("name" "User")))))
+    (json (example-hash (list (cons "id" id) (cons "name" "User"))))))
 
 (POST "/api/users"
   (let ((body (sinatra-request-body-json (request))))
@@ -83,8 +100,9 @@
 
 (GET "/template"
   (render-string "<h1>{{title}}</h1><p>{{message}}</p>"
-    (hash ("title" "Jerboa-Sinatra")
-          ("message" "Templates work!"))))
+    (example-hash
+      (list (cons "title" "Jerboa-Sinatra")
+            (cons "message" "Templates work!")))))
 
 (GET "/sxml"
   (render-sxml
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
new file mode 100644
index 0000000..e60de94
--- /dev/null
+++ b/scripts/reproducibility-report.sh
@@ -0,0 +1,235 @@
+#!/bin/sh
+set -eu
+
+repo_root=${REPO_ROOT:-$(pwd)}
+repro_dir=${REPRO_DIR:-dist/reproducibility}
+make_cmd=${MAKE:-make}
+tmp_parent=${JSINATRA_REPRO_TMPDIR:-/tmp}
+tmp_dir=$tmp_parent/jsinatra-repro-$$
+obj_dir=$tmp_dir/jerbuild-binary-sinatra
+wpo_canonicalize=${JERBOA_BINARY_CANONICALIZE_WPO:-1}
+prepared_artifact_list=$tmp_dir/prepared-lib-artifacts.list
+prepared_artifact_tar=$tmp_dir/prepared-lib-artifacts.tar
+prepared_artifact_status=none
+
+cleanup() {
+  rm -rf "$tmp_dir"
+}
+trap cleanup EXIT INT TERM
+
+cd "$repo_root"
+rm -rf "$tmp_dir"
+mkdir -p "$tmp_dir"
+
+source_manifest() {
+  find . \( -path './.git' -o -path './build' -o -path './dist' \) -prune -o \
+    -type f \
+    -not -name '*.so' \
+    -not -name '*.dylib' \
+    -not -name '*.wpo' \
+    -not -name '*.wp.so' \
+    -not -name '*.o' \
+    -not -name '*.a' \
+    -print | LC_ALL=C sort | xargs shasum -a 256
+}
+
+snapshot_prepared_artifacts() {
+  if [ -d "$obj_dir" ]; then
+    (
+      cd "$obj_dir"
+      find bundle u-lib -type f \( -name '*.so' -o -name '*.wpo' \) 2>/dev/null | LC_ALL=C sort
+    ) > "$prepared_artifact_list"
+  else
+    : > "$prepared_artifact_list"
+  fi
+
+  if [ -s "$prepared_artifact_list" ]; then
+    (
+      cd "$obj_dir"
+      tar -cf "$prepared_artifact_tar" -T "$prepared_artifact_list"
+      while IFS= read -r artifact; do
+        shasum -a 256 "$artifact"
+      done < "$prepared_artifact_list"
+    ) > "$tmp_dir/prepared-lib-artifacts.sha256"
+    prepared_artifact_status=present
+  else
+    : > "$tmp_dir/prepared-lib-artifacts.sha256"
+    prepared_artifact_status=none
+  fi
+}
+
+restore_prepared_artifacts() {
+  rm -rf "$obj_dir"
+  mkdir -p "$obj_dir"
+  if [ -f "$prepared_artifact_tar" ]; then
+    (
+      cd "$obj_dir"
+      tar -xf "$prepared_artifact_tar"
+    )
+  fi
+}
+
+stage_manifest() {
+  build_dir=$1
+  find "$build_dir/lib" -type f -name '*.sls' -print | LC_ALL=C sort |
+    while IFS= read -r file; do
+      rel=${file#"$build_dir/lib/"}
+      sed "s|$build_dir/src|<BUILD_SRC>|g" "$file" | shasum -a 256 | awk -v rel="$rel" '{print $1 "  " rel}'
+    done
+}
+
+hash_generated_inputs() {
+  label=$1
+  manifest="$tmp_dir/$label-generated-inputs.sha256"
+  if [ -d "$obj_dir" ]; then
+    (
+      cd "$obj_dir"
+      for f in \
+        cross-wpo-compile.ss \
+        cross-wpo-whole.ss \
+        ffi_symbols.h \
+        jerboa-entry.ss \
+        jerboa-entry-shebangless.ss \
+        main.c \
+        petite_boot.h \
+        program.so \
+        program.wpo \
+        program.wp.so \
+        program_boot.h \
+        scheme_boot.h
+      do
+        if [ -f "$f" ]; then
+          shasum -a 256 "$f"
+        fi
+      done
+    ) > "$manifest"
+  else
+    printf 'missing_obj_dir=%s\n' "$obj_dir" > "$manifest"
+  fi
+}
+
+build_once() {
+  label=$1
+  build_dir="$tmp_dir/build"
+  output_dir="$tmp_dir/output"
+  output="$output_dir/jerboa-sinatra-example"
+  rm -rf "$build_dir" "$output_dir" "$obj_dir"
+  mkdir -p "$output_dir"
+  restore_prepared_artifacts
+  env JERBUILD_BINARY_OBJ_DIR="$obj_dir" \
+    JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
+    JERBOA_BINARY_CANONICALIZE_WPO="$wpo_canonicalize" \
+    "$make_cmd" binary BUILD_DIR="$build_dir" BINARY_OUTPUT="$output" \
+    > "$tmp_dir/build-$label.txt" 2>&1
+  cp "$output" "$tmp_dir/$label-jerboa-sinatra-example"
+  shasum -a 256 "$tmp_dir/$label-jerboa-sinatra-example" | awk '{print $1}' > "$tmp_dir/binary-$label.sha256"
+  stage_manifest "$build_dir" > "$tmp_dir/stage-$label.sha256"
+  hash_generated_inputs "$label"
+}
+
+prewarm_build_dir="$tmp_dir/prewarm-build"
+prewarm_output_dir="$tmp_dir/prewarm-output"
+rm -rf "$prewarm_build_dir" "$prewarm_output_dir" "$obj_dir"
+mkdir -p "$prewarm_output_dir"
+env JERBUILD_BINARY_OBJ_DIR="$obj_dir" \
+  JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
+  JERBOA_BINARY_CANONICALIZE_WPO="$wpo_canonicalize" \
+  "$make_cmd" binary BUILD_DIR="$prewarm_build_dir" BINARY_OUTPUT="$prewarm_output_dir/jerboa-sinatra-example" \
+  > "$tmp_dir/prewarm-build.txt" 2>&1
+snapshot_prepared_artifacts
+
+source_manifest > "$tmp_dir/source-before.sha256"
+build_once warm
+build_once first
+build_once second
+
+"$make_cmd" web-security-corpus > "$tmp_dir/web-security-corpus-first.txt" 2>&1
+"$make_cmd" web-security-corpus > "$tmp_dir/web-security-corpus-second.txt" 2>&1
+
+source_manifest > "$tmp_dir/source-after.sha256"
+
+if cmp -s "$tmp_dir/binary-first.sha256" "$tmp_dir/binary-second.sha256"; then
+  binary_status=match
+else
+  binary_status=mismatch
+fi
+
+if cmp -s "$tmp_dir/stage-first.sha256" "$tmp_dir/stage-second.sha256"; then
+  staged_lib_status=match
+else
+  staged_lib_status=mismatch
+fi
+
+if cmp -s "$tmp_dir/first-generated-inputs.sha256" "$tmp_dir/second-generated-inputs.sha256"; then
+  generated_input_status=match
+else
+  generated_input_status=mismatch
+fi
+
+if cmp -s "$tmp_dir/source-before.sha256" "$tmp_dir/source-after.sha256"; then
+  source_manifest_status=match
+else
+  source_manifest_status=mismatch
+fi
+
+if cmp -s "$tmp_dir/web-security-corpus-first.txt" "$tmp_dir/web-security-corpus-second.txt"; then
+  web_security_corpus_status=match
+else
+  web_security_corpus_status=mismatch
+fi
+
+if [ "$binary_status" = match ] &&
+   [ "$staged_lib_status" = match ] &&
+   [ "$generated_input_status" = match ] &&
+   [ "$source_manifest_status" = match ] &&
+   [ "$web_security_corpus_status" = match ]; then
+  status=match
+else
+  status=mismatch
+fi
+
+{
+  echo "binary_status=$binary_status"
+  echo "staged_lib_status=$staged_lib_status"
+  echo "generated_input_status=$generated_input_status"
+  echo "source_manifest_status=$source_manifest_status"
+  echo "web_security_corpus_status=$web_security_corpus_status"
+  echo "jerbuild_binary_obj_dir=$obj_dir"
+  echo "jerbuild_binary_keep_obj_dir=1"
+  echo "jerboa_binary_canonicalize_wpo=$wpo_canonicalize"
+  echo "prepared_generated_lib_status=$prepared_artifact_status"
+  echo "discarded_warm_build=1"
+  echo "status=$status"
+} > "$tmp_dir/report.txt"
+
+if [ "$binary_status" != match ]; then
+  diff -u "$tmp_dir/binary-first.sha256" "$tmp_dir/binary-second.sha256" \
+    > "$tmp_dir/binary.diff" || true
+fi
+
+if [ "$staged_lib_status" != match ]; then
+  diff -u "$tmp_dir/stage-first.sha256" "$tmp_dir/stage-second.sha256" \
+    > "$tmp_dir/staged-libs.diff" || true
+fi
+
+if [ "$generated_input_status" != match ]; then
+  diff -u "$tmp_dir/first-generated-inputs.sha256" "$tmp_dir/second-generated-inputs.sha256" \