security: add source and restrict closure gates

ober

df41e657b0d3236cafaf4d7b179c23d9bd2ac3ed

diff --git a/.jerboa-system b/.jerboa-system
index 8ff7cca..498c723 100644
--- a/.jerboa-system
+++ b/.jerboa-system
@@ -31,6 +31,7 @@
               "tools/system-provenance.ss" "tools/system-diff.ss"
               "tools/check-audit-events.ss"
               "tools/check-generated-policy.ss"
+              "tools/check-restrict-closure.ss"
               "tools/workspace-health.ss"
               "tools/capability-plan.ss"
               "tools/check-capability-use.ss"
@@ -47,7 +48,7 @@
 
   (dependencies
     (vendored ("vendor/ChezScheme"))
-    (native ("jerboa-native-rs"))
+    (native ("jerboa-native-rs" "browser-repl"))
     (external-tools ("cc" "cargo" "make" "tar" "ln" "rm" "mkdir" "find"
                      "shasum|sha256sum")))
 
@@ -73,7 +74,7 @@
     (eval
       (modules declared-by-tooling))
     (native
-      (load ("jerboa-native-rs" "support/*.c")))
+      (load ("jerboa-native-rs" "browser-repl" "support/*.c")))
     (package
       (install none)
       (build declared)
@@ -86,7 +87,7 @@
     (test-targets ("system-check" "system-strict" "system-report"
                    "system-provenance" "system-diff" "audit-event-check"
                    "generated-policy-check" "unification-check"
-                   "workspace-health" "capability-plan"
+                   "restrict-closure-check" "workspace-health" "capability-plan"
                    "capability-use-check" "native-boundary-check"
                    "install-policy-check" "tcb-report"
                    "unification-release-check" "jerboa-smoke" "mcp-test-binary"
@@ -101,14 +102,14 @@
     (lock-file planned)
     (signing planned)
     (sbom planned)
-    (tcb ("Chez Scheme" "Jerboa stdlib" "jerboa-native-rs"
+    (tcb ("Chez Scheme" "Jerboa stdlib" "jerboa-native-rs" "browser-repl"
           "support/multicall-main.c" "support/jerboa-embed.c"
           "support/jerboa-embed.h"
           "support/hardening-report.h"
           "support/landlock-shim.c")))
 
   (native-boundary
-    (rust ("jerboa-native-rs"))
+    (rust ("jerboa-native-rs" "browser-repl"))
     (c-shims ("support/multicall-main.c" "support/jerboa-embed.c"
               "support/jerboa-embed.h" "support/hardening-report.h"
               "support/landlock-shim.c"))
diff --git a/Makefile b/Makefile
index f9285c0..25a1c90 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,7 @@ CHEZ_INSTALL_FLAGS = \
 	$(CHEZ_TERMUX_CONFIGURE_VARS) \
 	$(CHEZ_CONFIGURE_EXTRA)
 LIBDIRS = $(JERBOA_HOME)/lib:$(JERBOA_HOME)/vendor/jsqlite/src
+SOURCE_BALANCE_RUNNER ?= $(SCHEME) --libdirs $(LIBDIRS) --script
 # Base directory for chez-* repos (legacy C FFI — see `make native` for Rust backend)
 JERBOA_EXT_DIR ?= $(HOME)/src
 # External chez-* library paths for legacy wrapper modules
@@ -75,7 +76,7 @@ TYPED_LLVMIR_PARITY_FN ?= sample_typed_llvmir_smoke::main
 LLVM_BIN ?= $(shell if command -v llvm-as >/dev/null 2>&1; then dirname "$$(command -v llvm-as)"; elif [ -x /opt/homebrew/opt/llvm/bin/llvm-as ]; then echo /opt/homebrew/opt/llvm/bin; elif [ -x /usr/local/opt/llvm/bin/llvm-as ]; then echo /usr/local/opt/llvm/bin; fi)
 TYPED_WRAPPER_DIR ?= build/typed/jerboa
 
-.PHONY: help chez static-supported-check chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-kotlin test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit audit-native security-audit clean security security-production security-profile security-hardware-smoke sbom reproducibility-report reproducibility-compare verify release-evidence fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push lint
+.PHONY: help chez static-supported-check chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-kotlin test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit audit-native source-balance restrict-closure-check security-audit clean security security-production security-profile security-hardware-smoke sbom reproducibility-report reproducibility-compare verify release-evidence fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push lint
 .PHONY: check-cross-tools docker fuzz-websocket-fuzz jlsp jlsp-freebsd-amd64 \
         jlsp-install jlsp-linux-amd64 jlsp-portable jmcp-freebsd-amd64 \
         jmcp-freebsd-arm64 jmcp-linux-amd64 jmcp-linux-arm64 \
@@ -1722,7 +1723,15 @@ audit-native:
 security-audit:
 	@tools/security-audit.sh
 
-audit: security-audit audit-native data-check security-production unification-check
+source-balance:
+	@git ls-files '*.ss' '*.sls' \
+		| grep -v '^vendor/' \
+		| xargs $(SOURCE_BALANCE_RUNNER) support/check-source-balance.ss
+
+restrict-closure-check:
+	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-restrict-closure.ss
+
+audit: source-balance restrict-closure-check security-audit audit-native data-check security-production unification-check
 
 test-gaps:
 	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-gaps.ss
diff --git a/data/anti-patterns.sexp b/data/anti-patterns.sexp
index 566796d..fa41fad 100644
--- a/data/anti-patterns.sexp
+++ b/data/anti-patterns.sexp
@@ -5219,4 +5219,26 @@
      "stack-overflow" "generator")
    ("title" . "Typed Kotlin Same-Name Wrapper Recursion")
    ("tools" "jerboa_check_balance" "jerboa_verify"
-     "jerboa_run_tests" "adb logcat" "uiautomator dump")))
+     "jerboa_run_tests" "adb logcat" "uiautomator dump"))
+ (("advice"
+    .
+    "First verify with the real reader/compiler. Balance scanners and repair planners must skip \\\\x...; identifier escapes outside strings/chars before interpreting semicolons as comments.")
+   ("avoid"
+     .
+     "Do not append EOF closers or trust a delimiter-only repair plan when the reported form contains Chez escaped identifiers such as \\\\x7C;\\\\x3E;. The semicolon is part of the identifier escape, not a line comment.")
+   ("id" . "escaped-identifier-balance-false-positive")
+   ("kinds" "debug-error" "tooling" "scheme")
+   ("pattern"
+     .
+     "\\\\x[0-9A-Fa-f]+;.*(unclosed|unexpected close|repair plan)")
+   ("severity" . "medium")
+   ("tags" "balance" "chez" "escaped-identifier" "semicolon"
+     "repair")
+   ("title"
+     .
+     "Delimiter scanners treating Chez escaped identifiers as comments")
+   ("tools"
+     "jerboa_verify"
+     "jerboa_compile_check"
+     "jerboa_check_balance"
+     "jerboa_repair_balance")))
diff --git a/docs/ai-threat.md b/docs/ai-threat.md
index 2885fa2..b9dac72 100644
--- a/docs/ai-threat.md
+++ b/docs/ai-threat.md
@@ -182,7 +182,7 @@ wrong version of a function (safe vs unsafe) in edge cases.
 | Logic bugs | No advantage | No advantage | No advantage | No advantage |
 | DoS / resource exhaustion | Partial in-process; worker-defined for isolation | Behind | Behind | Behind (JVM has memlimits) |
 | Deserialization attacks | Depth-limited | Comparable | Ahead (limits) | Comparable |
