Security hardening and release readiness

ober

a6231fc11475b67caa8d528d7a70f4ea058c4713

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6f348d8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+name: CI
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  JERBOA_VERSION: v0.2.3
+  JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild
+
+jobs:
+  build-and-test:
+    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 git curl ca-certificates
+
+      - name: Install Jerboa toolchain
+        run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
+
+      - name: Release evidence
+        run: make release-evidence
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..1f61575
--- /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,}|glpat-[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 d00bfe3..5c9d25b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,12 @@ jgl-*-main.c
 # Cross-build isolated object dir
 /.build/
 
+# Release evidence
+/dist/
+.jerboa/*
+!.jerboa/
+!.jerboa/security.json
+
 # Default export output
 /issues/
 
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..387bb47
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,49 @@
+{
+  "version": 1,
+  "repo": "jerboa-gitlab",
+  "extends": ["jerboa:cli", "jerboa:network-client", "jerboa:credentialed-client"],
+  "paths": {
+    "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "Makefile"],
+    "tests": ["test/**", "tests/**", "**/*-test.ss"],
+    "generated": ["build/**", "dist/**", "target/**", "*.so", "*.dylib", "*.wpo", "*-main.c", "*_boot.h", "*_program.h"],
+    "vendor": ["vendor/**", "third_party/**"],
+    "docs": ["README.md", "docs/**", "*.md"]
+  },
+  "policy": {
+    "failOn": ["critical", "high"],
+    "imports": { "directChezscheme": "deny" },
+    "ffi": { "allowed": false },
+    "process": { "shellInterpolation": "deny" },
+    "network": { "requireTimeouts": true, "rawServiceOutputEvidence": "deny" },
+    "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false },
+    "secrets": { "forbidLogging": true, "leastPrivilegeOAuthScopes": true },
+    "credentials": {
+      "plaintextCache": "deny",
+      "rawCredentialEvidence": "deny",
+      "preferReadApiScope": true
+    },
+    "targetEvidence": {
+      "gitlabProof": "fail-closed",
+      "requiredMarkers": [
+        "gitlab_public_project_smoke_status=target-evidence-recorded",
+        "gitlab_private_project_smoke_status=target-evidence-recorded",
+        "least_privilege_token_scope_status=confirmed",
+        "token_source_hygiene_status=confirmed",
+        "api_error_redaction_status=target-evidence-recorded",
+        "export_attachment_redaction_status=target-evidence-recorded",
+        "credential_redaction_status=target-evidence-recorded",
+        "target_platform_smoke_status=target-evidence-recorded",
+        "production_credential_review_status=current-run-recorded",
+        "raw_api_output_status=not-preserved",
+        "sensitive_artifact_policy=no-gitlab-tokens-private-project-paths-issue-exports-attachments-or-raw-api-responses-preserved"
+      ]
+    }
+  },
+  "suppressions": [
+    {
+      "rule": "missing-taint-check-at-sink",
+      "path": "build-jgl-cross.ss",
+      "reason": "The cross-release build script invokes a configured compiler and only after shell-quoting path inputs."
+    }
+  ]
+}
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 ac6cc53..8a1c593 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,25 @@
 # jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust
-# crate, so building jerboa-gitlab needs only jerbuild + a C compiler + cargo —
-# no jerboa source checkout. jerboa-native provides Rustls HTTPS (tls,crypto).
-JERBUILD ?= jerbuild
-JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
-ifeq ($(JH),)
-$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild)
-endif
-
-NATIVE_DIR := $(JH)/jerboa-native-rs/target/release
-LIBDIRS    := --libdirs lib:$(JH)/lib
-JEXEC      := $(JERBUILD) exec $(LIBDIRS)
+# crate, so building jerboa-gitlab needs only jerbuild + a C compiler + cargo.
+# If no jerbuild is found, build/test targets fetch the release toolchain into
+# .jerboa/bin using support/ensure-jerboa.sh.
+JERBOA_VERSION ?= v0.2.3
+JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
+JERBUILD ?= $(shell if [ -x ./jerbuild ]; then echo ./jerbuild; \
+	elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \
+	elif [ -x ../jerboa/dist/jerbuild ]; then echo ../jerboa/dist/jerbuild; \
+	elif [ -x ../jerboa/jerbuild ]; then echo ../jerboa/jerbuild; \
+	elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; \
+	else echo "$(JERBOA_TOOL_DIR)/jerbuild"; fi)
+JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null)
+
+NATIVE_DIR = $(JH)/jerboa-native-rs/target/release
+LIBDIRS    = --libdirs lib:$(JH)/lib
+JEXEC      = "$(JERBUILD)" exec $(LIBDIRS)
 BIN        := jgl
 BIN_DIR    := $(HOME)/.local/bin
