release: migrate distribution to Forgejo and improve safety
ober
cff4ec15a766f6cd26a472f5449930e471174db2
--- a/AGENTS.md +++ b/AGENTS.md @@ -370,18 +370,16 @@ improved versions of the above. When working in a Jerboa project, **ONLY modify files in the current repo** unless the user explicitly names another path. -### Release Distribution: SourceHut Only - -**Never use GitHub Releases, GitHub release URLs, or GitHub as a fallback for -Jerboa release artifacts.** Jerboa's sole official release channel is attached -files on a versioned SourceHut git tag. Publish them with -`hut git -r ~lisp/jerboa artifact upload <file> --rev <tag>` (normally through -`make release-upload`) and download them from -`https://git.sr.ht/~lisp/jerboa/refs/download/<tag>/<artifact>`. - -Do not add a GitHub mirror or fallback when SourceHut delivery needs work; -repair or configure the SourceHut release path instead. See the SourceHut -manual: <https://man.sr.ht/git.sr.ht/#attaching-files-to-releases>. +### Release Distribution: git.jerboa.sh Only + +**Never use SourceHut or GitHub Releases, release URLs, mirrors, or fallbacks +for Jerboa artifacts.** The canonical human-facing repository URL is +`https://git.jerboa.sh/ober/jerboa`. Publish release files on the +matching versioned release at `git.jerboa.sh`; consumers download them from +`https://git.jerboa.sh/ober/jerboa/releases/download/<tag>/<artifact>`. + +If delivery needs work, repair or configure the `git.jerboa.sh` release path. +Do not reintroduce SourceHut or add a GitHub mirror. Common sibling repos that exist but must NOT be touched without explicit instruction: - `~/mine/jerboa-mcp` — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told. --- a/Makefile +++ b/Makefile @@ -336,7 +336,8 @@ help: @echo " (cross targets need: make chez-cross CHEZ_TARGET_MACHINE=ta6le|tarm64le|ta6fb ...)" @echo " release-artifact Package one built target: RELEASE_VERSION=v$(PROJECT_VERSION) RELEASE_TARGET=linux-amd64" @echo " release-artifacts Build/package all release targets" - @echo " release-upload Upload dist/release artifacts to a SourceHut git tag via hut" + @echo " release-upload Upload signed dist/release artifacts to Forgejo (requires FORGEJO_TOKEN)" + @echo " jpkg-workspace Verify/pack jerboa*/jpkg.sexp repos under JPKG_WORKSPACE_ROOT" @echo " native-cross Cross-build Rust native lib for a target" @echo " Args: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cc> [CROSS_NATIVE_FEATURES=tls,...]" @echo " typecheck Typecheck Typed Jerboa sources" @@ -853,25 +854,25 @@ check-cross-tools: jerboa-portable: check-cross-tools jerboa-macos-arm64 jerboa-linux-amd64 jerboa-linux-arm64 jerboa-freebsd-amd64 -# ── Release packaging for SourceHut tag artifacts ─────────────────────────── -# Native SourceHut jobs call `release-artifact` after `make jerboa`. Local -# maintainers can call `release-artifacts` to cross-build and package all -# supported bootstrap toolchains in one pass. +# ── Release packaging for Forgejo releases ───────────────────────────────── +# Run `release-artifact` natively on each release host, consolidate the output, +# sign it once, then upload the complete set to the matching Forgejo release. RELEASE_VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || printf 'v%s\n' "$(PROJECT_VERSION)") RELEASE_TARGET ?= $(shell host="$(HOST_UNAME_S)-$(HOST_UNAME_M)"; if [ "$(HOST_UNAME_O)" = Android ] && { [ "$(HOST_UNAME_M)" = aarch64 ] || [ "$(HOST_UNAME_M)" = arm64 ]; }; then echo android-arm64; elif [ "$$host" = Darwin-arm64 ]; then echo macos-arm64; elif [ "$$host" = Linux-x86_64 ]; then echo linux-amd64; elif [ "$$host" = Linux-aarch64 ] || [ "$$host" = Linux-arm64 ]; then echo linux-arm64; elif [ "$$host" = FreeBSD-amd64 ] || [ "$$host" = FreeBSD-x86_64 ]; then echo freebsd-amd64; else echo unknown; fi) -RELEASE_TARGETS ?= macos-arm64 linux-amd64 linux-arm64 freebsd-amd64 +RELEASE_TARGETS ?= macos-arm64 linux-amd64 freebsd-amd64 RELEASE_DIR ?= dist/release -RELEASE_REPO ?= ~lisp/jerboa +FORGEJO_OWNER ?= ober +FORGEJO_REPO ?= jerboa EVIDENCE_DIR ?= dist/release-evidence SIGNING_EVIDENCE_DIR ?= dist/signing-evidence SBOM_DIR ?= dist/sbom REPRO_DIR ?= dist/reproducibility -.PHONY: release-artifact release-artifacts sign-release-artifacts signing-evidence release-upload sbom reproducibility-report release-evidence +.PHONY: release-artifact release-artifacts sign-release-artifacts signing-evidence release-upload jpkg-workspace sbom reproducibility-report release-evidence release-artifact: @support/package-jerboa-release.sh "$(RELEASE_VERSION)" "$(RELEASE_TARGET)" "$(RELEASE_DIR)" -release-artifacts: jerboa-portable +release-artifacts: jerboa-macos-arm64 jerboa-linux-amd64 jerboa-freebsd-amd64 @for target in $(RELEASE_TARGETS); do \ support/package-jerboa-release.sh "$(RELEASE_VERSION)" "$$target" "$(RELEASE_DIR)"; \ done @@ -887,7 +888,7 @@ release-upload: JERBOA_REQUIRE_RELEASE_SIGNATURES=1 \ JERBOA_REQUIRE_RELEASE_SIGNATURE_VERIFICATION=1 \ support/sign-release-artifacts.sh check - @support/sourcehut-release-upload.sh "$(RELEASE_VERSION)" "$(RELEASE_REPO)" \ + @support/forgejo-release-upload.sh "$(RELEASE_VERSION)" "$(FORGEJO_OWNER)" "$(FORGEJO_REPO)" \ "$(RELEASE_DIR)"/jerboa-"$(RELEASE_VERSION)"-*.tar.gz \ "$(RELEASE_DIR)"/jerboa-"$(RELEASE_VERSION)"-*.tar.gz.sha256 \ "$(RELEASE_DIR)"/jerboa-"$(RELEASE_VERSION)"-*.tar.gz.asc \ @@ -905,6 +906,13 @@ release-upload: "$(RELEASE_DIR)"/install.sh.sig \ "$(RELEASE_DIR)"/install.sh.minisig +JPKG_WORKSPACE_ROOT ?= +JPKG_WORKSPACE_OUT ?= $(CURDIR)/dist/jpkg-workspace + +jpkg-workspace: + @test -n "$(JPKG_WORKSPACE_ROOT)" || { echo "ERROR: set JPKG_WORKSPACE_ROOT to the directory containing jerboa* repositories" >&2; exit 1; } + @support/pack-jpkg-workspace.sh "$(JPKG_WORKSPACE_ROOT)" "$(JPKG_WORKSPACE_OUT)" + sbom: JERBOA_SBOM_DIR="$(SBOM_DIR)" SCHEME="$(SCHEME)" support/sbom.sh new file mode 100644 --- /dev/null +++ b/branches.md @@ -0,0 +1,27 @@ +# Branch review + +Reviewed 2026-07-30 against `master` (`origin/master`). “Ahead” and “behind” are +the counts from `git rev-list --left-right --count master...<branch>`. + +## Unmerged branches + +| Branch | Remote/local refs | Ahead | Behind | Latest commit | +|---|---|---:|---:|---| +| `typed` | local, `origin/typed` | 3 | 0 | `0ec12c3c` remove DuckDB native integration | +| `cross` | local, `origin/cross` | 10 | 618 | `c9251c22` add docs | +| `secure` | local, `origin/secure` | 8 | 864 | `1668942e` Update secure.md: all 6 implementation items complete | +| `security/p0-audit-fixes` | local, `origin/security/p0-audit-fixes` | 3 | 163 | `e374ab65` data: add P0 prevention artifacts | +| `origin/gc-improvement` | remote only | 32 | 230 | `4b2954f3` Unify AGENTS.md with canonical jerboa version | +| `wip/native-hmac-ed25519-verify` | local only | 1 | 705 | `0bc3ddcf` WIP: HMAC-SHA512 + ed25519 verify FFI exports | +| `github/main` | remote default branch | 1 | 1091 | `03ece279` jerboa binary release mirror | + +The current branch is `typed`; no unmerged branches were deleted. + +## Deleted merged branches + +- Local: `browser-repl-plan`, `improve-gc`, `jerboa-on-jerboa`, `rust-or-bust`, + `slang2`, `worktree-agent-a2fd2155`. +- `origin`: `browser-repl-plan`, `jerboa-on-jerboa`, `rust-or-bust`, `slang2`. +- `github`: `master` (merged non-default branch; `github/main` was retained). + +The primary branches `master`, `origin/master`, and `github/main` were retained. --- a/data/anti-patterns.sexp +++ b/data/anti-patterns.sexp @@ -5945,4 +5945,23 @@ ("title" . "Jerboa prelude traps: keep, sleep, and make-mutex mismatch") - ("tools" "jerboa_eval" "jerboa_module_exports"))) + ("tools" "jerboa_eval" "jerboa_module_exports")) + (("advice" + . + "When a closer-run refactor touches a multi-argument core form (dynamic-wind, guard, call-with-values), verify the ARG COUNT by reading the form after jerboa_check_balance passes. Treat any build warning of the form \"possible incorrect argument count in call (<core-form> ...)\" as build-breaking: find it, fix it, rebuild until silent. After every server.ss change, boot the binary once and confirm it answers a query — jerboa-ss-boot-smoke.") + ("avoid" + . + "Assuming jerboa_check_balance (or a successful compile) means the form structure is right, and ignoring \"possible incorrect argument count in call (dynamic-wind ...)\" compile warnings. A paren moved one slot inside a closer run keeps the file balanced but nests an intended argument (e.g. the dynamic-wind OUT thunk) inside another lambda body, where it is silently discarded — the call then fails only at runtime, in a path unit tests never exercise.") + ("id" . "balanced-but-wrong-arity-closer-run") + ("kinds" "edit" "refactor" "review") + ("pattern" . "possible incorrect argument count in call") + ("severity" . "high") + ("tags" "dynamic-wind" "paren-balance" "compiler-warning" + "closer-run" "startup-crash") + ("title" + . + "Balanced file, wrong arity: paren drift inside closer runs survives check_balance") + ("tools" + "jerboa_check_balance" + "jerboa_verify" + "jerboa_compile_check"))) --- a/data/error-fixes.sexp +++ b/data/error-fixes.sexp @@ -3362,4 +3362,16 @@ "Use the quoted empty list `'()` when assigning or passing an empty list value; reserve square brackets for grouped forms with content.") ("id" . "empty-square-brackets-expression-invalid") ("pattern" . "invalid syntax \\(\\).*\\(set! .* \\[\\]\\)") - ("type" . "Syntax Error"))) + ("type" . "Syntax Error")) + (("code_example" + . + ";; BAD (raises whenever prelude is imported):\n;; (sleep (make-time 'time-duration 0 1))\n;; GOOD:\n(sleep-ms 1000) ;; prelude export, uses %chez-make-time internally") + ("explanation" + . + "Files importing (jerboa prelude) get its shadowed make-time. (sleep (make-time 'time-duration ns sec)) then raises \"not a time record of type time-duration\" — deterministically, in both interpreter and embedded builds. This silently killed jdns background threads (CDB reloader, TCP accept loop) because each called a locally-defined sleep-ms built on the shadowed make-time.") + ("fix" + . + "Do not pass (make-time ...) to Chez `sleep` when (jerboa prelude) is imported: the prelude shadows make-time with the (std datetime) constructor, which produces a dt-raw record, not a Chez time record. Use the prelude's own (sleep-ms ms) instead (it internally uses a private %chez-make-time). If you must build a duration by hand, re-import Chez's make-time under a rename: (rename (only (chezscheme) make-time) (make-time %chez-make-time)).") + ("id" . "sleep-prelude-make-time-type-mismatch") + ("pattern" . "is not a time record of type time-duration") + ("type" . "runtime"))) --- a/data/features.sexp +++ b/data/features.sexp @@ -4008,4 +4008,24 @@ ("use_case" . "Saving discoveries and checking duplicates should not be blocked by one malformed or compound anti-pattern field.") + ("votes" . 0)) + (("description" + . + "anti_pattern_lookup crashed with `Exception in string-downcase: (\"FileNotFoundException.*\\\\.csv|AssetManager.*open|box_types\") is not a string` while searching for sibling-checkout build-file anti-patterns. The lookup path should validate or stringify pattern fields so a malformed entry cannot prevent discovery or duplicate checks.") ("estimated_token_reduction" . "") + ("example_scenario" + . + "Call anti_pattern_lookup with query `build files fallback to ../jerboa sibling checkout or ~/mine/jerboa absolute path`; lookup should skip or normalize malformed entries and return matches instead of throwing.") + ("id" . "anti-pattern-lookup-non-string-pattern-guard") + ("impact" + . + "Restores reliable duplicate checking before adding anti-patterns and prevents agents from bypassing the knowledge-management workflow.") + ("status" . "open") + ("tags" "anti-patterns" "lookup" "knowledge-base" "tooling" + "robustness") + ("title" + . + "Guard anti_pattern_lookup against non-string pattern fields") + ("use_case" + . + "Before saving a reusable anti-pattern, agents must search existing entries. A single non-string pattern currently blocks that mandatory workflow.") ("votes" . 0))) --- a/docs/jerboa-for-clojure-devs.md +++ b/docs/jerboa-for-clojure-devs.md @@ -5,7 +5,7 @@ Jerboa is a Chez Scheme dialect that provides a Clojure-compatible surface for d ## Installation ```bash -git clone https://github.com/jafourni/jerboa.git +git clone https://git.jerboa.sh/ober/jerboa cd jerboa make build ``` --- a/docs/jpkg-migration.md +++ b/docs/jpkg-migration.md @@ -22,7 +22,7 @@ For a leaf package (no jerboa-* dependencies): ```sh cd ~/mine/jerboa-gitsafe -jerboa pkg init @lisp/gitsafe # creates jpkg.sexp +jerboa pkg init @ober/gitsafe # creates jpkg.sexp # edit jpkg.sexp: set version, license, (modules ((root "gitsafe"))), jerboa req jerboa pkg verify # manifest is valid jerboa pkg pack # → lisp-gitsafe-X.Y.Z.jpkg (reproducible) @@ -37,16 +37,16 @@ declare the dependencies in the dependent's `jpkg.sexp` and `jpkg add` them. ## 1. Decide scope and identity -All these repos live in the sourcehut `~lisp` namespace, so use the scope -**`@lisp`**. The package name should match the repo's purpose, not +All these repos live in the `git.jerboa.sh` `ober` namespace, so use the scope +**`@ober`**. The package name should match the repo's purpose, not necessarily the repo name: | Repo | Suggested package | |---|---| -| `jerboa-gitsafe` | `@lisp/gitsafe` | -| `jerboa-websearch` | `@lisp/websearch` (or `@lisp/jerbsearch`) | -| `jerboa-secmonlib` | `@lisp/secmon` | -| `jerboa-crypto` | `@lisp/crypto` | +| `jerboa-gitsafe` | `@ober/gitsafe` | +| `jerboa-websearch` | `@ober/websearch` (or `@ober/jerbsearch`) | +| `jerboa-secmonlib` | `@ober/secmon` | +| `jerboa-crypto` | `@ober/crypto` | Scopes are how delegation and publisher trust are organized later (§6), so keep them consistent. @@ -55,17 +55,17 @@ keep them consistent. ## 2. Add a manifest (`jpkg.sexp`) -Run `jerboa pkg init @lisp/<name>`, then edit. The key field is `modules`' +Run `jerboa pkg init @ober/<name>`, then edit. The key field is `modules`' `root` — point it at the directory that actually holds the package's `.ss` sources (this is what `pack` ships and what consumers import): ```scheme (package - (name "@lisp/gitsafe") + (name "@ober/gitsafe") (version "0.3.0") ; pick a real semver; bump on release (description "Git secret scanner") (license "Apache-2.0") - (source "https://git.sr.ht/~lisp/jerboa-gitsafe") + (source "https://git.jerboa.sh/ober/jerboa-gitsafe") (jerboa ">=0.1.0") (modules ((root "gitsafe") ; gitsafe/*.ss (src/ for most repos) (exports ((gitsafe main) (gitsafe scanner))))) @@ -122,27 +122,27 @@ resolved from a registry. For local and team use, a self-hosted registry is one directory plus one key. ```sh -mkdir -p ~/srv/jpkg/lisp -# one signing key for the @lisp scope (keep it safe; mode 0600 is set for you) -jerboa pkg publish --registry ~/srv/jpkg/lisp --key ~/.keys/lisp.key --keygen +mkdir -p ~/srv/jpkg/ober +# one signing key for the @ober scope (keep it safe; mode 0600 is set for you) +jerboa pkg publish --registry ~/srv/jpkg/ober --key ~/.keys/ober.key --keygen -# point your shell at it (or: jerboa pkg dir add lisp ~/srv/jpkg/lisp) -export JERBOA_PKG_REGISTRIES="lisp=$HOME/srv/jpkg/lisp" +# point your shell at it (or: jerboa pkg dir add ober ~/srv/jpkg/ober) +export JERBOA_PKG_REGISTRIES="ober=$HOME/srv/jpkg/ober" ``` The first publish bootstraps a TUF registry whose root/targets/snapshot/ -timestamp authority and `@lisp` publisher key are all this key — fine for +timestamp authority and `@ober` publisher key are all this key — fine for self-hosting. (Split the roles later via key rotation + delegation, §6.) Publish each leaf package: ```sh cd ~/mine/jerboa-secmonlib -jerboa pkg publish --registry ~/srv/jpkg/lisp --key ~/.keys/lisp.key \ - --source "https://git.sr.ht/~lisp/jerboa-secmonlib" +jerboa pkg publish --registry ~/srv/jpkg/ober --key ~/.keys/ober.key \ + --source "https://git.jerboa.sh/ober/jerboa-secmonlib" ``` -Now `@lisp/secmon` is a verifiable, immutable release with a signature, +Now `@ober/secmon` is a verifiable, immutable release with a signature, provenance, and a transparency-log entry. --- @@ -150,12 +150,12 @@ provenance, and a transparency-log entry. ## 5. Convert cross-repo dependencies Suppose `jerboa-secmon` uses code currently checked out from -`jerboa-secmonlib`. After publishing `@lisp/secmon` (the lib): +`jerboa-secmonlib`. After publishing `@ober/secmon` (the lib): ```sh cd ~/mine/jerboa-secmon -jerboa pkg init @lisp/secmon-app # if not already -jerboa pkg add @lisp/secmon # resolves, writes jpkg.lock, installs +jerboa pkg init @ober/secmon-app # if not already +jerboa pkg add @ober/secmon # resolves, writes jpkg.lock, installs git add jpkg.sexp jpkg.lock ``` @@ -175,9 +175,9 @@ every change — link the working checkout: ```sh cd ~/mine/jerboa-secmon -jerboa pkg link @lisp/secmon ~/mine/jerboa-secmonlib +jerboa pkg link @ober/secmon ~/mine/jerboa-secmonlib # …edit the lib freely; the app sees changes immediately… -jerboa pkg unlink @lisp/secmon # back to the registry version +jerboa pkg unlink @ober/secmon # back to the registry version ``` Linked deps are recorded in `jpkg.lock` as **non-reproducible**; @@ -192,7 +192,7 @@ release. Once more than one person publishes, split trust: - **Delegate the scope** to maintainer keys with a threshold, so publishing - to `@lisp/*` doesn't require the registry root key. + to `@ober/*` doesn't require the registry root key. - **Rotate** the root to offline-held keys. - **Transfer** a package's namespace to a new maintainer set when ownership changes. @@ -216,14 +216,14 @@ jerboa pkg audit # advisories, yanks, transparency For each `jerboa-*` repo: -- [ ] `jerboa pkg init @lisp/<name>`; set version, license, `source`. +- [ ] `jerboa pkg init @ober/<name>`; set version, license, `source`. - [ ] Set `(modules ((root "<srcdir>") (exports …)))` to the real source dir. - [ ] Declare `capabilities` matching `.jerbuild` (native/ffi/network/exec). - [ ] `jerboa pkg verify` passes. - [ ] `jerboa pkg pack` produces a `.jpkg`; `jerboa pkg verify FILE.jpkg` passes. - [ ] Commit `jpkg.sexp` (and `jpkg.lock` once it has deps). - [ ] Publish to the registry; confirm with `jerboa pkg search <name>`. -- [ ] Replace sibling/vendored deps with `jerboa pkg add @lisp/<dep>`. +- [ ] Replace sibling/vendored deps with `jerboa pkg add @ober/<dep>`. - [ ] Wire dep paths into the build via `jerboa pkg env` / `.jerbuild` libdirs. - [ ] In CI, set `JPKG_REQUIRE_SIGNATURES` and run `jerboa pkg audit`. @@ -246,7 +246,7 @@ For each `jerboa-*` repo: 1. Migrate **leaf** libraries first (no jerboa-* deps): `jerboa-crypto`, `jerboa-secmonlib`, `jerboa-compat`, `jerboa-temp-dir`, etc. -2. Stand up the `~lisp` registry (§4) and publish those leaves. +2. Stand up the `@ober` registry (§4) and publish those leaves. 3. Migrate **mid-tier** packages that depend on the leaves; `add` the deps. 4. Migrate **apps** (binaries) last; they pin everything via `jpkg.lock`. 5. Turn on fail-closed policy + `audit` in each repo's CI. --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -941,7 +941,7 @@ Chez cannot heap-cap a thread; `run-safe-eval` rightly refuses helper enters Capsicum before reading stdin or evaluating the supplied expression for supported `fs`/`exec`/`net`/`capsicum` axes. Per-platform CI parity now has a - macOS Seatbelt worker smoke workflow and a SourceHut FreeBSD Capsicum worker + macOS Seatbelt worker smoke workflow and a FreeBSD Capsicum worker smoke manifest. Arbitrary-command/proxy-aware Capsicum parity remains open. ### K3-P1-09 — Confused-deputy defenses: capability plans enforced at runtime --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -5,7 +5,7 @@ Get from zero to a working Jerboa program in 5 minutes. ## 1. Get Jerboa ```bash -git clone https://git.sr.ht/~lisp/jerboa +git clone https://git.jerboa.sh/ober/jerboa cd jerboa ``` --- a/docs/release-artifacts.md +++ b/docs/release-artifacts.md @@ -5,19 +5,16 @@ build without cloning and compiling this repository first. ## Distribution Channel -SourceHut git-tag artifacts are the only official release channel. Never -publish or download Jerboa release artifacts through GitHub Releases, including -as a mirror or fallback. Attach each signed release file to its versioned -SourceHut tag with `hut git -r ~lisp/jerboa artifact upload <file> --rev <tag>`; -`make release-upload` performs this for the complete release set. Consumers -download assets at: +The Jerboa repository and its versioned releases on `git.jerboa.sh` are the +only official distribution channel. Never publish or download Jerboa release +artifacts through SourceHut or GitHub Releases, including as a mirror or +fallback. Attach each signed release file to its matching versioned release at +`https://git.jerboa.sh/ober/jerboa`. Consumers download assets at: ```text -https://git.sr.ht/~lisp/jerboa/refs/download/<tag>/<artifact> +https://git.jerboa.sh/ober/jerboa/releases/download/<tag>/<artifact> ``` -See [SourceHut's release-artifact manual](https://man.sr.ht/git.sr.ht/#attaching-files-to-releases). - Each release target produces: ```text @@ -60,7 +57,7 @@ Once you have that small local trust file, the Linux AMD64 install is: ```sh version=vX.Y.Z # exact release tag -base="https://git.sr.ht/~lisp/jerboa/refs/download/$version" +base="https://git.jerboa.sh/ober/jerboa/releases/download/$version" trust_file="$HOME/.config/jerboa/release.allowed_signers" identity='jerboa-release@<publisher-domain>' @@ -101,8 +98,111 @@ installer accepts `--allowed-signers` and `--signer-identity` so its required trust inputs are visible in the command rather than hidden in environment variables. +### FreeBSD amd64 + +FreeBSD's base system provides `fetch`, `sha256`, `tar`, and `ssh-keygen`, so no +package manager dependency is required for the release installer. Confirm that +the machine reports `FreeBSD` and `amd64`, then use the authenticated installer +flow above; it selects `freebsd-amd64` automatically: + +```sh +uname -s +uname -m + +version=vX.Y.Z # replace with an exact tag that has attached artifacts +base="https://git.jerboa.sh/ober/jerboa/releases/download/$version" +trust_file="$HOME/.config/jerboa/release.allowed_signers" +identity='jerboa-release@<publisher-domain>' + +mkdir -p "$(dirname "$trust_file")" +# Populate $trust_file with the independently verified release signer. +fetch -o install.sh "$base/install.sh" +fetch -o install.sh.sig "$base/install.sh.sig" +ssh-keygen -Y verify \ + -f "$trust_file" -I "$identity" -n file \ + -s install.sh.sig < install.sh +chmod 0755 install.sh +./install.sh --version "$version" \ + --allowed-signers "$trust_file" --signer-identity "$identity" + +export PATH="$HOME/.local/bin:$PATH" +jerboa version +jerbuild --version +``` + +Replace `vX.Y.Z` with the exact release tag you intend to install; the tag must +have an installer, signature, manifest, and FreeBSD archive attached. Make the +`PATH` change persistent in `~/.profile` if necessary. For a system-wide +installation, run the verified installer as an administrator with +`--prefix /usr/local`; do not run an unverified network response as root. + +### Install from an already verified artifact or binary + +The FreeBSD artifact is named +`jerboa-<tag>-freebsd-amd64.tar.gz`. After authenticating +`release-manifest.sha256` with the independently obtained release key and +checking the archive digest, install its multicall binary directly: + +```sh +version=vX.Y.Z # replace with an exact tag that has attached artifacts +archive="jerboa-${version}-freebsd-amd64.tar.gz" +root="jerboa-${version}-freebsd-amd64" + +tar -xzf "$archive" +mkdir -p "$HOME/.local/bin" +install -m 0755 "$root/bin/jerboa" "$HOME/.local/bin/jerboa" +for command in jerbuild jpkg jmcp jlsp; do + ln -sf jerboa "$HOME/.local/bin/$command" +done + +"$HOME/.local/bin/jerboa" version +``` + +If you receive only the extracted `jerboa` binary, copy it with the same +`install -m 0755` command and create the four symlinks. A bare binary has no +built-in provenance: authenticate its digest against the signed release +manifest before executing it. Keep the archive's `NOTICE`, `LICENSE-CHEZ`, and +`share/licenses/jerboa/` files when redistributing the binary. + ## Build Locally +Build each requested release target natively on the matching architecture and +operating system. From an exact `vX.Y.Z` tag, run: + +```sh +make jerboa +make release-artifact RELEASE_VERSION=vX.Y.Z +``` + +The detected targets are `linux-amd64`, `freebsd-amd64`, and `macos-arm64`. +Copy the three resulting archives and `.sha256` files into one trusted +`dist/release/` directory on the signing host. Sign and verify the complete set: + +```sh +JERBOA_RELEASE_SIGNING_TOOL=ssh-keygen \ +JERBOA_RELEASE_SSH_SIGNING_KEY=/path/to/release-key \ +make sign-release-artifacts + +JERBOA_RELEASE_SSH_ALLOWED_SIGNERS=/path/to/release.allowed_signers \ +JERBOA_RELEASE_SSH_SIGNER_IDENTITY='jerboa-release@example.org' \ +make signing-evidence +``` + +Create a Forgejo API token with repository write permission, keep it out of +shell history, and upload the verified release set: + +```sh +export FORGEJO_TOKEN +make release-upload RELEASE_VERSION=vX.Y.Z +unset FORGEJO_TOKEN +``` + +`release-upload` creates a draft Forgejo release when necessary, uploads every +signed asset, and publishes it only after all uploads succeed. Existing assets +are never overwritten unless `FORGEJO_REPLACE_ASSETS=1` is explicitly set. +The exact tag must exist locally; protected remote-only builders can opt in with +`FORGEJO_ALLOW_UNTAGGED_RELEASE=1` after independently verifying the remote tag. + ```sh make release-artifacts ``` @@ -378,10 +478,11 @@ missing. Use `JERBOA_WASM_GC_RELEASE_ENGINES` only to split the same hard gate across hosts; do not use skipped engines as release evidence. Safari remains a supported split gate on hosts where remote automation has been enabled. -## SourceHut CI +## Release builders -The `.builds/release-*.yml` manifests build Linux and FreeBSD release targets -natively on SourceHut: +The checked-in `.builds/release-*.yml` files are legacy build manifests. They +describe the native Linux and FreeBSD release targets but are not an official +artifact host: ```text .builds/release-linux-amd64.yml @@ -390,28 +491,16 @@ natively on SourceHut: ``` The `macos-arm64` artifact is produced on an Apple Silicon macOS host with the -same `make release-artifact RELEASE_TARGET=macos-arm64` packaging path, then -uploaded to the release tag with `make release-upload`. - -They are constrained to `refs/tags/v*`, so ordinary branch pushes do not run -the release builders. The build job uses SourceHut's `GIT_REF` environment -variable to name the artifact, because the checkout itself is pinned by commit -hash. Every successful job exposes short-lived build artifacts for inspection. -Tag builds also run: - -```sh -make release-upload RELEASE_VERSION="$tag" -``` +same `make release-artifact RELEASE_TARGET=macos-arm64` packaging path. -`release-upload` now requires complete, locally verified signatures (including -the installer/manifest SSH signatures) and fails closed when verification -material is absent. The ordinary SourceHut builders intentionally cannot -promote their unsigned outputs; a protected signing/promotion environment must -provide the signed candidate and consumer trust policy. Promotion attaches the -versioned tarball, `.sha256` file, authenticated installer, detached signatures, -and `release-manifest.sha256` to the SourceHut git tag with -`hut git artifact upload --rev "$tag"`. SourceHut job artifacts are pruned; git -tag artifacts are the stable download channel. +Release builders must pin the checkout by commit and derive releases only from +`refs/tags/v*`; ordinary branch pushes are not release candidates. A protected +signing/promotion environment must provide the signed candidate and consumer +trust policy. Promotion attaches the versioned tarball, `.sha256` file, +authenticated installer, detached signatures, and +`release-manifest.sha256` to the matching release at +`https://git.jerboa.sh/ober/jerboa/releases`. Only those versioned release +attachments are the stable download channel. The `.sha256` files are integrity evidence, not publisher signatures. Release consumers should verify the detached signatures and compare uploaded checksums @@ -442,12 +531,13 @@ $(JERBOA_BINDIR)/jerboa: The bootstrap script downloads from: ```text -https://git.sr.ht/~lisp/jerboa/refs/download/<tag>/<artifact> +https://git.jerboa.sh/ober/jerboa/releases/download/<tag>/<artifact> ``` The allowed-signers file must be committed or provisioned by the consuming project, not downloaded beside the artifact. Override `JERBOA_RELEASE_REPO` or `JERBOA_RELEASE_ORIGIN` only for a fork. `JERBOA_RELEASE_BASE` is reserved for -local test fixtures; official artifacts are downloaded only from SourceHut. +local test fixtures; official artifacts are downloaded only from +`git.jerboa.sh`. Override `JERBOA_RELEASE_TARGET` only for tests or unusual hosts where `uname` detection is not enough. --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -20,7 +20,7 @@ Jerboa program — the project's CI verifies that. - A clone of Jerboa with the local runtime built and `jerboa` on your `PATH`: ```bash -git clone https://git.sr.ht/~lisp/jerboa +git clone https://git.jerboa.sh/ober/jerboa cd jerboa make chez build export PATH="$PWD/bin:$PATH" --- a/jpkg.sexp +++ b/jpkg.sexp @@ -1,5 +1,5 @@ (package - (name "@local/jerboa") + (name "@ober/jerboa") (version "0.2.0") (description "") (license "UNLICENSED") --- a/lib/jerboa/prelude/safe.ss +++ b/lib/jerboa/prelude/safe.ss @@ -100,6 +100,17 @@ ;; Safe source wrappers getenv + ;; ---- AI compatibility aliases (must match (jerboa prelude)) ---- + hash-has-key? hash-table-set! + directory-exists? + eql? + random-integer + read-line + force-output + string-map + processor-count + regex-match regex-search regex-replace regex-replace-all + ;; ---- Safe APIs under STANDARD names ---- ;; SQLite (literal-SQL macros over safe parameterized wrappers) @@ -208,6 +219,7 @@ (std misc list) (std misc alist) (except (std misc ports) read-file-string read-file-lines) + (std regex) ;; Safety modules (prefix (std safe) safe:) (prefix (std security taint) taint:) @@ -363,4 +375,30 @@ ;; Mode control (def *safe-mode* safe:*safe-mode*) + ;; ---- AI compatibility aliases ---- + (def hash-has-key? hash-key?) + (def hash-table-set! hash-put!) + (def directory-exists? file-directory?) + (def eql? eqv?) + (def random-integer random) + (def processor-count + ;; Safe fallback: no FFI. Use env var or default. + (let ([v (raw-getenv "JERBOA_PROCESSOR_COUNT")]) + (if v (string->number v) + (let ([v2 (raw-getenv "NCPUS")]) + (if v2 (string->number v2) 4))))) + (def (read-line . args) + (if (null? args) + (get-line (current-input-port)) + (get-line (car args)))) + (def (force-output . args) + (flush-output-port + (if (null? args) (current-output-port) (car args)))) + (def (string-map f s) + (list->string (map f (string->list s)))) + (def (regex-match pat str) (re-search pat str)) + (def (regex-search pat str) (re-search pat str)) + (def (regex-replace pat str rep) (re-replace pat str rep)) + (def (regex-replace-all pat str rep) (re-replace-all pat str rep)) + ) ;; end library --- a/lib/std/lsp/server.ss +++ b/lib/std/lsp/server.ss @@ -43,7 +43,7 @@ (def (read-message port) (let ([len (read-header port)]) (and len - (let ([buf (get-bytevector-n (standard-input-port) len)]) + (let ([buf (get-bytevector-n port len)]) (and (bytevector? buf) (string->json-object (utf8->string buf))))))) @@ -53,9 +53,8 @@ [len (bytevector-length bv)]) (display (string-append "Content-Length: " (number->string len) "\r\n\r\n") port) - (put-bytevector (standard-output-port) bv) - (flush-output-port port) - (flush-output-port (standard-output-port)))) + (put-bytevector port bv) + (flush-output-port port))) ;; ---- Response helpers ---- --- a/lib/std/net/httpsd.ss +++ b/lib/std/net/httpsd.ss @@ -307,7 +307,8 @@ (cond [(<= need 0) ""] [else - (let* ([body-start (+ header-end 4)] + ;; httparse returns the byte offset immediately after CRLFCRLF. + (let* ([body-start header-end] [available (max 0 (- filled body-start))] [pre (min available need)] [out (make-bytevector need)]) --- a/lib/std/pkg/search.ss +++ b/lib/std/pkg/search.ss @@ -180,7 +180,7 @@ (def (dir-list) ;; env override shadows the file (read-only) — report both honestly. ;; When neither env nor file configures anything, registry-config falls - ;; back to the built-in default @lisp registry; report that as "default". + ;; back to the built-in default @ober registry; report that as "default". (let ([env (getenv "JERBOA_PKG_REGISTRIES")] [no-default (getenv "JPKG_NO_DEFAULT_REGISTRY")]) (cond --- a/lsp/analysis/document.ss +++ b/lsp/analysis/document.ss @@ -25,10 +25,11 @@ ;; Convert file:// URI to filesystem path (define (uri->file-path uri) (cond + [(not (string? uri)) uri] [(string-prefix? "file://" uri) (let ([path (substring uri 7 (string-length uri))]) - ;; Handle percent-encoding for spaces, etc. - (uri-decode path))] + ;; Handle percent-encoding for spaces, etc. + (uri-decode path))] [else uri])) ;; Convert filesystem path to file:// URI --- a/lsp/handlers/lifecycle.ss +++ b/lsp/handlers/lifecycle.ss @@ -18,8 +18,9 @@ (let ([root-uri (json-get params "rootUri")] [root-path (json-get params "rootPath")]) ;; Set workspace root - (let ([root (or (and root-uri (uri->file-path root-uri)) root-path)]) - (when root + (let ([root (or (and (string? root-uri) (uri->file-path root-uri)) + (and (string? root-path) root-path))]) + (when (string? root) (server-state-workspace-root-set! state root) (lsp-info "Workspace root:" root))) --- a/lsp/util/position.ss +++ b/lsp/util/position.ss @@ -54,10 +54,13 @@ (if (>= line (length lines)) "" (let* ([ln (list-ref lines line)] - [end (min col (string-length ln))]) - (let ([start (find-word-start ln (max 0 (- end 1)))]) - (if (>= start end) "" - (substring ln start end))))))) + [len (string-length ln)]) + (if (or (= len 0) (= col 0)) + "" + (let* ([end (min col len)] + [start (find-word-start ln (max 0 (- end 1)))]) + (if (>= start end) "" + (substring ln start end)))))))) (define (find-word-start str pos) (let loop ([i pos]) new file mode 100755 --- /dev/null +++ b/support/forgejo-release-upload.sh @@ -0,0 +1,146 @@ +#!/bin/sh +# Create or update a Forgejo release and upload authenticated artifacts. + +set -eu +umask 077 + +usage() { + echo "Usage: $0 VERSION OWNER REPO FILE..." >&2 + echo " requires FORGEJO_TOKEN with repository write permission" >&2 +} + +[ "${1:-}" ] || { usage; exit 2; } +[ "${2:-}" ] || { usage; exit 2; } +[ "${3:-}" ] || { usage; exit 2; } + +version=$1 +owner=$2 +repo=$3 +shift 3 + +[ "$#" -gt 0 ] || { usage; exit 2; } +[ -n "${FORGEJO_TOKEN:-}" ] || { + echo "ERROR: FORGEJO_TOKEN is required" >&2 + exit 1 +} +printf '%s\n' "$version" | grep -Eq '^v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' || { + echo "ERROR: version must be an exact tag such as v0.2.8" >&2 + exit 1 +} +case "$owner/$repo" in + *[!A-Za-z0-9._/-]*) echo "ERROR: invalid Forgejo owner/repo" >&2; exit 1 ;; +esac + +api=${FORGEJO_API_URL:-https://git.jerboa.sh/api/v1} +publish=${FORGEJO_PUBLISH_RELEASE:-1} +replace=${FORGEJO_REPLACE_ASSETS:-0} + +command -v curl >/dev/null 2>&1 || { + echo "ERROR: curl is required" >&2 + exit 1 +} +command -v jq >/dev/null 2>&1 || { + echo "ERROR: jq is required" >&2 + exit 1 +} + +if git rev-parse --git-dir >/dev/null 2>&1 && + ! git rev-parse -q --verify "refs/tags/$version" >/dev/null; then + [ "${FORGEJO_ALLOW_UNTAGGED_RELEASE:-0}" = 1 ] || { + echo "ERROR: $version is not a local tag" >&2 + echo "Set FORGEJO_ALLOW_UNTAGGED_RELEASE=1 only when a protected builder verified the remote tag." >&2 + exit 1 + } +fi + +tmp=$(mktemp -d "${TMPDIR:-/tmp}/jerboa-forgejo-upload.XXXXXX") +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +request() { + method=$1 + url=$2 + output=$3 + shift 3 + curl --silent --show-error --location \ + --request "$method" \ + --header "Authorization: token $FORGEJO_TOKEN" \ + --output "$output" \ + --write-out '%{http_code}' \ + "$@" "$url" +} + +release_json="$tmp/release.json" +status=$(request GET "$api/repos/$owner/$repo/releases/tags/$version" "$release_json") +case "$status" in + 200) ;; + 404) + create_json="$tmp/create.json" + jq -n --arg tag "$version" --arg name "Jerboa $version" \ + '{tag_name:$tag,name:$name,draft:true,prerelease:false}' > "$create_json" + status=$(request POST "$api/repos/$owner/$repo/releases" "$release_json" \ + --header 'Content-Type: application/json' --data-binary "@$create_json") + [ "$status" = 201 ] || { + echo "ERROR: Forgejo release creation failed (HTTP $status)" >&2 + jq -r '.message // empty' "$release_json" >&2 || true + exit 1 + } + ;; + *) + echo "ERROR: Forgejo release lookup failed (HTTP $status)" >&2 + jq -r '.message // empty' "$release_json" >&2 || true + exit 1 + ;; +esac + +release_id=$(jq -r '.id // empty' "$release_json") +[ -n "$release_id" ] || { echo "ERROR: Forgejo response omitted release id" >&2; exit 1; } + +assets_json="$tmp/assets.json" +status=$(request GET "$api/repos/$owner/$repo/releases/$release_id/assets" "$assets_json") +[ "$status" = 200 ] || { echo "ERROR: asset listing failed (HTTP $status)" >&2; exit 1; } + +files= +for file in "$@"; do + case "$file" in *'*'*) continue ;; esac + [ -f "$file" ] || continue + files="${files}${files:+ +}$file" +done +[ -n "$files" ] || { echo "ERROR: no release files matched" >&2; exit 1; } + +printf '%s\n' "$files" | while IFS= read -r file; do + name=$(basename "$file") + case "$name" in + ''|*[!A-Za-z0-9._-]*) echo "ERROR: unsupported asset name: $name" >&2; exit 1 ;; + esac + existing_id=$(jq -r --arg name "$name" '.[] | select(.name == $name) | .id' "$assets_json" | head -n 1) + if [ -n "$existing_id" ]; then + if [ "$replace" != 1 ]; then + echo "ERROR: release asset already exists: $name" >&2 + echo "Set FORGEJO_REPLACE_ASSETS=1 to replace it explicitly." >&2 + exit 1 + fi + status=$(request DELETE "$api/repos/$owner/$repo/releases/$release_id/assets/$existing_id" "$tmp/delete.json") + [ "$status" = 204 ] || { echo "ERROR: deleting $name failed (HTTP $status)" >&2; exit 1; } + fi + + echo "uploading $name to $owner/$repo release $version" + status=$(request POST "$api/repos/$owner/$repo/releases/$release_id/assets?name=$name" "$tmp/upload.json" \ + --header 'Content-Type: application/octet-stream' --data-binary "@$file") + [ "$status" = 201 ] || { + echo "ERROR: uploading $name failed (HTTP $status)" >&2 + jq -r '.message // empty' "$tmp/upload.json" >&2 || true + exit 1 + } +done + +if [ "$publish" = 1 ]; then + edit_json="$tmp/edit.json" + jq -n --arg tag "$version" --arg name "Jerboa $version" \ + '{tag_name:$tag,name:$name,draft:false,prerelease:false}' > "$edit_json" + status=$(request PATCH "$api/repos/$owner/$repo/releases/$release_id" "$release_json" \ + --header 'Content-Type: application/json' --data-binary "@$edit_json") + [ "$status" = 200 ] || { echo "ERROR: publishing release failed (HTTP $status)" >&2; exit 1; } +fi + +echo "Forgejo release ready: https://git.jerboa.sh/$owner/$repo/releases/tag/$version" --- a/support/install.sh +++ b/support/install.sh @@ -33,8 +33,8 @@ Other environment: JERBOA_VERSION Exact version tag (alternative to --version) JERBOA_INSTALL_DIR Bin directory (default: $HOME/.local/bin) PREFIX Prefix used when JERBOA_INSTALL_DIR is unset - JERBOA_RELEASE_REPO SourceHut repo path (default: ~lisp/jerboa) - JERBOA_RELEASE_ORIGIN HTTPS SourceHut origin (default: https://git.sr.ht) + JERBOA_RELEASE_REPO Forgejo repo path (default: ober/jerboa) + JERBOA_RELEASE_ORIGIN HTTPS Forgejo origin (default: https://git.jerboa.sh) JERBOA_RELEASE_BASE Complete HTTPS release base URL override JERBOA_RELEASE_TARGET Target override, e.g. linux-amd64 JERBOA_RELEASE_SSH_NAMESPACE Signature namespace (default: file) @@ -170,8 +170,8 @@ if [ -z "$bindir" ]; then fi fi -repo=${JERBOA_RELEASE_REPO:-~lisp/jerboa} -origin=${JERBOA_RELEASE_ORIGIN:-https://git.sr.ht} +repo=${JERBOA_RELEASE_REPO:-ober/jerboa} +origin=${JERBOA_RELEASE_ORIGIN:-https://git.jerboa.sh} repo_url="${origin%/}/$repo" target=$(detect_target) case "$target" in @@ -181,10 +181,10 @@ esac file="jerboa-${version}-${target}.tar.gz" -# Release base URL. SourceHut git-tag artifacts are the only official release +# Release base URL. Forgejo release attachments are the only official release # download channel. JERBOA_RELEASE_BASE exists solely for local test fixtures. -sourcehut_base="${repo_url}/refs/download/${version}" -release_bases=${JERBOA_RELEASE_BASE:-$sourcehut_base} +forgejo_base="${repo_url}/releases/download/${version}" +release_bases=${JERBOA_RELEASE_BASE:-$forgejo_base} tmp_parent=$(choose_tmp_parent) tmp=$(mktemp -d "$tmp_parent/jerboa-install.XXXXXX") new file mode 100755 --- /dev/null +++ b/support/pack-jpkg-workspace.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# Verify and deterministically pack every Jerboa package under a workspace. + +set -u + +usage() { + echo "Usage: $0 WORKSPACE_DIR [OUTPUT_DIR]" >&2 +} + +[ "${1:-}" ] || { usage; exit 2; } + +workspace=$1 +output=${2:-dist/jpkg-workspace} +root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +jerboa=${JPKG_JERBOA:-$root/bin/jerboa} +replace=${JPKG_REPLACE_ARTIFACTS:-0} + +[ -d "$workspace" ] || { echo "ERROR: workspace directory not found: $workspace" >&2; exit 1; } +[ -x "$jerboa" ] || { echo "ERROR: Jerboa executable not found: $jerboa" >&2; exit 1; } + +mkdir -p "$output" +output=$(CDPATH= cd -- "$output" && pwd) +report="$output/report.tsv" +printf 'repository\tstatus\tartifact-or-error\n' > "$report" + +pass=0 +fail=0 +found=0 + +for manifest in "$workspace"/jerboa*/jpkg.sexp; do + [ -f "$manifest" ] || continue + found=$((found + 1)) + repo=${manifest%/jpkg.sexp} + repo_name=$(basename "$repo") + version=$(sed -n 's/^[[:space:]]*(version "\([^"]*\)").*/\1/p' "$manifest" | head -n 1) + if [ -z "$version" ]; then + printf '%s\tfail\tmissing version\n' "$repo_name" >> "$report" + echo "FAIL $repo_name: missing version" >&2 + fail=$((fail + 1)) + continue + fi + + artifact="$output/$repo_name-$version.jpkg" + if [ -e "$artifact" ] && [ "$replace" != 1 ]; then + printf '%s\tfail\tartifact already exists\n' "$repo_name" >> "$report" + echo "FAIL $repo_name: artifact already exists (set JPKG_REPLACE_ARTIFACTS=1)" >&2 + fail=$((fail + 1))