-| Supply chain attacks | SBOM + content-addressed | Behind (cargo audit) | Behind (npm audit) | Behind (Maven) |
+| Supply chain attacks | TUF metadata, Ed25519 signatures, transparency log, SBOM, OSV audit, content-addressed artifacts | Comparable | Comparable | Comparable |
 
 ### Summary
 
@@ -208,10 +208,11 @@ patterns. If the pattern doesn't exist, there's nothing to find.
 | Temp file race in `run-safe` | Medium | Obsolete: thunk execution and child result paths are retired |
 | Unsafe Scheme execution after raw fork | High | Fixed: `run-safe` fails closed; use an exec worker |
 | No memory limit in `run-safe-eval` | Medium | By design: use OS limits on an exec worker |
-| Import conflict in safe prelude | Low | Known, cosmetic |
+| Import conflict in safe prelude | Low | Known; tracked because deterministic safe-symbol resolution matters |
 | SQL injection heuristic bypasses | Medium | By design (use parameterized queries) |
 | Seccomp/Landlock x86_64-only | Low | By design (arch-specific syscalls) |
 | Silent degradation of requested process controls | Medium | Fixed: reject unless `allow-degraded? #t` is explicit |
+| Raw `read` / native FASL call sites need provenance triage | Medium | Open; tracked in the K3 security handoff |
 
 ---
 
diff --git a/docs/index.md b/docs/index.md
index 5059367..e209c83 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -57,7 +57,7 @@ Start with [quickstart.md](quickstart.md), then use
 
 ## Security
 
-- [security-reference.md](security-reference.md) - security module reference and threat-model checklist
+- [security-reference.md](security-reference.md) - security module reference and limitation ledger
 - [safety-guide.md](safety-guide.md) - practical secure-application guide
 - [release-security.md](release-security.md) - release gates and security evidence
 - [capability.md](capability.md) - object-capability model
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index a53e867..9c1890c 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -4,7 +4,10 @@ Status: living handoff document. Created 2026-07-27 from a full review of
 `docs/*`, `SECURITY.md`, `lib/std/security/`, `jerboa-native-rs/`, `tests/`,
 the `Makefile`, and the `.jerboa-system` manifest. Tone per
 [`Philosophy.md`](Philosophy.md): concrete, no hype, no stubs. Every claim
-about code was verified in the tree on the date above.
+about code was verified in the tree on the date above. Updated 2026-07-27:
+the committed `pattern.ss` balance blocker named in K3-P0-01 was repaired,
+the balance scanner was fixed for escaped identifiers, and `make audit` now
+includes a tracked-source balance gate.
 
 **Audience:** the next model (or human) asked to implement security work in
 this repo. Each work item has an ID, the threat it closes, exact files to