+DIST_DIR ?= $(CURDIR)/dist
+RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence
+TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence
 # The std net modules dlopen libjerboa_native in interpreter mode; point the
 # loader at jerbuild's cache for `make run`/`make test`.
 DYLD := DYLD_FALLBACK_LIBRARY_PATH="$(NATIVE_DIR)" LD_LIBRARY_PATH="$(NATIVE_DIR):$$LD_LIBRARY_PATH"
@@ -30,28 +38,117 @@ MUSL_TARGET   := x86_64-unknown-linux-musl
 MUSL_CC       ?= x86_64-linux-musl-gcc
 NATIVE_MUSL_A := $(JH)/jerboa-native-rs/target/$(MUSL_TARGET)/release/libjerboa_native.a
 
-.PHONY: all build binary run test install clean help cross-linux
+.PHONY: all build binary run test native-runtime security sbom reproducibility-report target-evidence target-evidence-selftest release-evidence install clean help cross-linux ensure-jerboa-tools
 .DEFAULT_GOAL := help
 
 all: binary
 
+ensure-jerboa-tools:
+	@if "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then \
+	  echo "=== Using Jerboa toolchain: $(JERBUILD) ==="; \
+	elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && "$(JERBOA_TOOL_DIR)/jerbuild" --jerboa-home >/dev/null 2>&1; then \
+	  echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \
+	elif command -v jerbuild >/dev/null 2>&1 && jerbuild --jerboa-home >/dev/null 2>&1; then \
+	  echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \
+	else \
+	  echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \
+	  sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \
+	fi
+	@"$(JERBUILD)" --jerboa-home >/dev/null || { \
+	  echo "ERROR: Jerboa toolchain is unavailable; set JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>" >&2; \
+	  exit 1; \
+	}
+
+native-runtime: ensure-jerboa-tools
+	@command -v cargo >/dev/null 2>&1 || { echo "ERROR: cargo not found on PATH."; exit 1; }
+	cargo build --release --manifest-path "$(JH)/jerboa-native-rs/Cargo.toml" --no-default-features --features tls,crypto
+
 # Standalone native binary via .jerbuild (entry jgl.ss -> jgl).
 # Two passes: pass 1 cargo-builds jerboa-native into jerbuild's cache; we then
 # regenerate the FFI symbol list (platform-correct) and relink.
-binary:
+binary: ensure-jerboa-tools
 	@touch support/ffi-symbols.gen
-	$(JERBUILD) build
-	sh support/gen-ffi-symbols.sh
-	$(JERBUILD) build
+	"$(JERBUILD)" build
+	JERBUILD="$(JERBUILD)" sh support/gen-ffi-symbols.sh
+	"$(JERBUILD)" build
 
 build: binary
 
 run: binary
 	$(DYLD) ./$(BIN) $(ARGS)
 
-test:
+test: native-runtime
 	$(DYLD) $(JEXEC) test/test-jgl.ss
 
