Security hardening and release readiness

ober

6fc0d2b4192922870e10ccc6bc2be8b674529d34

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6236f7d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+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 system tools
+        run: |
+          set -eu
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends build-essential ca-certificates curl git ripgrep
+
+      - name: Install cargo-audit
+        run: |
+          set -eu
+          cargo install cargo-audit --locked
+
+      - name: Verify
+        run: make verify
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..28a713e
--- /dev/null
+++ b/.github/workflows/security-baseline.yml
@@ -0,0 +1,35 @@
+name: Security Baseline
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  baseline:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Required release files
+        run: |
+          set -eu
+          test -f LICENSE
+          test -f SECURITY.md
+          test -f .gitignore
+          find . -maxdepth 1 -iname "README*" -type f | grep -q .
+
+      - name: High-confidence secret scan
+        run: |
+          set -eu
+          pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})"
+          matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"
+          if [ -n "$matches" ]; then
+            echo "$matches"
+            echo "High-confidence secret pattern found."
+            exit 1
+          fi
diff --git a/.gitignore b/.gitignore
index 26f8103..995f797 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,13 @@
 /jsecmon-analyze
 /jsecmon-collector
 /jsecmon-agent
+/jsecmon-telemetry
+
+# Release evidence is generated per candidate.
+/dist/
+
+# Local CI/bootstrap cache.
+/.jerboa/bin/
 
 # Stray Chez compile artifacts (build-binary.ss cleans these; ignore as backstop).
 *.so
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..3a50968
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,97 @@
+{
+  "version": 1,
+  "repo": "jerboa-secmon",
+  "extends": [
+    "jerboa:daemon",
+    "jerboa:security-monitor",
+    "jerboa:endpoint-agent",
+    "jerboa:network-service",
+    "jerboa:static-binary",
+    "jerboa:ffi-boundary",
+    "jerboa:crypto-boundary",
+    "jerboa:sqlite-store"
+  ],
+  "paths": {
+    "production": [
+      "Makefile",
+      "build-binary.ss",
+      "bin/**/*.ss",
+      "jsecmon/**/*.ss",
+      "typed/**/*.ss",
+      "tests/**/*.rs"
+    ],
+    "tests": [
+      "examples/**",
+      "tests/**"
+    ],
+    "generated": [
+      "build/**",
+      "dist/**",
+      "vendor/**",
+      "jsecmon-keygen",
+      "jsecmon-analyze",
+      "jsecmon-collector",
+      "jsecmon-agent",
+      "jsecmon-telemetry"
+    ],
+    "docs": [
+      "README.md",
+      "SECURITY.md",
+      "docs/**",
+      "*.md"
+    ]
+  },
+  "policy": {
+    "failOn": ["critical", "high"],
+    "crypto": {
+      "customCrypto": "deny",
+      "requireAuditedDependencies": true,
+      "requireKeySeparation": true,
+      "requireNonceRandomizationTests": true
+    },
+    "network": {
+      "requireAuthenticatedTransport": true,
+      "requireLoopbackTelemetryTests": true,
+      "requireConnectionLimits": true,
+      "requireDeterministicShutdown": true
+    },
+    "daemon": {
+      "requirePrivilegeDrop": true,
+      "requireSandboxPlan": true,
+      "requireReleaseEvidence": true,
+      "requireNoEmbeddedSecrets": true
+    },
+    "ffi": {
+      "requireSafeSharedObjectLoading": true,
+      "requireCollectSafeBlockingCalls": true,
+      "requireOwnershipDocumentation": true
+    },
+    "storage": {
+      "requireParameterizedSql": true,
+      "requireEncryptedLocalStore": true,
+      "requireTempCleanup": true
+    },
+    "releaseEvidence": {
+      "targetLoadProof": {
+        "proofFileEnv": "JSECMON_TARGET_LOAD_PROOF_FILE",
+        "requireEnv": "JSECMON_REQUIRE_TARGET_LOAD_PROOF",
+        "requiredMarkers": [
+          "production_load_status=current-run-recorded",
+          "agent_load_status=release-host-sustained-recorded",
+          "telemetry_load_status=release-host-sustained-recorded",
+          "collector_load_status=release-host-sustained-recorded"
+        ]
+      },
+      "targetTelemetryProof": {
+        "proofFileEnv": "JSECMON_TARGET_TELEMETRY_PROOF_FILE",
+        "requireEnv": "JSECMON_REQUIRE_TARGET_TELEMETRY_PROOF",
+        "requiredMarkers": [
+          "target_telemetry_status=target-evidence-recorded",
+          "privileged_monitor_status=target-evidence-recorded",
+          "listener_confinement_status=target-evidence-recorded"
+        ]
+      }
+    }
+  },
+  "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 7334905..20becfe 100644
--- a/Makefile
+++ b/Makefile
@@ -13,8 +13,15 @@ JERBOA_NATIVE_FEATURES ?= tls,crypto
 SCHEME ?= $(JERBOA)/.chez/bin/scheme
 BUILD  ?= build/rust
 TYPED  := $(wildcard typed/*.ss)
+DIST_DIR ?= dist/release-evidence
+REPRO_DIR ?= dist/reproducibility
+SOAK_DIR ?= dist/soak
+SOAK_TIMEOUT_SECONDS ?= 300
+SOAK_ITERATIONS ?= 8
+BINARY_SMOKE_TIMEOUT_SECONDS ?= 10
 
 .PHONY: vendor-deps ensure-jerboa ensure-jsqlite rust test ffi-demo kernels-check triage-check triage-store-check analytics-check detect-check storage-check entity-check threats-check geoip-check sigma-check yaml-rules-check buffer-check dns-sniffer-check suspicious-check netconn-check kernmod-check selinux-check container-check dns-servers-check sensitive-path-check dtrace-parse-check dtrace-runtime-check stealth-check ebpf-events-check ebpf-runtime-check proc-linux-check freebsd-parse-check event-meta-check config-check privdrop-check event-danger-check persistence-check file-change-check webshell-check platform-mounts-check analyze-cli-check collector-cli-check event-summary-check ioc-check frame-check correlate-check revshell-check cron-check logtamper-check detection-rules-check daemon-telemetry-check mux-telemetry-check ipaddr-check auth-check lolbin-check dga-check calendar-check monitor-process-check monitor-network-check monitor-files-check monitor-auth-check monitor-kernel-check monitor-cron-check monitor-container-check monitor-rootkit-check monitor-podman-check monitor-selinux-check monitor-lateral-check monitor-webshell-check monitor-revshell-check monitor-persistence-check monitor-logtamper-check monitor-dns-check monitor-manager-check event-json-check collector-check protocol-check event-codec-check local-store-check collector-pull-check agent-server-check checks native-runtime keygen analyze collector agent telemetry binaries clean
+.PHONY: security audit verify reproducibility-report release-evidence soak-check soak-evidence binary-smoke
 # Combined libdir path so sibling libraries `(jsecmon ...)` resolve to ./jsecmon
 # (a second --libdirs would replace, not append, the jerboa one).
 LIBDIRS := "$(CURDIR):$(JSQLITE_SRC):$(JERBOA)/lib"
@@ -624,6 +631,62 @@ checks: kernels-check ensure-jsqlite
 	$(SCHEME) --libdirs $(LIBDIRS) --script examples/event_json_check.ss
 	$(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/collector_check.ss
 
+# ── Release/security gates ───────────────────────────────────────────────────
+
+security:
+	scripts/security-check.sh
+
+audit: rust
+	@if ! cargo audit --version >/dev/null 2>&1; then \
+		echo "cargo audit is required. Install with: cargo install cargo-audit --locked"; \
+		exit 1; \
+	fi
+	cargo audit --file "$(BUILD)/Cargo.lock" -D warnings
+
+soak-check: rust ensure-jsqlite
+	cd $(BUILD) && cargo build --release
+	@i=1; while [ "$$i" -le "$(SOAK_ITERATIONS)" ]; do \
+	  echo "soak iteration $$i: agent poll server"; \
+	  $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/agent_server_check.ss; \
+	  echo "soak iteration $$i: mux telemetry listener"; \
+	  $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/mux_telemetry_check.ss; \
+	  echo "soak iteration $$i: collector persistence loop"; \
+	  $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/collector_check.ss; \
+	  i=$$((i + 1)); \
+	done
+
+soak-evidence:
+	SOAK_DIR="$(SOAK_DIR)" SOAK_ITERATIONS="$(SOAK_ITERATIONS)" SOAK_TIMEOUT_SECONDS="$(SOAK_TIMEOUT_SECONDS)" MAKE="$(MAKE)" bash scripts/soak-evidence.sh
+
+verify: security test checks audit
+
+reproducibility-report:
+	REPO_ROOT="$(CURDIR)" REPRO_DIR="$(REPRO_DIR)" MAKE="$(MAKE)" JERBOA="$(JERBOA)" JSQLITE_REPO="$(JSQLITE_REPO)" JSQLITE_SRC="$(JSQLITE_SRC)" SCHEME="$(SCHEME)" sh scripts/reproducibility-report.sh
+
+binary-smoke:
+	REPO_ROOT="$(CURDIR)" BINARY_SMOKE_TIMEOUT_SECONDS="$(BINARY_SMOKE_TIMEOUT_SECONDS)" sh scripts/binary-smoke.sh
+
+release-evidence: verify reproducibility-report
+	rm -rf "$(DIST_DIR)"
+	mkdir -p "$(DIST_DIR)"
+	$(MAKE) --no-print-directory soak-evidence
+	REPO_ROOT="$(CURDIR)" BINARY_SMOKE_TIMEOUT_SECONDS="$(BINARY_SMOKE_TIMEOUT_SECONDS)" sh scripts/binary-smoke.sh > "$(DIST_DIR)/binary-smoke.txt"
+	rm -rf "$(DIST_DIR)/soak"
+	cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak"
+	cp "$(SOAK_DIR)/soak-check.txt" "$(DIST_DIR)/soak-check.txt"
+	git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt"
+	git status --short > "$(DIST_DIR)/git-status.txt"
+	uname -a > "$(DIST_DIR)/build-environment.txt"
+	"$(SCHEME)" --version >> "$(DIST_DIR)/build-environment.txt"
+	cargo metadata --manifest-path "$(BUILD)/Cargo.toml" --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-typed.json"
+	cargo audit --file "$(BUILD)/Cargo.lock" -D warnings > "$(DIST_DIR)/rustsec-typed.txt"
+	shasum -a 256 jsecmon-keygen jsecmon-analyze jsecmon-collector jsecmon-agent jsecmon-telemetry > "$(DIST_DIR)/binaries-sha256.txt"
+	shasum -a 256 Makefile build-binary.ss scripts/build-binaries.sh scripts/binary-smoke.sh scripts/security-check.sh scripts/reproducibility-report.sh scripts/soak-evidence.sh .jerboa/security.json SECURITY.md docs/threat-model.md docs/deployment-security.md docs/release-evidence.md > "$(DIST_DIR)/release-inputs-sha256.txt"
+	{ for bin in jsecmon-keygen jsecmon-analyze jsecmon-collector jsecmon-agent jsecmon-telemetry; do echo "== $$bin =="; if command -v otool >/dev/null 2>&1; then otool -L "$$bin"; elif command -v ldd >/dev/null 2>&1; then ldd "$$bin"; else echo "no dynamic-linkage inspector found"; fi; done; } > "$(DIST_DIR)/native-linkage.txt"
+	rm -rf "$(DIST_DIR)/reproducibility"
+	cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility"
+	test "$$(grep '^status=' "$(DIST_DIR)/reproducibility/report.txt" | cut -d= -f2)" = "match"
+
 # ── Native binaries ───────────────────────────────────────────────────────────
 # The shippable artifacts are compiled, self-contained executables — never
 # `scheme --script` launchers. build-binary.ss whole-program-compiles a bin/*.ss
@@ -637,14 +700,14 @@ native-runtime: ensure-jerboa
 
 keygen: rust ensure-jsqlite
 	cd $(BUILD) && cargo build --release
-	JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/keygen.ss jsecmon-keygen
+	JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" JSECMON_BINARY_WPO=0 $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/keygen.ss jsecmon-keygen
 
 # secmon-analyze: query/detect/triage/risk over the SQLite store. Statically
 # links the Rust kernels (lolbin/dga scoring, calendar) like keygen; SQLite is
 # provided by vendored jsqlite.
 analyze: rust native-runtime ensure-jsqlite
 	cd $(BUILD) && cargo build --release
-	JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/analyze.ss jsecmon-analyze
+	JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/analyze.ss jsecmon-analyze
 
 # secmon-collector: pull/status/watch over the PSK-encrypted protocol; decrypts
 # ECIES events and prints (human/NDJSON) and/or stores them. Statically links the
@@ -652,17 +715,17 @@ analyze: rust native-runtime ensure-jsqlite
 # SQLite is available through vendored jsqlite when --db is used.
 collector: rust native-runtime ensure-jsqlite
 	cd $(BUILD) && cargo build --release
-	JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/collector.ss jsecmon-collector
+	JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" JSECMON_BINARY_WPO=0 $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/collector.ss jsecmon-collector
 
 # secmon-agent: Linux polling monitors + encrypted event buffer + poll server.
 # It loads the collector public key and PSK at runtime, never embeds secrets.
 agent: rust native-runtime ensure-jsqlite
 	cd $(BUILD) && cargo build --release
-	JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/agent.ss jsecmon-agent
+	JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/agent.ss jsecmon-agent
 
 telemetry: rust native-runtime ensure-jsqlite
 	cd $(BUILD) && cargo build --release
-	JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/telemetry.ss jsecmon-telemetry
+	JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/telemetry.ss jsecmon-telemetry
 
 # ── Experimental: Typed Jerboa → LLVM IR native backend ──────────────────────
 #
@@ -710,7 +773,8 @@ llvmir-clean:
 	rm -rf $(LLVMIR_DIR)
 
 # All shippable binaries.
-binaries: keygen analyze collector agent telemetry
+binaries:
+	REPO_ROOT="$(CURDIR)" JERBOA="$(JERBOA)" JSQLITE_REPO="$(JSQLITE_REPO)" JSQLITE_SRC="$(JSQLITE_SRC)" SCHEME="$(SCHEME)" BUILD="$(BUILD)" MAKE="$(MAKE)" sh scripts/build-binaries.sh
 
 clean:
 	rm -rf $(BUILD) $(LLVMIR_DIR)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3e35d97
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,49 @@
+# Security Policy
+
+This repository is part of the Jerboa ecosystem. Treat it as experimental unless the README and release notes explicitly state a stronger support level.
+
+Production claims require the release gates tracked in `~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` to be complete for this repository.
+
+Secmon-specific security documentation:
+
+- `docs/threat-model.md`
+- `docs/deployment-security.md`
+- `docs/release-evidence.md`
+
+## 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 documented build and test commands pass.
+- Dependency and native-code audits are clean, or accepted risks are documented.
+- Any FFI, filesystem, network, shell, or credential-handling behavior is documented.
+- Release evidence includes explicit local soak and target-OS production load
+  status. `production_load_status=blocked-not-run` is a production blocker.
+  Reviewed target proof may be attached with `JSECMON_TARGET_LOAD_PROOF_FILE`
+  and `JSECMON_TARGET_TELEMETRY_PROOF_FILE`, but required proof must be paired
+  with `JSECMON_REQUIRE_TARGET_LOAD_PROOF=1` or
+  `JSECMON_REQUIRE_TARGET_TELEMETRY_PROOF=1` and fail closed as
+  `local_loopback_status=blocked-target-proof` if missing or marker-incomplete.
+- Release evidence includes `binary-smoke.txt`; every shipped `jsecmon-*`
+  executable must pass its no-secret runtime smoke path, and macOS executables
+  must pass code-signature verification.
+- Agent and telemetry listener sockets use Secmon's TCP runtime wrapper, which
+  verifies nonblocking mode and sets close-on-exec before background accept
+  loops start.
+
+## Security Expectations
+
+- Do not commit secrets, private keys, tokens, production `.env` files, operational hostnames, customer/user data, or private infrastructure details.
+- Do not implement cryptography directly. Use audited libraries or delegated components.
+- Prefer Rust for byte-level parsing, FFI boundaries, crypto-adjacent integration, and sandbox setup.
+- Prefer Jerboa for policy, orchestration, configuration, routing, tests, and high-level behavior.
+- Document file I/O, shell execution, network access, FFI ownership/lifetime rules, and credential storage before publication.
+- Daemons and hostile-input parsers require threat models, fuzz harnesses, regression corpora, resource limits, sandboxing documentation, and review before public production use.
+- For `jsecmon-agent` and `jsecmon-telemetry`, never expose default listeners directly to the public Internet. Bind to a private management network or loopback/VPN path and enforce host firewall rules.
+- Treat SQLite stores, WAL/SHM sidecars, release evidence, and analyzer output as sensitive operational records.
+
+## 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/bin/agent.ss b/bin/agent.ss
index bf1068e..bb4d12e 100644
--- a/bin/agent.ss
+++ b/bin/agent.ss
@@ -1,7 +1,7 @@
 #!chezscheme
 ;;; jsecmon-agent — monitor locally, ECIES-buffer events, serve collector pulls.
 
-(import (except (chezscheme)
+(import (except (scheme)
                 make-hash-table hash-table?
                 sort sort!
                 printf fprintf
diff --git a/bin/analyze.ss b/bin/analyze.ss
index 3ef604f..4d23cf6 100644
--- a/bin/analyze.ss
+++ b/bin/analyze.ss
@@ -21,7 +21,7 @@
 ;;; in via (command-line-arguments) = (command db remaining…), i.e. secmon's
 ;;; args[1..]. SQLite persistence resolves through vendored jsqlite.
 
-(import (except (chezscheme)
+(import (except (scheme)
                 make-hash-table hash-table?
                 sort sort!
                 printf fprintf
@@ -66,7 +66,9 @@
         (only (jsecmon analyze-cli)
               parse-duration-ms parse-alert-sink parse-alert-sinks
               parse-flag-value has-flag is-json-format)
-        (only (jsecmon lolbin) score-json-cmdline lol-label-summary))
+        (only (jsecmon lolbin) score-json-cmdline lol-label-summary)
+        (only (std pkg util) mkdir-p)
+        (only (std os aproc) aproc-run/status*))
 
 ;; ── tiny output helpers ───────────────────────────────────────────────────────
 (def (println . parts) (for-each display parts) (newline))
@@ -74,6 +76,9 @@
   (let ((p (current-error-port)))
     (for-each (lambda (x) (display x p)) parts) (newline p)))
 (def (die . parts) (apply eprintln parts) (exit 1))
+(def (write-text-file/replace path writer)
+  (call-with-port (open-file-output-port path (file-options no-fail) (buffer-mode block))
+    writer))
 
 (def (sev-marker s)
   (cond ((string=? s "critical") "!!!") ((string=? s "high") "!! ")
@@ -299,31 +304,30 @@
       (let ((ids (compute-triaged-ids db filter)))
         (values (filter-set filter "exclude_event_ids" ids) (length ids)))))
 
-;; ── alert sinks (stdout / file / webhook / syslog) ────────────────────────────
-(def (shell-quote s) (string-append "'" (string-join (string-split s #\') "'\\''") "'"))
-
 (def (sink-dispatch sink line)
   (case (car sink)
     ((stdout) (println line) (ok #t))
     ((file)
      (let* ((path (cadr sink))
             (old (if (file-exists? path) (read-file-string path) "")))
-       (try (begin (call-with-output-file path
-                     (lambda (o) (display old o) (display line o) (newline o)) 'replace)
+       (try (begin (write-text-file/replace path
+                     (lambda (o) (display old o) (display line o) (newline o)))
                    (ok #t))
             (catch (e) (err (str "write " path ": " e))))))
     ((webhook syslog)
-     (let ((tmp (str "/tmp/jsecmon-alert-" (now-ms) "-" (random 100000))))
-       (call-with-output-file tmp (lambda (o) (display line o)) 'replace)
-       (let ((rc (system
+     (let-values (((out err rc)
                    (if (eq? (car sink) 'webhook)
-                       (str "curl -sS --max-time 10 -X POST "
-                            "-H 'Content-Type: application/json' --data-binary @"
-                            tmp " " (shell-quote (cadr sink)) " >/dev/null")
-                       (str "logger -t " (shell-quote (cadr sink))
-                            " -p auth.warning -f " tmp)))))
-         (when (file-exists? tmp) (delete-file tmp))
-         (if (= rc 0) (ok #t) (err (str (car sink) " exited " rc))))))
+                       (aproc-run/status*
+                        (list "curl" "-sS" "--max-time" "10" "-X" "POST"
+                              "-H" "Content-Type: application/json"
+                              "--data-binary" "@-" (cadr sink))
+                        'stdin: line
+                        'timeout-ms: 15000)
+                       (aproc-run/status*
+                        (list "logger" "-t" (cadr sink) "-p" "auth.warning")
+                        'stdin: line
+                        'timeout-ms: 15000))))
+       (if (= rc 0) (ok #t) (err (str (car sink) " exited " rc)))))
     (else (err "unknown sink"))))
 
 (def (dispatch-anomalies sinks anomalies)
@@ -1009,7 +1013,7 @@
   (let ((input (car args))
         (out-dir (or (parse-flag-value args "--out") (die "--out <yaml-dir> is required")))
         (dry (has-flag args "--dry-run")))
-    (unless dry (system (str "mkdir -p " (shell-quote out-dir))))
+    (unless dry (mkdir-p out-dir))
     (let* ((files (if (file-directory? input) (sigma-files input) (list input)))
            (results (map (lambda (f)
                            (cons f (let ((txt (try (read-file-string f) (catch (e) #f))))
@@ -1025,8 +1029,8 @@
                   (if dry
                       (begin (println "# " path " -> " (imported-rule-name rule) ".yml")
                              (println (imported-rule-yaml rule)))
-                      (call-with-output-file (path-join out-dir (str (imported-rule-name rule) ".yml"))
-                        (lambda (o) (display (imported-rule-yaml rule) o)) 'replace)))
+                      (write-text-file/replace (path-join out-dir (str (imported-rule-name rule) ".yml"))
+                        (lambda (o) (display (imported-rule-yaml rule) o)))))
                 (begin (set! skip (+ skip 1)) (eprintln "skip " path ": " (unwrap-err res))))))
         results)
       (eprintln ok-n "/" total " rules imported, " skip " skipped" (if dry " (dry-run)" "")))))
diff --git a/bin/collector.ss b/bin/collector.ss
index 64cb1a9..b4a1bba 100644
--- a/bin/collector.ss
+++ b/bin/collector.ss
@@ -29,7 +29,7 @@
 ;;; observable behaviour without a fiber runtime. Linux/live-only; the codecs it
 ;;; rests on are vector-tested, this orchestration is exercised by hand.
 
-(import (except (chezscheme)
+(import (except (scheme)
                 make-hash-table hash-table?
                 sort sort!
                 printf fprintf
@@ -74,6 +74,10 @@
   (let ((p (current-error-port)))
     (for-each (lambda (x) (display x p)) parts) (newline p)))
 (def (die . parts) (apply eprintln parts) (exit 1))
+(def (raise-message who msg)
+  (raise (condition
+          (make-who-condition who)
+          (make-message-condition msg))))
 
 ;; ── key loading (secmon load_key, minus the compile-time embedded fallback) ───
 ;; env var first; a short value (<64) starting with / or . is a key-file path,
@@ -112,9 +116,9 @@
       (if (>= off n) buf
           (let ((chunk (get-bytevector-n in (- n off))))
             (when (or (eof-object? chunk) (not (bytevector? chunk)))
-              (error 'recv-message "connection closed"))
+              (raise-message 'recv-message "connection closed"))
             (let ((k (bytevector-length chunk)))
-              (when (= k 0) (error 'recv-message "connection closed"))
+              (when (= k 0) (raise-message 'recv-message "connection closed"))
               (bytevector-copy! chunk 0 buf off k)
               (loop (+ off k))))))))
 
@@ -125,7 +129,7 @@
 ;; → (ok message) | (err string); throws only on a closed/oversize socket.
 (def (recv-message in transport-key)
   (let ((len (frame-read-length (read-exact in 4))))
-    (when (> len *max-msg*) (error 'recv-message "message too large"))
+    (when (> len *max-msg*) (raise-message 'recv-message "message too large"))
     (let ((dec (transport-decrypt transport-key (read-exact in len))))
       (if (not dec) (err "transport decryption failed")
           (message-from-bytes dec)))))
@@ -140,9 +144,11 @@
            (transport-key (derive-transport-key psk)))
       (let-values (((in out) (tcp-connect-binary addr port)))
         (let ((r (recv-message in transport-key)))
-          (when (err? r) (error 'connect (unwrap-err r)))
+          (when (err? r)
+            (raise-message 'connect (unwrap-err r)))
           (let ((msg (unwrap r)))
-            (unless (eq? (car msg) 'challenge) (error 'connect "expected challenge"))
+            (unless (eq? (car msg) 'challenge)
+              (raise-message 'connect "expected challenge"))
             (send-message out transport-key
                           (list 'challenge-response (respond-to-challenge auth-key (cadr msg))))
             (make-client in out transport-key)))))))
@@ -154,25 +160,25 @@
 (def (client-request c msg)                  ;; send one request, await one reply
   (send-message (client-out c) (client-tk c) msg)
   (let ((r (recv-message (client-in c) (client-tk c))))
-    (if (err? r) (error 'request (unwrap-err r)) (unwrap r))))
+    (if (err? r) (raise-message 'request (unwrap-err r)) (unwrap r))))
 
 ;; GetEventsAfter → the SerializedEvent list (or signal auth/error).
 (def (client-get-events-after c seq)
   (let ((msg (client-request c (list 'request (list 'get-events-after seq)))))
     (cond
       ((and (eq? (car msg) 'response) (eq? (car (cadr msg)) 'events)) (cdr (cadr msg)))
-      ((eq? (car msg) 'auth-failed) (error 'request "authentication failed"))
-      ((eq? (car msg) 'error) (error 'request (cadr msg)))
-      (#t (error 'request "unexpected response")))))
+      ((eq? (car msg) 'auth-failed) (raise-message 'request "authentication failed"))
+      ((eq? (car msg) 'error) (raise-message 'request (cadr msg)))
+      (#t (raise-message 'request "unexpected response")))))
 
 ;; Status → (buffered latest-seq uptime db-count db-latest).
 (def (client-get-status c)
   (let ((msg (client-request c (list 'request (list 'status)))))
     (cond
       ((and (eq? (car msg) 'response) (eq? (car (cadr msg)) 'status)) (cdr (cadr msg)))
-      ((eq? (car msg) 'auth-failed) (error 'request "authentication failed"))
-      ((eq? (car msg) 'error) (error 'request (cadr msg)))
-      (#t (error 'request "unexpected response")))))
+      ((eq? (car msg) 'auth-failed) (raise-message 'request "authentication failed"))
+      ((eq? (car msg) 'error) (raise-message 'request (cadr msg)))
+      (#t (raise-message 'request "unexpected response")))))
 
 ;; ── per-event decrypt → the monitor row-hash (or err) ─────────────────────────
 (def (decrypt-event recipient-secret sev)
diff --git a/bin/keygen.ss b/bin/keygen.ss
index bcc8255..5d08a86 100644
--- a/bin/keygen.ss
+++ b/bin/keygen.ss
@@ -12,7 +12,7 @@
 ;;; loads its public key + PSK from config paths/env at runtime (see config.ss),
 ;;; it does not embed them at compile time the way the Rust agent did.
 
-(import (except (chezscheme)
+(import (except (scheme)
                 make-hash-table hash-table?
                 sort sort!
                 printf fprintf
diff --git a/bin/telemetry.ss b/bin/telemetry.ss
index 8e2cd02..d2fa1f1 100644
--- a/bin/telemetry.ss
+++ b/bin/telemetry.ss
@@ -5,7 +5,7 @@
 ;;; telemetry, stores accepted rows in the jsecmon events database, and returns
 ;;; encrypted ACK/ERROR mux frames.
 
-(import (except (chezscheme)
+(import (except (scheme)
                 make-hash-table hash-table?
                 sort sort!
                 printf fprintf
@@ -23,7 +23,7 @@
               mux-telemetry-server-start!
               mux-telemetry-server-port))
 
-(def *default-listen* "0.0.0.0:31338")
+(def *default-bind* "0.0.0.0:31338")
 
 (def (println . parts) (for-each display parts) (newline))
 (def (eprintln . parts)
@@ -40,7 +40,7 @@
   (eprintln "  SECMON_PSK                  Pre-shared key (hex) or path to key file")
   (eprintln "  PSK                         Alias accepted from keygen output")
   (eprintln "  SECMON_PSK_FILE             Path to PSK hex file")
-  (eprintln "  SECMON_TELEMETRY_LISTEN     Listen address, default " *default-listen*)
+  (eprintln "  SECMON_TELEMETRY_LISTEN     Listen address, default " *default-bind*)
   (eprintln "  SECMON_DB_PATH              Events DB path"))
 
 (def (arg? flag argv) (and (member flag argv) #t))
@@ -119,7 +119,7 @@
       (exit 0))
     (let* ((listen (or (flag-value argv "--listen")
                        (getenv "SECMON_TELEMETRY_LISTEN")
-                       *default-listen*))
+                       *default-bind*))
            (db-path (or (flag-value argv "--db")
                         (local-db-path getenv (runtime-platform))))
            (psk (decode-32 "PSK" (load-psk-hex)))
diff --git a/build-binary.ss b/build-binary.ss
index 4b98cea..773bf96 100644
--- a/build-binary.ss
+++ b/build-binary.ss
@@ -23,7 +23,8 @@
 ;;; (e.g. (jerboa core)) come back in compile-whole-program's "missing" list and
 ;;; are bundled into the boot file instead.
 
-(import (chezscheme))
+(import (scheme)
+        (only (std security taint) safe-delete-file))
 
 ;; ── args ─────────────────────────────────────────────────────────────────────
 (define args (cdr (command-line)))
@@ -36,13 +37,181 @@
       entry))
 (define prog-so (string-append output "-all.so"))
 
+(define (write-file/replace path writer)
+  (call-with-port (open-file-output-port path (file-options no-fail)
+                                        (buffer-mode block)
+                                        (native-transcoder))
+    writer))
+
+(define (try-load-shared-object name)
+  (guard (e [(condition? e) #f])
+    (load-shared-object name)
+    #t))
+
+(define tracked-foreign-alloc foreign-alloc)
+
+(define *process-ffi-loaded?* #f)
+(define c-posix-spawnp #f)
+(define c-waitpid #f)
+
+(define (resolve-process-ffi!)
+  (unless *process-ffi-loaded?*
+    (or (try-load-shared-object #f)
+        (try-load-shared-object "/usr/lib/libSystem.B.dylib")
+        (try-load-shared-object "libc.so.6")
+        (try-load-shared-object "libc.so.7")
+        (try-load-shared-object "libc.so"))
+    (set! c-posix-spawnp
+          (guard (e [(condition? e) #f])
+            (foreign-procedure "posix_spawnp"
+                               (void* string void* void* void* void*)
+                               int)))
+    (set! c-waitpid
+          (guard (e [(condition? e) #f])
+            (foreign-procedure __collect_safe
+                               "waitpid" (int void* int) int)))
+    (set! *process-ffi-loaded?*
+          (and c-posix-spawnp c-waitpid)))
+  *process-ffi-loaded?*)
+
+(define (alloc-cstring s)
+  ;; The returned pointer is owned by the argv bundle and released by
+  ;; free-c-argv! after posix_spawnp returns.
+  (let* ([bv (string->utf8 s)]
+         [len (bytevector-length bv)]
+         [ptr (tracked-foreign-alloc (+ len 1))])
+    (let loop ([i 0])
+      (when (< i len)
+        (foreign-set! 'unsigned-8 ptr i (bytevector-u8-ref bv i))
+        (loop (+ i 1))))
+    (foreign-set! 'unsigned-8 ptr len 0)
+    ptr))
+
+(define (alloc-c-argv argv)
+  (let* ([argc (length argv)]
+         [ptr-size (foreign-sizeof 'void*)]
+         [argv-ptr (tracked-foreign-alloc (* (+ argc 1) ptr-size))])
+    (let loop ([xs argv] [i 0] [allocated '()])
+      (if (null? xs)
+          (begin
+            (foreign-set! 'void* argv-ptr (* i ptr-size) 0)
+            (cons argv-ptr (reverse allocated)))
+          (let ([arg-ptr (alloc-cstring (car xs))])
+            (foreign-set! 'void* argv-ptr (* i ptr-size) arg-ptr)
+            (loop (cdr xs) (+ i 1) (cons arg-ptr allocated)))))))
+
+(define (free-c-argv! bundle)
+  (for-each foreign-free (cdr bundle))
+  (foreign-free (car bundle)))
+
+(define child-environment-variable-names
+  '("HOME"
+    "PATH"
+    "TMPDIR"
+    "JERBOA_HOME"
+    "JSQLITE_SRC"
+    "JSQLITE_DIR"
+    "CHEZ_DIR"
+    "SCHEME"
+    "DYLD_LIBRARY_PATH"
+    "LD_LIBRARY_PATH"
+    "SDKROOT"
+    "DEVELOPER_DIR"
+    "MACOSX_DEPLOYMENT_TARGET"
+    "CARGO_HOME"
+    "RUSTUP_HOME"
+    "SSL_CERT_FILE"
+    "SSL_CERT_DIR"))
+
+(define (child-environment-assignments)
+  (let loop ([names child-environment-variable-names] [out '()])
+    (if (null? names)
+        (reverse out)
+        (let ([value (getenv (car names))])
+          (loop (cdr names)
+                (if value
+                    (cons (string-append (car names) "=" value) out)
+                    out))))))
+
+(define (alloc-c-envp)
+  (alloc-c-argv (child-environment-assignments)))
+
+(define (wait-status->exit-code status)
+  (let ([signal (bitwise-and status #x7f)])
+    (cond
+      [(= signal 0) (bitwise-arithmetic-shift-right status 8)]
+      [(= signal #x7f) 1]
+      [else (+ 128 signal)])))
+
+(define (run-command/status argv)
+  (if (null? argv)
+      1
+      (if (not (resolve-process-ffi!))
+          255
+          (let ([bundle (alloc-c-argv argv)]
+                [env-bundle (alloc-c-envp)]
+                [pid-ptr (tracked-foreign-alloc (foreign-sizeof 'int))]
+                [status-ptr (tracked-foreign-alloc (foreign-sizeof 'int))])
+            (let ([spawn-rc
+                   (c-posix-spawnp pid-ptr
+                                    (car argv)
+                                    0
+                                    0
+                                    (car bundle)
+                                    (car env-bundle))])
+              (let ([rc
+                     (if (= spawn-rc 0)
+                         (let ([pid (foreign-ref 'int pid-ptr 0)])
+                           (let ([waited (c-waitpid pid status-ptr 0)])
+                             (if (= waited pid)
+                                 (wait-status->exit-code
+                                  (foreign-ref 'int status-ptr 0))
+                                 255)))
+                         255)])
+                (free-c-argv! bundle)
+                (free-c-argv! env-bundle)
+                (foreign-free pid-ptr)
+                (foreign-free status-ptr)
+                rc))))))
+
+(define (run-command label argv)
+  (let ([rc (run-command/status argv)])
+    (unless (= rc 0)
+      (printf "Error: ~a failed (exit ~a)\n" label rc)
+      (exit 1))))
+
+(define (bounded-positive-integer value default)
+  (if value
+      (let ([n (string->number value)])
+        (if (and (integer? n) (> n 0) (< n 10)) n default))
+      default))
+
+(define (wpo-helper-attempts)
+  (bounded-positive-integer (getenv "JSECMON_WPO_HELPER_ATTEMPTS") 2))
+
+(define (run-wpo-helper argv)
+  (let ([max-attempts (wpo-helper-attempts)])
+    (let loop ([attempt 1])
+      (let ([rc (run-command/status argv)])
+        (cond
+          [(= rc 0) #t]
+          [(< attempt max-attempts)
+           (printf "  WPO helper failed (exit ~a); retrying (~a/~a)\n"
+                   rc (+ attempt 1) max-attempts)
+           (when (file-exists? prog-so) (safe-delete-file prog-so))
+           (when (file-exists? wpo-missing-file) (safe-delete-file wpo-missing-file))
+           (loop (+ attempt 1))]
+          [else
+           (printf "Error: WPO helper failed (exit ~a)\n" rc)
+           (exit 1)])))))
+
 ;; ── helper: binary file → C unsigned-char array header ────────────────────────
 (define (file->c-header input-path output-path array-name size-name)
   (let* ([port (open-file-input-port input-path)]
          [data (get-bytevector-all port)]
          [size (bytevector-length data)])
     (close-port port)
-    (call-with-output-file output-path
+    (write-file/replace output-path