@@ -274,9 +277,10 @@ when" must be answerable from `dist/release-evidence/` in minutes.
    harness-only; nothing preserves found crashes as tests.
 4. **`docs/ai-threat.md` comparative table is stale**: it rates supply
    chain "Behind (cargo audit)" but jpkg now has TUF/signatures/transparency
-   /OSV audit. Refresh the table (P2-06).
-5. **Three security modules are undocumented** in `security-reference.md`:
+   /OSV audit. Fixed 2026-07-27.
+5. **Three security modules were undocumented** in `security-reference.md`:
    `(std security cage)`, `(std security secret)`, `(std security env)`.
+   Fixed 2026-07-27.
 6. **Cross-repo findings** recorded in `security-reference.md`'s appended
    checklist remain open in sibling repos (out of scope here, tracked in
    §8 P2-04 note): `jerboa-code` external-LLM launcher passes
@@ -307,7 +311,9 @@ then industrialize find-first and recover-fast.
 ## 6. P0 — Containment foundation (do first)
 
 ### K3-P0-01 — Repair the build; add a balance gate
-**Serves:** G4. **Effort:** hours.
+**Serves:** G4. **Effort:** hours. **Status:** `pattern.ss` repair, escaped
+identifier scanner fix, and audit gate landed 2026-07-27; full native release
+build remains the normal pre-commit gate.
 
 A committed unbalanced file means the safety guarantees in the docs are not
 the code that ships. This violates the repo's own pre-commit rule.
@@ -373,7 +379,9 @@ that already exist individually.
   API.
 
 ### K3-P0-03 — Prove the restricted-environment allowlist closed
-**Serves:** G1, G4. **Effort:** 2–3 days.
+**Serves:** G1, G4. **Effort:** 2–3 days. **Status:** mechanical closure
+gate landed 2026-07-27; full reachability audit through Chez internals remains
+open.
 
 The allowlist is Jerboa's crown jewel; it currently rests on manual curation.
 
