A supply-chain policy proxy for package registries, written in Haskell. The name Écluse (Quebec French: "ayy-cluze", [e.klyz]) is French for a canal lock: the controlled passage every dependency clears before it reaches your build.
Verifying the image covers how to verify a release instead of trusting it: the keyless provenance and SBOM attestations, and the bit-for-bit reproducible rebuild.
Status: generally available, pre-1.0.0. The npm packument, tarball, and publish paths run today and are ready for use. A
pypimount serves reads, the PEP 691 Simple index and the distribution files under it, and writes nothing. An AWS-backed deployment is wired end to end: an SQS mirror queue, a demand-driven worker, and writes under a container-role credential. The GCP backends and the deployment runbook are still to come. Configuration can still change beforev1.0.0, though repeated future-proofing passes aim to keep changes additive. The operator manual is the deployment contract.
Haddock API docs auto-publish from main.
Écluse is a proxy you put in front of public package registries to protect the builds that install from them. Point your CI and developer tooling at Écluse instead of at a public registry. Écluse fetches from that registry on their behalf and decides which versions a build may install. npm was the first protocol supported, and any client that speaks it works, such as npm, pnpm, yarn, or bun. PyPI is served for reads, so pip, uv, and Poetry install through the same gate.
A new public version waits in a quarantine, seven days by default, before a build can install it. Most malicious publishes are found and pulled within days, so the wait alone sidesteps them, with no attempt to detect malice. With an advisory database synced, a version that an advisory names as the exact fix for a vulnerability skips the wait, so the quarantine never delays a security patch. Everything else is deny by default and opt-in by name.
If you run a private registry, Écluse reads it first and passes your own packages through untouched.
Any https registry that speaks the ecosystem's protocol serves in that role. Écluse can also mirror
each admitted public version into a registry you nominate, so a mirrored version survives a public
outage or yank. You declare each registry under a tag that names the store behind it. A mirror
target under the codeArtifact tag mints its own short-lived write token, and the other tags take
a static token you supply. Écluse hosts no packages itself.
A mirror keeps what it was given, so a version your rules later deny stays until something removes
it. ecluse dredger is that role: it walks each mirror store, re-evaluates the versions a new
advisory or an operator deny can have changed, and deletes only what a named rule condemns. It is
the only role that deletes, and it does so only from a store carrying the operator's own consent
marker, under a per-cycle cap.
The operator manual covers running Écluse.
Ecluse.Core.Snapshot carries each upstream body's digest through metadata projection and assembly.
Ecluse.Core.Package.Entry defines artifact coordinates, and Registry.ServedDocument selects only
the exact entries that admission kept. The npm and PyPI adapters own their wire parsing and rendering.
Ecluse.Core.Registry.Metadata.Projection shares full-document validation and metadata error mapping across adapters.
docs/architecture.md has the design: the registry roles, the rules
engine, and the mirror queue. The threat model (OWASP Threat Dragon, STRIDE) lives in
threat-modelling/ecluse.json. The site build renders it as a
readable register.
The operator manual covers configuration, connecting your
clients, the network-egress safety you're responsible for, the rule policy, and the health and
observability endpoints. The docs/architecture/ documents are the
why behind each setting.
Écluse publishes to GitHub Container Registry and nowhere else: ghcr.io/alexadewit/ecluse,
one immutable tag per version, no latest. Pin a deployment by digest and verify what you
pin (Verifying the image).
Release and supply-chain operations
covers the publish flow.
Releases. The release workflow publishes every version to GitHub Container Registry (
ghcr.io/alexadewit/ecluse).
Each tag is a single multi-arch image (linux/amd64 + linux/arm64) carrying keyless
(Sigstore) provenance and SBOM attestations in the public Rekor log. Each
GitHub Release publishes the digest for its
version and attaches those attestations as assets. Verify with the GitHub CLI:
IMAGE=ghcr.io/alexadewit/ecluse@sha256:… # pin by digest, the index
# Provenance on the index. `verify` checks one predicate type per run, and this is
# its default, so the flag is optional:
gh attestation verify "oci://$IMAGE" --repo AlexaDeWit/Ecluse \
--predicate-type https://slsa.dev/provenance/v1
# The SBOM is attested per platform, never on the index, so name a platform digest
# (`docker manifest inspect` lists them) and the SPDX predicate type:
gh attestation verify "oci://ghcr.io/alexadewit/ecluse@<platform-digest>" \
--repo AlexaDeWit/Ecluse --predicate-type https://spdx.dev/Document/v2.3The command checks the signature against the release workflow's identity and the Rekor
log, and confirms the subject matches your digest. Add --format json to extract the
documents.
Both commands read GitHub's attestations API. Each release also attaches the same documents as assets, so you can verify a copy pinned to the release object instead:
gh release download v<version> -p 'ecluse-*-provenance.sigstore.json'
gh attestation verify "oci://$IMAGE" --repo AlexaDeWit/Ecluse \
--bundle ecluse-<version>-provenance.sigstore.jsonA …-sbom.sigstore.json bundle needs --predicate-type as well. The two
ecluse-<version>-<arch>-sbom.spdx.json assets are the SPDX documents themselves, not
bundles: read those directly, and pass --bundle the matching …-sbom.sigstore.json to
check the signature over one.
Stronger still, the image is bit-for-bit reproducible. Rebuild it from pinned source and compare, instead of trusting anyone.
nix build github:AlexaDeWit/Ecluse/<ref>#dockerImage # → ./result (a docker-archive)See Release and supply-chain operations.
The version lives in ecluse.cabal's version: field, and the image, git, and release tags
derive from it. VERSIONING.md is the policy: what the numbers promise, and
what 0.y.z does not.
Nix with flakes is a hard dependency: the whole toolchain (GHC 9.10, Cabal, fourmolu, hlint, Semgrep) comes from the pinned dev shell.
nix develop # enter the dev shell (direnv does this automatically)
task build # build the library, executable, and tests
task check # fast pre-push checks (a subset of the gate)
task gate # the full CI-gate mirror (adds the Docker integration + Haddock tiers)Getting Started covers full setup, the task workflow, and
dependency locking. CONTRIBUTING.md covers the contribution process and
DCO sign-off. The Code of Conduct governs participation, and
GOVERNANCE.md says who decides.
Ecluse.Core.Stream provides the byte limiter shared by EPSS ingestion and advisory downloads.
| Path | Purpose |
|---|---|
core/ |
ecluse-core library: the pure, ecosystem-agnostic capability core (Ecluse.Core.*) |
core/src/Ecluse/Core/Server/Cache/ |
Bounded stores and conservative accounting for retained release fields |
runtime/ |
ecluse-runtime library: the effectful edge (OTel SDK, warp, scribes, and cloud adapters, Ecluse.Runtime.*) |
src/ |
ecluse library: the composition shell that assembles and runs the tiers (Ecluse.*) |
app/ |
Executable entry point, thin wiring only |
test/ |
Unit and integration tests |
config/ |
The embedded defaults (default.yaml), the schema guidepost operator configs override |
runbooks/ |
Maintainer procedures run step by step (releases) |
docs/ |
Architecture and design documents |
web/ |
The documentation site (Zola): content, templates, and styles. web/content/docs/ holds the operator manual |
flake.nix |
Nix dev shell (GHC 9.10, cabal, HLS, ghcid) and the package build (nix build) plus hermetic checks (nix flake check) |