+security:
+	@set -eu; \
+	pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gldt-[A-Za-z0-9_-]{20,}|glrt-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|ASIA[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; \
+	echo "secret_scan_status=pass"
+
+sbom: ensure-jerboa-tools
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	FFI_SYMBOLS="support/ffi-symbols.gen" \
+	BIN="$(BIN)" \
+	sh scripts/sbom.sh
+
+reproducibility-report: ensure-jerboa-tools
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	MAKE="$(MAKE)" \
+	BIN="$(BIN)" \
+	sh scripts/reproducibility-report.sh
+
+target-evidence:
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" \
+	sh scripts/target-evidence.sh
+
+target-evidence-selftest:
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	sh scripts/test-target-evidence.sh
+
+release-evidence:
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)" "$(TARGET_EVIDENCE_DIR)" "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/sbom"
+	@mkdir -p "$(RELEASE_EVIDENCE_DIR)"
+	@echo "==> Collecting git status"
+	@git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt"
+	@git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-head.txt" 2>/dev/null || true
+	@echo "==> Running high-confidence secret scan"
+	@$(MAKE) security > "$(RELEASE_EVIDENCE_DIR)/security.log" 2>&1
+	@echo "==> Running tests"
+	@$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1
+	@echo "==> Building binary"
+	@$(MAKE) binary > "$(RELEASE_EVIDENCE_DIR)/binary.log" 2>&1
+	@echo "==> Running binary help smoke"
+	@./$(BIN) --help > "$(RELEASE_EVIDENCE_DIR)/binary-help.log" 2>&1
+	@echo "==> Running target evidence selftest"
+	@$(MAKE) target-evidence-selftest > "$(RELEASE_EVIDENCE_DIR)/target-evidence-selftest.log" 2>&1
+	@echo "==> Recording target GitLab evidence"
+	@$(MAKE) target-evidence > "$(RELEASE_EVIDENCE_DIR)/target-evidence.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/target-evidence"
+	@cp -R "$(TARGET_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/target-evidence"
+	@echo "==> Running reproducibility report"
+	@$(MAKE) reproducibility-report > "$(RELEASE_EVIDENCE_DIR)/reproducibility.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/reproducibility"
+	@cp -R "$(DIST_DIR)/reproducibility" "$(RELEASE_EVIDENCE_DIR)/reproducibility"
+	@echo "==> Generating SBOM"
+	@$(MAKE) sbom > "$(RELEASE_EVIDENCE_DIR)/sbom.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom"
+	@cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom"
+	@find jgl.ss build*.ss lib test support docs scripts .github .jerboa/security.json -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256"
+	@echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)"
+
 install: binary
 	mkdir -p $(BIN_DIR)
 	install -m 0755 $(BIN) $(BIN_DIR)/$(BIN)
@@ -59,7 +156,7 @@ install: binary
 
 # Cross-compile macOS -> Linux x86_64 musl (static). Builds the rustls native
 # crate for musl, lists its FFI symbols, then cross-links jgl-linux-amd64.
-cross-linux:
+cross-linux: ensure-jerboa-tools
 	cd $(JH)/jerboa-native-rs && \
 	  RUSTC="$$(rustup which --toolchain $(RUSTUP_TC) rustc)" \
 	  CC_x86_64_unknown_linux_musl=$(MUSL_CC) \
@@ -76,6 +173,7 @@ cross-linux:
 clean:
 	rm -f $(BIN) jgl-linux-amd64 jgl-*main.c *.h *.boot *.wp.so
 	find . \( -name '*.so' -o -name '*.wpo' -o -name '*.tarm64osx' -o -name '*.ta6le' \) -delete 2>/dev/null || true
+	rm -rf dist
 
 help:
 	@echo "jerboa-gitlab -- the jgl GitLab CLI (jerbuild + cargo + cc)"
@@ -83,6 +181,7 @@ help:
 	@echo "  make binary           Build the standalone ./jgl"
 	@echo "  make run ARGS='...'    Build + run ./jgl"
 	@echo "  make test             Run the test suite"
+	@echo "  make release-evidence Run test/build/security/SBOM/reproducibility gate"
 	@echo "  make install          Build + install to ~/.local/bin/jgl"
 	@echo "  make cross-linux      Cross-build jgl-linux-amd64 (musl static, rustls)"
 	@echo "  make clean            Remove build artifacts"
diff --git a/README.md b/README.md
index 5470c34..383c0f9 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ HTTPS). You need `jerbuild`, a C compiler, and `cargo`.
 make binary       # -> ./jgl
 make install      # -> ~/.local/bin/jgl
 make test         # run the unit tests