@@ -774,7 +782,7 @@ fix must add its scanner rule in the same commit (write it into
   `SECURITY.md`.
 - Move the appended "Security Patterns & Threat-Model Checklist" findings
   out of `security-reference.md` into a dated `docs/reviews/` file —
-  reference docs shouldn't carry stale review state.
+  reference docs shouldn't carry stale review state. Done 2026-07-27.
 
 ### K3-P2-07 — Secret-handling standard adoption
 **Serves:** G2. **Effort:** 1 week.
@@ -871,7 +879,7 @@ Track these in `docs/status.md` per release:
 
 | Metric | Baseline (2026-07-27) | Target |
 |---|---|---|
-| Build balance clean | **failing** (pattern.ss) | always clean |
+| Build balance clean | `pattern.ss` repaired 2026-07-27; `source-balance` in `make audit` | always clean |
 | Confined worker exists | no | yes, tested (P0-02) |
 | Unclassified raw `read`/FASL sites | unknown (4+ named) | 0 |
 | FFI bindings audited | 0 / 91 files | 100% with verdicts |
@@ -965,7 +973,10 @@ fake confidence happens.
 - The confined exec worker does not exist yet (P0-02); until it lands,
   every doc sentence saying "use an exec worker" describes an aspiration,
   not an API.
-- `lib/std/compiler/pattern.ss` is unbalanced as of this writing (P0-01).
+- The committed `pattern.ss` balance blocker named in P0-01 was repaired on
+  2026-07-27. The `pipeline.ss` and `test-pipeline.ss` reports were traced to
+  escaped-identifier false positives in the balance scanner; the scanner now
+  skips `\x...;` identifier escapes.
 
 ---
 
diff --git a/docs/reviews/2026-07-27-security-patterns-threat-model-checklist.md b/docs/reviews/2026-07-27-security-patterns-threat-model-checklist.md
new file mode 100644
index 0000000..9fab121
--- /dev/null
+++ b/docs/reviews/2026-07-27-security-patterns-threat-model-checklist.md
@@ -0,0 +1,116 @@
+# Security Patterns & Threat-Model Checklist
+
+Date: 2026-07-27.
+
+Scope: `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*`.
+
+This is a dated review record. Stable API descriptions and the current
+limitation ledger live in [security-reference.md](../security-reference.md).
+Prioritized implementation work lives in
+[kimi3-security-recommmendations.md](../kimi3-security-recommmendations.md).
+
+## Findings
+
+### Critical: sibling build scripts can mutate the main Jerboa checkout
+
+The cross-build scripts in `jerboa-lsp` and `jerboa-awk` patch
+`$jerboa-home/lib` in place, delete compiled artifacts, then run
+`git checkout --` against `.ss` and `.sls` files. If `$jerboa-home` points at
+`/Users/user/mine/jerboa`, these scripts can silently discard uncommitted work
+in the main repo.
+
+- `/Users/user/mine/jerboa-lsp/build-jlsp-cross.ss:51`
+- `/Users/user/mine/jerboa-lsp/build-jlsp-freebsd-cross.ss:50`
+- `/Users/user/mine/jerboa-awk/build-jawk-cross.ss:173`
+
+### Critical: `jerboa-code` disables LLM safeguards while allowing broad local access
+
+`src/jcode/tool/external-llm.ss` adds provider flags such as
+`--dangerously-skip-permissions`, `--yolo`, and `--sandbox danger-full-access`.
+The macOS sandbox profile is allow-by-default, then the command is executed
+through the shell. That combination gives model-driven CLI processes broad
+filesystem and network reach.
+
+- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:53`
+- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:157`
+- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:215`
+
+### High: main repo had a confirmed syntax/build blocker
+
+`lib/std/compiler/pattern.ss` reached EOF while still inside the `(library ...)`
+form. The Jerboa health check also reported balance failures in
+`lib/std/pipeline.ss`, `tests/test-pipeline.ss`, and
+`tests/test-security2-parsers.ss`.
+
+- `/Users/user/mine/jerboa/lib/std/compiler/pattern.ss:316`
+
+Current status in this repo: `pattern.ss` was repaired on 2026-07-27 and
+`make audit` now includes a tracked-source balance gate.
+`test-security2-parsers.ss` was rechecked and was already balanced.
+`pipeline.ss` and `test-pipeline.ss` verify cleanly with the compiler; their
+original balance findings were false positives caused by escaped identifiers
+such as `\x7C;\x3E;`, and the balance scanner was updated to handle that syntax.
+
+### High: `jerboa-inotify` exposes unchecked FFI buffer reads
+
+`jerboa_inotify_shim.c` casts `buf + offset` to `struct inotify_event *`
+without a buffer length or alignment check. A caller can read past the end of
+the buffer. The `read` wrapper also treats `EINTR` as a hard error instead of
+retrying.
+
+- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:33`
+- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:66`
+
+### High: `jerboa-wafter` has shell injection in the Wireshark converter
+
+`tools/wireshark-convert.ss` accepts `--out`, then builds `mkdir -p` with string
+concatenation and passes it to `system`. A crafted output directory can inject
+shell syntax.
+
+- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1095`
+- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1118`
+
+### Medium: `pipeline-timeout` can leave work running after timeout
+
+`pipeline-timeout` forks a worker, waits, then raises on timeout without joining
+or cancelling the worker. The worker can continue running and mutate captured
+state after the timeout path returns.
+
+- `/Users/user/mine/jerboa/lib/std/pipeline.ss:170`
+
+The related test does not actually exercise a failing inner stage:
+
+- `/Users/user/mine/jerboa/tests/test-pipeline.ss:164`
+
+### Medium: `jerboa-emacs` appears to use Gerbil process APIs
+
+Several Jerboa source files call `open-process` and `process-status`. Unless
+this repo injects compatibility wrappers at build time, those are not the
+Jerboa/Chez process APIs described by the project instructions.
+
+- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:110`
+- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:132`
+- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:1108`
+
+### Medium: main repo has raw `read` / FASL deserialization paths
+
+The Jerboa security scan found many raw `read` and FASL deserialization paths.
+These need source-by-source triage because untrusted `read` or FASL input is a
+deserialization risk.
+
+Examples:
+
+- `/Users/user/mine/jerboa/jerbuild.ss:308`
+- `/Users/user/mine/jerboa/lib/std/actor/distributed.ss:303`
+- `/Users/user/mine/jerboa/lib/std/fasl.ss:27`
+- `/Users/user/mine/jerboa/lib/std/net/grpc.ss:162`
+
+## Review Notes
+
+The review covered all matching `/Users/user/mine/jerboa*` and
+`/Users/user/mine/chez*` directories with `rg`, manual source reads, git status
+inventory, and Jerboa MCP health/security/stale checks on the main repo plus
+key sibling repos.
+
+Full builds were not run because many worktrees are dirty and several build
+scripts write/delete generated artifacts across checkouts.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index f2eb039..f8377f2 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -38,6 +38,22 @@ Creates an evaluation environment containing only approved bindings. Uses Chez S
 - `restricted-eval-string` -- parse and evaluate a string (uses `jerboa-read`, not Chez `read`)
 - `safe-bindings` -- the list of allowed symbols
 
+### Closure gate
+
+`make audit` runs `tools/check-restrict-closure.ss`. The gate:
+
+- constructs the restricted environment, which fails if any listed Chez binding
+  is missing in the pinned runtime;
+- rejects duplicate bindings and dangerous exact/prefix classes such as
+  `system`, `eval`, `gensym`, `foreign-*`, `compile*`, and `open-*-file`;
+- evaluates escape attempts for process execution, `eval`, FFI, file output,
+  `gensym`, and `call/cc` and requires all of them to fail;
+- keeps an allowed arithmetic/list smoke test so the environment is not merely
+  broken closed.
+
+The current gate is a mechanical closure and escape test. It is not a formal
+reachability proof through Chez internals.
+
 ### What's allowed
 
 Core syntax (`lambda`, `if`, `begin`, `let`, `cond`, `case`, `when`, `unless`, `do`, `define-syntax`, `syntax-rules`, `quasiquote`), arithmetic (including bitwise ops), comparison, booleans, pairs/lists, strings, characters, vectors, bytevectors, symbols, string-port I/O, hashtables, error handling, `apply`, `values`, `dynamic-wind`, `sort`, `format`, `void`.
@@ -519,6 +535,9 @@ These modules are implemented but not covered in depth above.
 | `(std security flow)` | Information flow control. Security levels form a lattice: public < internal < secret < top-secret. Data flows up freely; downward flow requires explicit `declassify` which is logged. |
 | `(std security metrics)` | Security counters, gauges, histograms with alerting thresholds. |
 | `(std security io-intercept)` | Effect-based I/O interception. All filesystem, network, and process operations mediated by handlers that can audit, deny, or mock. |
+| `(std security cage)` | Irreversible process self-confinement. Applies the platform sandbox for the current process and descendants from a `cage-config`: root, read-only paths, read-write paths, executable paths, network allowance, system paths, and temp directory. |
+| `(std security secret)` | Lifetime-scoped bytevector secrets. `make-secret`, `secret-use`, `secret-peek`, and `with-secret` ensure scoped values are consumed or wiped on exit, including exceptional exits. |
+| `(std security env)` | Environment and secret broker for child processes. Builds allowlisted child environments, refuses argv values containing registered secrets, injects named secrets explicitly, and produces audit-safe redacted summaries. |
 
 ---
 
@@ -542,83 +561,8 @@ These are known gaps documented as current limitations, not implementation promi
 - **No red team evaluation.** No independent adversarial testing has been performed.
 - **Secure memory is outside GC.** The `with-secure-region` API requires manual pointer arithmetic via `foreign-ref`/`foreign-set!`. There is no high-level typed interface.
 
-
 ---
 
-## Security Patterns & Threat-Model Checklist
-
-
-Scope: `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*`.
-
-## Findings
-
-### Critical: sibling build scripts can mutate the main Jerboa checkout
-
-The cross-build scripts in `jerboa-lsp` and `jerboa-awk` patch `$jerboa-home/lib` in place, delete compiled artifacts, then run `git checkout --` against `.ss` and `.sls` files. If `$jerboa-home` points at `/Users/user/mine/jerboa`, these scripts can silently discard uncommitted work in the main repo.
-
-- `/Users/user/mine/jerboa-lsp/build-jlsp-cross.ss:51`
-- `/Users/user/mine/jerboa-lsp/build-jlsp-freebsd-cross.ss:50`
-- `/Users/user/mine/jerboa-awk/build-jawk-cross.ss:173`
-
-### Critical: `jerboa-code` disables LLM safeguards while allowing broad local access
-
-`src/jcode/tool/external-llm.ss` adds provider flags such as `--dangerously-skip-permissions`, `--yolo`, and `--sandbox danger-full-access`. The macOS sandbox profile is allow-by-default, then the command is executed through the shell. That combination gives model-driven CLI processes broad filesystem and network reach.
-
-- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:53`
-- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:157`
-- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:215`
-
-### High: main repo has a confirmed syntax/build blocker
-
-`lib/std/compiler/pattern.ss` reaches EOF while still inside the `(library ...)` form. The Jerboa health check also reported balance failures in `lib/std/pipeline.ss`, `tests/test-pipeline.ss`, and `tests/test-security2-parsers.ss`.
-
-- `/Users/user/mine/jerboa/lib/std/compiler/pattern.ss:316`
-
-### High: `jerboa-inotify` exposes unchecked FFI buffer reads
-
-`jerboa_inotify_shim.c` casts `buf + offset` to `struct inotify_event *` without a buffer length or alignment check. A caller can read past the end of the buffer. The `read` wrapper also treats `EINTR` as a hard error instead of retrying.
-
-- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:33`
-- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:66`
-
-### High: `jerboa-wafter` has shell injection in the Wireshark converter
-
-`tools/wireshark-convert.ss` accepts `--out`, then builds `mkdir -p` with string concatenation and passes it to `system`. A crafted output directory can inject shell syntax.
-
-- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1095`
-- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1118`
-
-### Medium: `pipeline-timeout` can leave work running after timeout
-
-`pipeline-timeout` forks a worker, waits, then raises on timeout without joining or cancelling the worker. The worker can continue running and mutate captured state after the timeout path returns.
-
-- `/Users/user/mine/jerboa/lib/std/pipeline.ss:170`
-
-The related test does not actually exercise a failing inner stage:
-
-- `/Users/user/mine/jerboa/tests/test-pipeline.ss:164`
-
-### Medium: `jerboa-emacs` appears to use Gerbil process APIs
-
-Several Jerboa source files call `open-process` and `process-status`. Unless this repo injects compatibility wrappers at build time, those are not the Jerboa/Chez process APIs described by the project instructions.
-
-- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:110`
-- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:132`
-- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:1108`
-
-### Medium: main repo has raw `read` / FASL deserialization paths
-
-The Jerboa security scan found many raw `read` and FASL deserialization paths. These need source-by-source triage because untrusted `read` or FASL input is a deserialization risk.
-
-Examples:
-
-- `/Users/user/mine/jerboa/jerbuild.ss:308`
-- `/Users/user/mine/jerboa/lib/std/actor/distributed.ss:303`
-- `/Users/user/mine/jerboa/lib/std/fasl.ss:27`
-- `/Users/user/mine/jerboa/lib/std/net/grpc.ss:162`
-
-## Review Notes
-
-The review covered all matching `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*` directories with `rg`, manual source reads, git status inventory, and Jerboa MCP health/security/stale checks on the main repo plus key sibling repos.
-
-Full builds were not run because many worktrees are dirty and several build scripts write/delete generated artifacts across checkouts.
+Current dated security reviews are archived under [reviews/](reviews/). The
+2026-07-27 review that previously lived in this reference is now
+[reviews/2026-07-27-security-patterns-threat-model-checklist.md](reviews/2026-07-27-security-patterns-threat-model-checklist.md).
diff --git a/jerboa-native-rs/Cargo.lock b/jerboa-native-rs/Cargo.lock
index d2d19c7..7d24f94 100644
--- a/jerboa-native-rs/Cargo.lock
+++ b/jerboa-native-rs/Cargo.lock
@@ -2679,9 +2679,9 @@ dependencies = [
 
 [[package]]
 name = "spin"
-version = "0.9.8"
+version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
 
 [[package]]
 name = "spki"
diff --git a/lib/std/compiler/pattern.ss b/lib/std/compiler/pattern.ss
index c8e54b9..5802e6b 100644
--- a/lib/std/compiler/pattern.ss
+++ b/lib/std/compiler/pattern.ss
@@ -313,4 +313,4 @@
   (def (template-compile template)
     "Compile template into substitution function"
     (lambda (bindings)
-      (template-substitute* template bindings))))
\ No newline at end of file
+      (template-substitute* template bindings)))))
\ No newline at end of file
diff --git a/lib/std/security/restrict.ss b/lib/std/security/restrict.ss
index f66378b..73083fc 100644
--- a/lib/std/security/restrict.ss
+++ b/lib/std/security/restrict.ss
@@ -61,8 +61,7 @@
       string=? string<? string>? string<=? string>=?
       substring string->list list->string
       string-upcase string-downcase
