| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Boot
Description
Écluse (package ecluse) sits between consumers (developers, CI) and a package
registry, applying a configurable resilience policy before any dependency reaches
a build, without hosting packages itself. The name is French for a canal lock: a
chamber whose gates never open at once. Every dependency is held and cleared
through that controlled passage before it is admitted to a build.
The goal is resilience, not malware detection: shrink the blast radius of a bad publish (a hijacked maintainer account, a race-to-publish, a typosquat) rather than promise to recognise malice. Écluse is not a registry: storage is delegated to whatever backend the operator runs (AWS CodeArtifact, GCP Artifact Registry), and Écluse only governs what may be fetched from, and mirrored to, those backends. npm is the first ecosystem; the domain model is ecosystem-agnostic so PyPI and RubyGems can follow.
How a request is cleared
Écluse speaks a registry's native protocol across three read-path registries (the client's, a private upstream of already-vetted packages, and the public registry), and the two request shapes use them differently:
- A tarball request is gated for that one version: a private-upstream hit is streamed unfiltered (already vetted); on a miss, the proxy fetches the version's public metadata, evaluates the rules, and either streams it from public and enqueues an asynchronous mirror job or returns a denial.
- A packument (metadata) request is a merge: the private and public
upstreams are fetched in parallel, public versions are filtered by the rules
while private versions are trusted, and the two are combined into one document
(private wins a version collision, an integrity divergence is flagged as a
supply-chain signal, and
latestis repointed to the newest survivor).
Two properties run through both shapes: the rules engine is deny by default (a version is admitted only if some rule allows it and none denies it), and mirroring is demand-driven, so only versions actually pulled are mirrored, never on the request's critical path.
How the code is organised
Écluse is a functional core with effects at the edges: the policy and
protocol logic is pure and trivially testable, and IO is confined to a thin
shell. Swappable backends sit behind handles (records of functions chosen at a
single composition root), so a new cloud or a new ecosystem is an added
implementation behind an existing handle, not a structural change.
The library's vocabulary, roughly from the pure core outward:
- Domain model: Ecluse.Core.Package (the ecosystem-agnostic package vocabulary the rules reason over), Ecluse.Core.Version (version identity and per-ecosystem ordering), and Ecluse.Core.Ecosystem (the ecosystem tag the rest dispatches on).
- Policy: Ecluse.Core.Rules (deny-by-default evaluation) over the rule types in Ecluse.Core.Rules.Types.
- Protocol boundary: Ecluse.Core.Registry (the registry-protocol handle),
Ecluse.Core.Registry.Npm.Wire and Ecluse.Core.Registry.Npm.Project (the lenient npm
wire decoders and their projection onto the domain model),
Ecluse.Core.Registry.Npm.Route (the npm path grammar), and Ecluse.Core.Server.Route
(the shared serve-action
Routeset and the injected route classifier). - Cloud handles: Ecluse.Core.Credential (minting the mirror-target write token) and Ecluse.Core.Queue (the durable mirror-job hand-off to the worker).
- Mirror worker: Ecluse.Core.Worker (the supervised consume loop that fetches, verifies against the job's integrity digest, and publishes an approved artifact).
run is the entry point the ecluse executable invokes (see Main). It lives
in the library, not in app/Main.hs, so the composition root is a single
importable unit and app/Main.hs stays a thin shell that only calls it.
Further reading
docs/architecture.md is the systems-design index: the vision, the end-to-end
request lifecycle, and a map to the per-concern design documents. CONTRIBUTING.md
covers the codebase layout and testing strategy, and STYLE.md the coding and
documentation conventions.
Synopsis
- data BootEnv = BootEnv {}
- withBootEnv :: (BootEnv -> IO ()) -> IO ()
- data BootAborted = BootAborted
- orExit :: (e -> Text) -> Either e a -> IO a
- logBootWarning :: LogEnv -> Text -> IO ()
- logBootInfo :: LogEnv -> Text -> IO ()
- logRuleBootOrder :: LogEnv -> [MountBinding] -> IO ()
- buildMirrorQueue :: LogEnv -> MirrorQueuePlan -> IO MirrorQueue
Documentation
The boot context assembled once at start-up and handed to each subcommand: the
validated configuration, the process logger, and the telemetry handle. withBootEnv
builds it, and the ecluse entry point (see Ecluse) dispatches the selected
subcommand over it. The heavier serve- and worker-side handles (the HTTP managers,
the mirror queue, the metadata cache) are built later, per subcommand (see
Ecluse.Proxy).
Constructors
| BootEnv | |
Fields
| |
withBootEnv :: (BootEnv -> IO ()) -> IO () Source #
Assemble the BootEnv and run action within it: load and validate the
configuration (failing fast on any error), apply the runtime posture, build the
logger, and bracket the telemetry substrate for the action's lifetime.
data BootAborted Source #
Raised to abort start-up after a boot phase has reported its aggregated
failure to stderr. A distinct type -- rather than a bare exitFailure -- so the
abort is observable in a test without the process actually exiting; uncaught, it
propagates to main and the runtime exits non-zero, the operator-facing fail-fast.
Constructors
| BootAborted |
Instances
| Exception BootAborted Source # | |
Defined in Ecluse.Boot Methods toException :: BootAborted -> SomeException # fromException :: SomeException -> Maybe BootAborted # displayException :: BootAborted -> String # backtraceDesired :: BootAborted -> Bool # | |
| Show BootAborted Source # | |
Defined in Ecluse.Boot Methods showsPrec :: Int -> BootAborted -> ShowS # show :: BootAborted -> String # showList :: [BootAborted] -> ShowS # | |
| Eq BootAborted Source # | |
Defined in Ecluse.Boot | |
logRuleBootOrder :: LogEnv -> [MountBinding] -> IO () Source #
buildMirrorQueue :: LogEnv -> MirrorQueuePlan -> IO MirrorQueue Source #