+make release-evidence
 ```
 
 The same `make binary` works natively on macOS, Linux, and FreeBSD. To
@@ -69,6 +70,17 @@ jgl <command> [options]
 A token with `read_api` scope is needed for private projects; public projects
 read without one.
 
+## Security
+
+`jgl` can read private GitLab project metadata, issues, comments, CI status, and
+attachments. Prefer a `read_api` token, avoid passing tokens in argv on shared
+systems, and treat `jgl export` output as sensitive. Credential and export
+handling are documented in [`docs/credential-handling.md`](docs/credential-handling.md).
+Native dependency provenance is documented in
+[`docs/dependency-provenance.md`](docs/dependency-provenance.md), and local
+release evidence is documented in
+[`docs/release-evidence.md`](docs/release-evidence.md).
+
 ### Configuration
 
 `jgl` resolves each setting with the precedence **flag › environment ›
@@ -123,7 +135,9 @@ Each `README.md` has the title, metadata (state, author, assignees, labels,
 dates, URL), the description, and non-system comments. When the project is given
 as a path (`group/proj`), referenced `/uploads/` attachments are downloaded into
 `attachments/` and the links are rewritten to point at the local copies; if a
-download fails the original link is left intact.
+download fails the original link is left intact. Issue directory names and
+attachment filenames derived from API responses are sanitized before local
+filesystem writes.
 
 ## Cross-compilation
 
@@ -166,4 +180,4 @@ test/test-jgl.ss           unit tests (pure logic)
 
 ## License
 
-MIT.
+Apache-2.0.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..396b89d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,48 @@
+# Security Policy
+
+`jerboa-gitlab` is a GitLab API CLI and issue exporter. It handles GitLab access
+tokens and can write private issue data to disk, so public release requires a
+clean security gate even though it is not a daemon.
+
+## Supported Status
+
+No public production-support commitment exists yet. Treat current builds as
+experimental until the release checklist in `~/Release-plan.md` and
+`~/mine/jerboa-production-readiness.md` is complete for this repository.
+
+## Security Expectations
+
+- Use the narrowest GitLab token scope possible. `read_api` is expected for
+  private read-only project usage.
+- Prefer `GITLAB_TOKEN` from a secret manager or a mode-0600 config file over
+  passing tokens in argv.
+- Do not commit `~/.gitlab.yaml`, GitLab tokens, exported issue archives,
+  downloaded attachments, private project paths, or private hostnames.
+- Treat `jgl export` output as sensitive. It may contain issue bodies, comments,
+  labels, usernames, project URLs, and attachments.
+- Keep raw GitLab API response bodies out of logs and errors. Runtime errors
+  redact response bodies and strip query strings from failed API paths.
+- Production release requires marker-complete live GitLab target evidence
+  through `JERBOA_GITLAB_TARGET_PROOF_FILE`; setting
+  `JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1` makes missing or incomplete proof fail
+  closed.
+- Target proof files must contain only status markers and sanitized reviewer
+  notes. Do not preserve GitLab tokens, `PRIVATE-TOKEN` headers, bearer
+  headers, private project paths, exported issue archives, attachments, private
+  hostnames, or raw API response bodies.
+- Release builds must pass `make release-evidence`, which runs tests, binary
+  build and help smoke, high-confidence secret scan, SBOM/dependency capture,
+  cargo audit when installed, and repeated binary/FFI-symbol reproducibility
+  checks.
+
+Credential and export details are documented in
+[`docs/credential-handling.md`](docs/credential-handling.md).
+Native dependency and local release-evidence details are documented in
+[`docs/dependency-provenance.md`](docs/dependency-provenance.md) and
+[`docs/release-evidence.md`](docs/release-evidence.md).
+
+## 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/build-jgl-cross.ss b/build-jgl-cross.ss
index 058c79d..2765973 100644
--- a/build-jgl-cross.ss
+++ b/build-jgl-cross.ss
@@ -29,8 +29,6 @@
 ;;;
 ;;; Produces: jgl-linux-amd64 (static Linux x86_64 ELF).
 
-(import (chezscheme))
-
 ;; ── Params ──────────────────────────────────────────────────────────────────
 (define jerboa-home (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa"))
 (define cross-prefix (format "~a/.chez-cross-ta6le" jerboa-home))
@@ -110,6 +108,32 @@
 (printf "    cross csv-dir: ~a~n    native .a:     ~a~n    symbols:       ~a jerboa_* + ~a libc~n    output:        ~a~n~n"
         cross-csv-dir native-a (length jerboa-symbols) (length libc-symbols) output)
 
+(define (shell-quote s)
+  (let ([out (open-output-string)])
+    (write-char #\' out)
+    (let loop ([chars (string->list s)])
+      (unless (null? chars)
+        (let ([ch (car chars)])
+          (if (char=? ch #\')
+            (display "'\\''" out)
+            (write-char ch out)))
+        (loop (cdr chars))))
+    (write-char #\' out)
+    (get-output-string out)))
+
+(define (run-safe-system! who safe-cmd)
+  (let ([status (system safe-cmd)]) ; jerboa-security: suppress missing-taint-check-at-sink -- trusted cross-release build command after path shell-quoting
+    (unless (zero? status)
+      (error who "command failed" safe-cmd status))))
+
+(define (remove-build-dir! dir)
+  (run-safe-system! 'build-jgl-cross
+    (string-append "rm -rf " (shell-quote dir))))
+
+(define (reset-build-dir! dir)
+  (run-safe-system! 'build-jgl-cross
+    (string-append "rm -rf " (shell-quote dir) " && mkdir -p " (shell-quote dir))))
+
 ;; ── Stage 0.5: isolate compiled objects ────────────────────────────────────
 ;; Never patch source libraries in place. Chez maps each library source dir to
 ;; a writable object dir; point them ALL at obj-dir so the ta6le .so/.wpo land
@@ -117,7 +141,7 @@
 (define obj-dir   (format "~a/.build/~a-obj" jgl-repo output))
 (define entry-so  (format "~a/program.so"  obj-dir))
 (define entry-wpo (format "~a/program.wpo" obj-dir))
-(system (format "rm -rf '~a' && mkdir -p '~a'" obj-dir obj-dir))
+(reset-build-dir! obj-dir)
 
 ;; ── Stage 1: load xpatch (target=ta6le emit mode) ──────────────────────────
 (define orig-libdirs (library-directories))
@@ -130,13 +154,13 @@
 
 ;; ── Stage 2: compile-program ────────────────────────────────────────────────
 (printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so)
-(guard (e (#t (system (format "rm -rf '~a'" obj-dir)) (raise e)))
+(guard (e (#t (remove-build-dir! obj-dir) (raise e)))
   (compile-program entry-script entry-so))
 
 ;; ── Stage 3: whole-program optimize → single .so ────────────────────────────
 (define wpo-output (string-append output ".wp.so"))
 (printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output)
-(guard (e (#t (system (format "rm -rf '~a'" obj-dir)) (raise e)))
+(guard (e (#t (remove-build-dir! obj-dir) (raise e)))
   (compile-whole-program entry-wpo wpo-output #t))
 
 ;; ── Stage 4: embed boots + program as C arrays ──────────────────────────────
@@ -222,18 +246,18 @@
 ;; ── Stage 6: compile + link with cross-cc ──────────────────────────────────
 (printf "==> [6/6] compile + link with ~a~n" cross-cc)
 (define link-cmd
-  (format
-    (string-append
-      "~a -static -O2 -I~a ~a "
-      "-Wl,--start-group "
-      "~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a "
-      "-lpthread -lm -ldl "
-      "-Wl,--end-group -o ~a")
-    cross-cc cross-csv-dir main-c-path
-    native-a cross-csv-dir cross-csv-dir cross-csv-dir
-    output))
+  (string-append
+    (shell-quote cross-cc)
+    " -static -O2 -I " (shell-quote cross-csv-dir)
+    " " (shell-quote main-c-path)
+    " -Wl,--start-group "
+    (shell-quote native-a) " "
+    (shell-quote (format "~a/libkernel.a" cross-csv-dir)) " "
+    (shell-quote (format "~a/libz.a" cross-csv-dir)) " "
+    (shell-quote (format "~a/liblz4.a" cross-csv-dir)) " "
+    "-lpthread -lm -ldl "
+    "-Wl,--end-group -o " (shell-quote output)))
 (printf "    ~a~n" link-cmd)
-(unless (zero? (system link-cmd))
-  (error 'build-jgl-cross "link failed"))
-(system (format "rm -rf '~a'" obj-dir))   ;; drop isolated objects
+(run-safe-system! 'build-jgl-cross link-cmd)
+(remove-build-dir! obj-dir)   ;; drop isolated objects
 (printf "~n==> done: ~a~n" output)
diff --git a/docs/credential-handling.md b/docs/credential-handling.md
new file mode 100644
index 0000000..7f7c7b0
--- /dev/null
+++ b/docs/credential-handling.md
@@ -0,0 +1,65 @@
+# Credential Handling
+
+`jgl` is a GitLab API client. Treat every token, project path, issue body,
+comment, attachment, and exported archive as sensitive unless the project is
+known public.
+
+## Token Sources
+
+`jgl` resolves credentials in this order:
+
+1. `--token TOKEN`
+2. `GITLAB_TOKEN`
+3. `token:` in `~/.gitlab.yaml` or the path supplied by `--config` or
+   `GITLAB_CONFIG`
+
+Use a GitLab token with the narrowest scope that works. For private read-only
+usage, `read_api` is the expected scope. Do not use admin, write, registry, or
+sudo-capable tokens for routine issue and CI queries.
+
+## Local Storage
+
+`jgl` does not persist tokens by itself. If a config file stores a token, the
+operator is responsible for filesystem permissions. Recommended mode:
+
+```sh
+chmod 0600 ~/.gitlab.yaml
+```
+
+Avoid passing tokens on a shared shell command line because process listings and
+shell history can expose argv. Prefer `GITLAB_TOKEN` from a secret manager or a
+mode-0600 config file.
+
+## Error Output
+
+GitLab API error responses can echo request data or private project content.
+`jgl` redacts response bodies in raised error messages and keeps only the
+response byte count. Failed API paths are reported without query strings.
+
+## Exports And Attachments
+
+`jgl export` writes issue markdown under `--output` and may download referenced
+`/uploads/` attachments. Export output can contain issue descriptions, comments,
+usernames, labels, private project URLs, and binary attachments. Store exports
+outside public repos unless intentionally publishing sanitized project data.
+Issue directory names and downloaded attachment filenames derived from API
+responses are sanitized before local filesystem writes.
+
+## Release Requirements
+
+Before a production release:
+
+- Run `make release-evidence` from a clean checkout.
+- Run release evidence with `JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1` and a
+  marker-complete `JERBOA_GITLAB_TARGET_PROOF_FILE` from a least-privilege live
+  GitLab token.
+- The target proof must confirm public and private project smokes,
+  least-privilege token scope, token source hygiene, API error redaction,
+  export/attachment redaction, credential redaction, target-platform smoke,
+  production credential review, no raw API output retention, and the
+  sensitive-artifact policy.
+- Run the baseline secret scan and a history scan for GitLab token patterns.
+- Confirm release artifacts do not include local exports, generated stale C
+  files, tokens, private project paths, or private GitLab hostnames.
+- Review `jerboa-native` dependency versions and produce SBOM/reproducibility
+  notes.
diff --git a/docs/dependency-provenance.md b/docs/dependency-provenance.md
new file mode 100644
index 0000000..3d6e23e
--- /dev/null
+++ b/docs/dependency-provenance.md
@@ -0,0 +1,17 @@
+# Dependency Provenance
+
+`jgl` uses Jerboa's `(std net request)` module for HTTPS requests to the GitLab
+REST API. TLS is provided by the bundled `jerboa-native-rs` crate built with
+`tls,crypto` features.
+
+The release-evidence SBOM records:
+
+- selected Jerboa toolchain and cached Jerboa home;
+- `jerboa-native-rs` `Cargo.toml` and `Cargo.lock`;
+- `cargo tree` for the `tls,crypto` native feature set;
+- `cargo audit` output when the tool is installed;
+- binary linkage/load-command data and source/workflow hashes.
+
+No OpenSSL CLI subprocess is used for GitLab API requests. Release review
+should still check the native Rust dependency lockfile, hosted CI output, and
+GitLab token handling before public production support.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..59df337
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,64 @@
+# Release Evidence
+
+`make release-evidence` writes ignored local evidence under
+`dist/release-evidence/`.
+
+The evidence bundle contains:
+
+- high-confidence tracked-file secret-scan output;
+- unit-test output;
+- standalone binary build and `--help` smoke output;
+- SBOM-style source, workflow, binary, toolchain, and native dependency
+  manifests;
+- Jerboa native Cargo metadata, dependency tree, and `cargo audit` status when
+  `cargo audit` is installed;
+- target GitLab proof status and proof-gate self-test output;
+- repeated binary and generated FFI-symbol reproducibility checks.
+
+GitLab API access uses `(std net request)` over Jerboa's bundled
+`jerboa-native-rs` TLS provider with `tls,crypto` features. The client sends
+the token in the `PRIVATE-TOKEN` header and redacts non-2xx API response bodies
+from raised errors.
+
+This evidence does not replace GitLab-specific release blockers: hosted CI on
+the release platform, live least-privilege token smoke tests against public and
+private projects, export/attachment redaction review, and external review of
+credentialed client behavior are still required before production claims.
+
+## Target GitLab Proof
+
+Local `make release-evidence` always writes
+`dist/release-evidence/target-evidence/status.txt`. Without a proof file, the
+target-only GitLab checks are recorded as `blocked-not-run` so local evidence is
+not mistaken for live token evidence.
+
+To attach production target evidence, create a sanitized marker-only proof file
+and run:
+
+```sh
+JERBOA_GITLAB_TARGET_PROOF_FILE=/path/to/gitlab-target-proof.txt \
+JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1 \
+make release-evidence
+```
+
+The proof file must contain every line below exactly:
+
+```text
+gitlab_public_project_smoke_status=target-evidence-recorded
+gitlab_private_project_smoke_status=target-evidence-recorded
+least_privilege_token_scope_status=confirmed
+token_source_hygiene_status=confirmed
+api_error_redaction_status=target-evidence-recorded
+export_attachment_redaction_status=target-evidence-recorded
+credential_redaction_status=target-evidence-recorded
+target_platform_smoke_status=target-evidence-recorded
+production_credential_review_status=current-run-recorded
+raw_api_output_status=not-preserved
+sensitive_artifact_policy=no-gitlab-tokens-private-project-paths-issue-exports-attachments-or-raw-api-responses-preserved
+```
+
+The proof gate rejects missing, empty, marker-incomplete, or high-confidence
+sensitive proof files. The proof must not preserve GitLab tokens,
+`PRIVATE-TOKEN` headers, bearer headers, private project paths, exported issue
+archives, attachments, private hostnames, raw API responses, or downloaded
+content.
diff --git a/jgl.ss b/jgl.ss
index e18d51c..a75fa6d 100644
--- a/jgl.ss
+++ b/jgl.ss
@@ -6,7 +6,7 @@
 ;;; argv through <PREFIX>_ARGC / <PREFIX>_ARGn environment variables. We try the
 ;;; expected prefixes and fall back to (command-line) for interpreted mode.
 
-(import (chezscheme)
+(import (jerboa core)
         (jgl cli))
 
 (define (env-args)
diff --git a/lib/jgl/client.ss b/lib/jgl/client.ss
index daadb87..d8be20b 100644
--- a/lib/jgl/client.ss
+++ b/lib/jgl/client.ss
@@ -8,7 +8,7 @@
 (library (jgl client)
   (export make-client client? client-base-url client-token
           gl-get gl-get-all gl-get-text gl-try-get gl-download
-          pct-encode build-query api-path)
+          pct-encode build-query api-path gitlab-error-response-summary)
   (import (except (chezscheme)
                   make-hash-table hash-table?
                   sort sort!
@@ -74,16 +74,31 @@
     (list (cons "PRIVATE-TOKEN" (client-token c))
           (cons "Accept" "application/json")))
 
-  (def (truncate-str s n)
-    (if (> (string-length s) n) (string-append (substring s 0 n) "...") s))
+  (def (string-index-char s ch)
+    (let loop ([i 0])
+      (cond
+        [(>= i (string-length s)) #f]
+        [(char=? (string-ref s i) ch) i]
+        [else (loop (+ i 1))])))
+
+  (def (path-without-query path)
+    (let ([idx (string-index-char path #\?)])
+      (if idx (substring path 0 idx) path)))
+
+  (def (gitlab-error-response-summary body)
+    ;; GitLab errors may echo request data or private issue/project content.
+    ;; Preserve only the byte count so CLI/log callers do not persist secrets.
+    (if (and (string? body) (> (string-length body) 0))
+      (string-append " -- response redacted ("
+                     (number->string (string-length body))
+                     " bytes)")
+      ""))
 
   (def (gl-error status body path)
     (error 'gitlab
       (string-append "GitLab API returned " (number->string status)
-                     " for /" path
-                     (if (> (string-length body) 0)
-                       (string-append " -- " (truncate-str body 400))
-                       ""))))
+                     " for /" (path-without-query path)
+                     (gitlab-error-response-summary body))))
 
   ;; Raw authenticated GET. Returns (values status body-text).
   (def (gl-request c path)
diff --git a/lib/jgl/issues.ss b/lib/jgl/issues.ss
index 9ed87ee..fe1d1d2 100644
--- a/lib/jgl/issues.ss
+++ b/lib/jgl/issues.ss
@@ -7,7 +7,8 @@
 
 (library (jgl issues)
   (export list-issues get-issue get-notes
-          issue->markdown export-issues)
+          issue->markdown export-issues
+          safe-export-file-name safe-issue-dir-name)
   (import (except (chezscheme)
                   make-hash-table hash-table?
                   sort sort!
@@ -112,6 +113,31 @@
     (let ([idx (last-index path #\/)])
       (if idx (substring path (+ idx 1) (string-length path)) path)))
 
+  (def (safe-name-char? ch)
+    (or (char-alphabetic? ch)
+        (char-numeric? ch)
+        (memv ch '(#\. #\_ #\-))))
+
+  (def (safe-export-file-name raw fallback)
+    (let* ([source (if (and (string? raw) (not (string=? raw "")))
+                     raw
+                     fallback)]
+           [out (open-output-string)])
+      (for-each
+        (lambda (ch)
+          (write-char (if (safe-name-char? ch) ch #\_) out))
+        (string->list source))
+      (let ([name (get-output-string out)])
+        (cond
+          [(or (string=? name "") (string=? name ".") (string=? name ".."))
+           fallback]
+          [(char=? (string-ref name 0) #\.)
+           (string-append "_" name)]
+          [else name]))))
+
+  (def (safe-issue-dir-name iid)
+    (string-append "issue-" (safe-export-file-name iid "unknown")))
+
   ;; Collect distinct "/uploads/..." paths referenced in `text`.
   (def (extract-upload-paths text)
     (let ([n (string-length text)] [needle "/uploads/"])
@@ -158,16 +184,20 @@
         content
         (let ([attach-dir (string-append issue-dir "/attachments")])
           (ensure-dir attach-dir)
-          (let loop ([us uploads] [text content])
+          (let loop ([us uploads] [text content] [index 1])
             (if (null? us)
               text
               (let* ([up (car us)]
-                     [fname (basename up)]
+                     [fname (string-append
+                              (number->string index)
+                              "-"
+                              (safe-export-file-name (basename up) "attachment"))]
                      [url (string-append project-web up)]
                      [dest (string-append attach-dir "/" fname)]
                      [ok (guard (e (#t #f)) (gl-download c url dest))])
                 (loop (cdr us)
-                      (if ok (replace-all text up (string-append "attachments/" fname)) text)))))))))
+                      (if ok (replace-all text up (string-append "attachments/" fname)) text)
+                      (+ index 1)))))))))
 
   ;; Export every issue to <outdir>/issue-<iid>/README.md. Returns the count.
   (def (export-issues c project outdir)
@@ -181,7 +211,7 @@
         (when (pair? is)