-      string-copy number->string symbol->string
-      string->number string->symbol
+      string-copy
 
       ;; Characters
       char? char=? char<? char>?
diff --git a/mcp/server.ss b/mcp/server.ss
index e1b9458..2073fd4 100644
--- a/mcp/server.ss
+++ b/mcp/server.ss
@@ -4022,6 +4022,13 @@
        (if (or (>= j len) (delimiter? (string-ref source j)))
            j
            (loop (+ j 1))))]))
+(def (symbol-hex-escape-end source i)
+  (def source-size (string-length source))
+  (let loop ([j (+ i 2)])
+    (cond
+      [(>= j source-size) source-size]
+      [(char=? (string-ref source j) #\;) (+ j 1)]
+      [else (loop (+ j 1))])))
 
 (def (balance-scan source)
   (def stack '())
@@ -4047,6 +4054,13 @@
              [(char=? ch #\\) (loop (+ i 1) #t #t #f)]
              [(char=? ch #\") (loop (+ i 1) #f #f #f)]
              [else (loop (+ i 1) #t #f #f)])]
+          [(and (char=? ch #\\)
+                (< (+ i 1) len)
+                (or (char=? (string-ref source (+ i 1)) #\x)
+                    (char=? (string-ref source (+ i 1)) #\X)))
+           (let ([j (symbol-hex-escape-end source i)])
+             (set! col (+ col (- j i)))
+             (loop j #f #f #f))]
           [(char=? ch #\;)
            (set! col (+ col 1))
            (loop (+ i 1) #f #f #t)]
@@ -4133,6 +4147,14 @@
              [(char=? ch #\\) (loop (+ i 1) #t #t #f)]
              [(char=? ch #\") (loop (+ i 1) #f #f #f)]
              [else (loop (+ i 1) #t #f #f)])]
+          [(and (char=? ch #\\)
+                (< (+ i 1) len)
+                (or (char=? (string-ref source (+ i 1)) #\x)
+                    (char=? (string-ref source (+ i 1)) #\X)))
+           (let ([j (symbol-hex-escape-end source i)])
+             (display (substring source i j) out)
+             (set! col (+ col (- j i)))
+             (loop j #f #f #f))]
           [(char=? ch #\;)
            (set! col (+ col 1))
            (write-char ch out)
diff --git a/support/check-source-balance.ss b/support/check-source-balance.ss
new file mode 100644
index 0000000..9c4d491
--- /dev/null
+++ b/support/check-source-balance.ss
@@ -0,0 +1,164 @@
+#!chezscheme
+;;; Check tracked Jerboa/Chez source files for delimiter balance.
+;;; This mirrors the MCP balance checker used by AGENTS.md after .ss edits.
+
+(import (scheme))
+
+(define failures 0)
+(define checked 0)
+
+(define (read-file-string path)
+  (call-with-input-file path
+    (lambda (port) (get-string-all port))))
+
+(define (matching-opener closer)
+  (cond
+    [(char=? closer #\)) #\(]
+    [(char=? closer #\]) #\[]
+    [(char=? closer #\}) #\{]
+    [else closer]))
+
+(define (delimiter? ch)
+  (or (char-whitespace? ch)
+      (char=? ch #\()
+      (char=? ch #\))
+      (char=? ch #\[)
+      (char=? ch #\])
+      (char=? ch #\{)
+      (char=? ch #\})
+      (char=? ch #\")
+      (char=? ch #\;)))
+
+(define (char-literal-end source i)
+  (let* ([source-size (string-length source)]
+         [start (+ i 2)])
+    (cond
+      [(>= start source-size) source-size]
+      [(delimiter? (string-ref source start)) (+ start 1)]
+      [else
+       (let loop ([j start])
+         (if (or (>= j source-size) (delimiter? (string-ref source j)))
+             j
+             (loop (+ j 1))))])))
+(define (symbol-hex-escape-end source i)
+  (let ([source-size (string-length source)])
+    (let loop ([j (+ i 2)])
+      (cond
+        [(>= j source-size) source-size]
+        [(char=? (string-ref source j) #\;) (+ j 1)]
+        [else (loop (+ j 1))]))))
+
+(define (balance-scan source)
+  (let ([stack '()]
+        [errors '()]
+        [line 1]
+        [col 1]
+        [source-size (string-length source)])
+    (let loop ([i 0] [in-string #f] [escaped #f] [comment #f])
+      (when (< i source-size)
+        (let ([ch (string-ref source i)])
+          (cond
+            [(char=? ch #\newline)
+             (set! line (+ line 1))
+             (set! col 1)
+             (loop (+ i 1) in-string #f #f)]
+            [comment
+             (set! col (+ col 1))
+             (loop (+ i 1) in-string #f comment)]
+            [in-string
+             (set! col (+ col 1))
+             (cond
+               [escaped (loop (+ i 1) #t #f #f)]
+               [(char=? ch #\\) (loop (+ i 1) #t #t #f)]
+               [(char=? ch #\") (loop (+ i 1) #f #f #f)]
+               [else (loop (+ i 1) #t #f #f)])]
+            [(and (char=? ch #\\)
+                  (< (+ i 1) source-size)
+                  (or (char=? (string-ref source (+ i 1)) #\x)
+                      (char=? (string-ref source (+ i 1)) #\X)))
+             (let ([j (symbol-hex-escape-end source i)])
+               (set! col (+ col (- j i)))
+               (loop j #f #f #f))]
+            [(char=? ch #\;)
+             (set! col (+ col 1))
+             (loop (+ i 1) #f #f #t)]
+            [(char=? ch #\")
+             (set! col (+ col 1))
+             (loop (+ i 1) #t #f #f)]
+            [(and (char=? ch #\#)
+                  (< (+ i 1) source-size)
+                  (char=? (string-ref source (+ i 1)) #\\))
+             (let ([j (char-literal-end source i)])
+               (set! col (+ col (- j i)))
+               (loop j #f #f #f))]
+            [(or (char=? ch #\() (char=? ch #\[) (char=? ch #\{))
+             (set! stack (cons (list ch line col) stack))
+             (set! col (+ col 1))
+             (loop (+ i 1) #f #f #f)]
+            [(or (char=? ch #\)) (char=? ch #\]) (char=? ch #\}))
+             (if (or (null? stack)
+                     (not (char=? (caar stack) (matching-opener ch))))
+                 (set! errors
+                       (cons (string-append "unexpected "
+                                            (string ch)
+                                            " at "
+                                            (number->string line)
+                                            ":"
+                                            (number->string col))
+                             errors))
+                 (set! stack (cdr stack)))
+             (set! col (+ col 1))
+             (loop (+ i 1) #f #f #f)]
+            [else
+             (set! col (+ col 1))
+             (loop (+ i 1) #f #f #f)]))))
+    (for-each
+      (lambda (entry)
+        (set! errors
+              (cons (string-append "unclosed "
+                                   (string (car entry))
+                                   " at "
+                                   (number->string (cadr entry))
+                                   ":"
+                                   (number->string (caddr entry)))
+                    errors)))
+      stack)
+    (reverse errors)))
+
+(define (check-file path)
+  (guard (e [else
+             (set! failures (+ failures 1))
+             (fprintf (current-error-port)
+                      "source-balance: ~a: read error: ~a\n"
+                      path
+                      (condition-message e))])
+    (let ([errors (balance-scan (read-file-string path))])
+      (if (null? errors)
+          (set! checked (+ checked 1))
+          (begin
+            (set! failures (+ failures 1))
+            (for-each
+              (lambda (err)
+                (fprintf (current-error-port)
+                         "source-balance: ~a: ~a\n"
+                         path
+                         err))
+              errors))))))
+
+(define (main args)
+  (when (null? args)
+    (fprintf (current-error-port)
+             "source-balance: no files supplied\n")
+    (exit 2))
+  (for-each check-file args)
+  (if (zero? failures)
+      (begin
+        (printf "source-balance: checked ~a file(s)\n" checked)
+        (exit 0))
+      (begin
+        (fprintf (current-error-port)
+                 "source-balance: ~a file(s) failed\n"
+                 failures)
+        (exit 1))))
+
+(main (command-line-arguments))
diff --git a/tools/check-restrict-closure.ss b/tools/check-restrict-closure.ss
new file mode 100644
index 0000000..b9de8e5
--- /dev/null
+++ b/tools/check-restrict-closure.ss
@@ -0,0 +1,121 @@
+#!/usr/bin/env -S scheme --libdirs lib --script
+;;; check-restrict-closure.ss -- verify restricted eval allowlist closure.
+
+(import (jerboa prelude)
+        (std security restrict))
+
+(define errors '())
+(define warnings '())
+
+(define forbidden-exact
+  '(system process load eval getenv putenv fork-thread import library
+    call/cc call-with-current-continuation gensym read-eval
+    open-input-file open-output-file call-with-input-file call-with-output-file
+    delete-file rename-file file-exists? directory-list current-directory
+    compile compile-file compile-program foreign-procedure))
+
+(define escape-attempts
+  '(system-call "(system \"id\")"
+    eval-call "(eval '(+ 1 2))"
+    ffi-call "((foreign-procedure \"exit\" (int) void) 0)"
+    gensym-call "(gensym)"
+    file-output "(open-output-file \"/tmp/jerboa-restrict-escape\")"
+    callcc-call "(call/cc (lambda (k) (k 1)))"))
+
+(define (add-error msg) (set! errors (cons msg errors)))
+(define (add-warning msg) (set! warnings (cons msg warnings)))
+
+(define (string-prefix? prefix s)
+  (let ([n (string-length prefix)]
+        [m (string-length s)])
+    (and (>= m n) (string=? prefix (substring s 0 n)))))
+
+(define (string-suffix? suffix s)
+  (let ([n (string-length suffix)]
+        [m (string-length s)])
+    (and (>= m n) (string=? suffix (substring s (- m n) m)))))
+
+(define (symbol-prefix? prefix sym)
+  (string-prefix? prefix (symbol->string sym)))
+
+(define (symbol-open-file? sym)
+  (let ([s (symbol->string sym)])
+    (and (string-prefix? "open-" s) (string-suffix? "-file" s))))
+
+(define (forbidden-symbol? sym)
+  (or (memq sym forbidden-exact)
+      (symbol-prefix? "foreign-" sym)
+      (symbol-prefix? "compile" sym)
+      (symbol-open-file? sym)))
+
+(define (duplicates xs)
+  (let loop ([rest xs] [seen '()] [dups '()])
+    (cond
+      [(null? rest) (reverse dups)]
+      [(memq (car rest) seen) (loop (cdr rest) seen (cons (car rest) dups))]
+      [else (loop (cdr rest) (cons (car rest) seen) dups)])))
+
+(define (attempt-fails? env source)
+  (guard (exn [else #t])
+    (restricted-eval-string source env)
+    #f))
+
+(define (check-safe-bindings)
+  (let ([dups (duplicates safe-bindings)])
+    (unless (null? dups)
+      (add-error (format "safe-bindings contains duplicates: ~s" dups))))
+  (for-each
+    (lambda (sym)
+      (when (forbidden-symbol? sym)
+        (add-error (format "forbidden binding in safe-bindings: ~s" sym))))
+    safe-bindings))
+
+(define (check-environment-constructs)
+  (guard (exn [else
+               (add-error
+                 (format "make-restricted-environment failed: ~a"
+                         (with-output-to-string
+                           (lambda () (display-condition exn)))))
+               #f])
+    (make-restricted-environment)))
+
+(define (check-escape-attempts env)
+  (let loop ([rest escape-attempts])
+    (unless (null? rest)
+      (let ([label (car rest)]
+            [source (cadr rest)])
+        (unless (attempt-fails? env source)
+          (add-error (format "escape attempt unexpectedly succeeded: ~a" label)))
+        (loop (cddr rest))))))
+
+(define (check-allowed-smoke env)
+  (guard (exn [else
+               (add-error
+                 (format "allowed arithmetic/list smoke failed: ~a"
+                         (with-output-to-string
+                           (lambda () (display-condition exn)))))])
+    (let ([result (restricted-eval-string "(+ 1 (length '(a b c)))" env)])
+      (unless (= result 4)
+        (add-error (format "allowed smoke returned ~s, expected 4" result))))))
+
+(define (main)
+  (check-safe-bindings)
+  (let ([env (check-environment-constructs)])
+    (when env
+      (check-allowed-smoke env)
+      (check-escape-attempts env)))
+  (display "restrict-closure:\n")
+  (display "  safe-bindings: ") (write (length safe-bindings)) (newline)
+  (display "  forbidden-patterns: exact, foreign-*, compile*, open-*-file\n")
+  (display "  escape-attempts: ") (write (/ (length escape-attempts) 2)) (newline)
+  (for-each
+    (lambda (msg) (display "warning: ") (display msg) (newline))
+    (reverse warnings))
+  (for-each
+    (lambda (msg) (display "error: ") (display msg) (newline))
+    (reverse errors))
+  (if (null? errors)
+      (begin (display "restrict-closure: PASS\n") (exit 0))
+      (begin (display "restrict-closure: FAIL\n") (exit 1))))
+
+(main)
diff --git a/tools/security-audit.sh b/tools/security-audit.sh
index 63d27f8..27f1f50 100755
--- a/tools/security-audit.sh
+++ b/tools/security-audit.sh
@@ -49,8 +49,8 @@ done
 require_dir vendor/ChezScheme
 require_dir jerboa-native-rs
 
-if ! grep -q 'Apache License' LICENSE; then
-  fail "LICENSE does not appear to contain Apache License text"
+if ! grep -q 'MIT License' LICENSE; then
+  fail "LICENSE does not appear to contain MIT License text"
 fi
 if ! grep -q 'Chez Scheme' LICENSE-CHEZ; then
   fail "LICENSE-CHEZ does not identify Chez Scheme"