◆ Flux

Packages and distribution

A Flux library is distributed as a compiled, sandboxed artifact with a sealed manifest, pinned by the hash of its contents — never by a version range, never by a name a registry resolves at install time. This page specifies what a package is, how content addressing dissolves the dependency diamond, how a readable version becomes a hash, what a fluxpack archive carries, and how capabilities aggregate along import edges without ever escalating. The reproducible-build harness that proves two compilations land on the same bytes is specified at Verification; the capability catalogue itself lives in host-services.

That one decision — pin by hash — propagates into everything here: the dependency diamond is dissolved rather than solved, the build is reproducible, a purchased library cannot smuggle a capability into your app, and a script you shipped last year still runs, byte for byte, today.

New here? Start with Guide §11 — Determinism, replay and trust → — the accessible retelling of why pinning by hash is the property everything else hangs on.

What a package is

Two notions are easy to confuse, so they are named apart:

a registry (indicators, representations, drawing tools) a runtime extension point — a script registers itself under an id, and the host serves it like a built-in
a package a versioned dependency artifact, pinned by hash, with an aggregated capability manifest — something you import

A package is named by a readable coordinate — author/package — and imported:

FLUX
import author/indicators as ind

plot ind.superSmoother(close, 20)     // its `pub` entries; everything else stays private

Only entries marked pub cross an import boundary. private and package visibility remain intra-script encapsulation, and are orthogonal to the package boundary.

Content addressing dissolves the diamond

The coordinate author/package is a readable indirection. What is actually linked is a content hash.

So two versions of the same library are two distinct hashes that coexist, with no name conflict. The classic diamond — A depends on C@x, B depends on C@y, your app pulls in both A and B — is not resolved. It does not arise:

The dependency diamond, dissolved Figure — two versions of one library are two linked units; there is no version to select, and therefore no conflict to resolve.

And the monomorphic type discipline makes that safe rather than merely possible: a record exported by C@x and one exported by C@y are two distinct monomorphic types. The seam between A and B can never pass one where the other is expected — that is [ErrField], at compile time, by inference. Not a warning. Not a convention.

The size cost of coexistence is recovered by common-subexpression elimination across the graph: two versions that share an identical sub-graph share it at the node level, whatever their names.

Selecting a version, when a human is in the loop

The grammar of an import is exactly import author/package [as alias]. There is no version constraint in the source, and that is not an oversight — a source that carried a range would be a source whose meaning depended on what a registry answered that day.

A readable version layer above the coordinate — the place where a human states “at least 1.2” and a tool turns that into a hash — is designed as an optional overlay on the naming layer, never a production of the language; its rollout follows v1. Where it applies, the resolution is minimal version selection: take the lowest version satisfying every constraint, then pin its content hash, and write the hash into the lock.

Why the lowest, and why not a solver. Minimal version selection is deterministic by construction — no solver, no search, no “resolution changed because the registry did”. The build becomes a pure function of the constraint set. The alternative — “the newest compatible version floats underneath you” — would break byte-identity and server-side replay, because two builds of the same source would link different code.

The readable version lives on the naming layer, and it is a convenience for the human who chooses. Underneath the artifact, exactness is the hash — and the hash is what the source, the lock and the server all speak.

The lockfile is the build hash

An application resolves its graph once, into a set of content hashes — the transitive closure — plus the pinned compiler version and the pinned routines. That set is the build hash.

Which is what makes this sentence definable, and checkable: the same dependency graph produces the same bytes. Byte-identity between the two engines and server-side replay both re-link the exact closure the lock pinned — never a “compatible version” chosen at link time, which would desynchronize client and server.

The manifest is where that closure becomes inspectable. Four of its fields are inputs to the build hash, which is another way of saying that changing any of them produces a different artifact with a different name:

Pinned in the manifest Why it is part of the identity
the module hash the sealed bytes — what a server re-executes, and what a verifier recomputes
the toolchain — compiler version, and the pinned optimizer backend the same source through a different compiler is different bytes. Ignoring it would serve a cached module that no recompilation would ever produce again
the pinned routines — the hash of the deterministic maths library itself the module was gated against that library. A consumer holding a different one is running code nobody verified
the declared memory — pages, state cells, capacity checked against the module before instantiating it, so a footprint is a contract rather than a surprise

Why the maths library is in the hash and not merely “recommended”. It is the subtlest of the four, and the one a normal packaging system would have missed. A pack’s bytes are proved byte-identical to the interpreter’s evaluation against a specific implementation of the transcendental functions. Link the same module against a different one — a bug fixed, a rounding tightened, a genuine improvement — and the proof no longer covers it. So a consumer whose maths library does not match the one in the manifest refuses to run the pack, and re-fetches. Not a warning, not a compatibility shim: a refusal. The drift this closes is exactly the drift nobody would notice, because the numbers would still look right.

Linking

A purchased dependency cannot be compile-inlined: its source is never shipped — you do not inline what you are not allowed to receive. So a third-party library is a separate, signed WebAssembly module, linked by module imports. And that is the rule for every dependency, not only the purchased one: an open dependency ships its source, but it is still linked as its own module, so that its provenance, its trust tier and its hash stay its own rather than dissolving into yours. Two things come with that:

Every app pins the exact hash of every dependency, so an “update” produces a new app hash — never a silent drift underneath a frozen app. Mutable shared third-party modules are forbidden, because they would break byte-identity and replay at the root.

Capabilities aggregate — and cannot escalate

This is the security property that makes a marketplace tolerable:

manifest(A) = ( ⋃ emit Cap over the transitive closure of A ) ⊓ the user's grant

Three consequences, all normative:

  1. A transitive dependency’s appetite is visible. If a library three levels down wants the network, that request surfaces in your app’s manifest, and the person installing your app sees it before they install. There is no hidden capability, and the confused-deputy attack is closed at the root.
  2. No dependency can exceed what the user granted. Authority flows only along import edges, capped by the grant.
  3. A dependency holds no capability object at all — so it can neither re-delegate one nor amplify one.

Non-escalation is structural: it is recomputed at compile time and pinned into the app’s hash.

The artifact

A distributed package is a fluxpack: an archive holding the compiled module, the sealed manifest, the compiled metadata a consumer needs in order to attach the module without a compiler — and, when the author distributes it openly, the source it was compiled from.

Entry What it is
the manifest canonical JSON — the sealed capability list, the provenance, the parameter schema, the declared presentation
the compiled module the WebAssembly the consumer actually runs
the compiled metadata sink layout, column offsets, series names — so a consumer attaches the module without inferring anything
the source optional, and the only transparent thing in the archive. Present ⇒ the pack is verifiable
documentation, an icon, a signature optional; the icon is hard-sanitized at load, because a pack is untrusted input

The compiled intermediate representation is never shipped, in any class of pack. A verifier that wants to check the module does not read an IR the author supplied — it re-derives the IR from the source and recompiles. Shipping an IR would mean trusting it.

What a fluxpack contains Figure — the artifact carries everything a consumer needs to decide, and nothing a consumer must trust.

Three distribution classes, and open is the default

Whether the source travels is a declared property of the pack, and it is the first field a consumer reads:

Class The source What the consumer can do
open — the default shipped, in the archive recompile it locally and check the module against it, byte for byte. Identity is checkable, not promised
closed not shipped run it in the sandbox, and inspect the sealed manifest — but never re-derive the module
licensed not shipped, and the module is encrypted and key-gated the same, under a licence the host enforces

That default is load-bearing, and it is the opposite of the usual one. A package’s honesty about what it computes is checkable unless its author opts out — and opting out is visible in the manifest, before installation, next to the capability list. A consumer who is handed a closed pack knows exactly what they have given up.

Why a closed pack is still safe to run. Verifiability and safety are two different properties, and it is worth refusing to conflate them. The sandbox is the safety: a pack is a pure function over numbers, with no clock, no network, no I/O and no way to grow its own memory. The worst a malicious pack can do is compute wrong numbers — a bad signal, which the capability model and the sanitizer contain, and which no amount of source-reading would have caught either. Verifiability is a different guarantee: not “this cannot hurt me” but “this is what it says it is”. open gives you both. closed gives you the first, and says so.

The archive is deterministic, and the hash is the name

A package is content-addressed by the hash of its archive bytes, so the archive itself must be reproducible or the name is not stable. An ordinary zip is not: entry order, timestamps, permission bits and compression all vary. This one is constrained until it is:

Storing rather than compressing costs almost nothing — the wire is compressed by the transport anyway, and a pack is kilobytes — and it closes the decompression-bomb surface by construction rather than with a limit somebody has to get right.

A pack is untrusted input. It is verified before it runs: the structure, the manifest, the provenance, the declared limits — and the declared memory footprint is checked against the module before instantiation, so the footprint is a contract rather than a surprise. And the rebuild gate closes the last gap — the same source and the same lock, recompiled twice on different machines with different thread counts, must produce a byte-identical module. A non-reproducible build would break server-side replay silently, because a value-level oracle cannot see the bytes emitted across two compilations. The gate and the harness that enforces it are specified at Verification; what packaging adds is the deterministic archive that makes the pack hash stable in the first place.

Licence compatibility is computed on the closure at publication and can refuse a publication (a paid closed artifact built on a copyleft dependency, for instance) — surfaced in the same inspect-before-install panel as the manifest.

The compute runtime is retained forever, append-only. The shared module of native kernels is linked by hash like any other dependency, so evolving a kernel produces a new hash and re-links only on republication — never a drift under a frozen app. An app bought years ago pins its runtime and stays verifiable; a build whose runtime reaches end-of-life is marked locally scored, never silently invalidated.

What is deliberately excluded

Excluded Why
nested duplicate installs of the same library it fights byte-determinism; content addressing replaces it
constraint solving not bit-reproducible — minimal version selection or a hash instead
dynamic loading of untrusted source eval and friends are forbidden; every “load” is a host-mediated instantiation of a pre-vetted module
generics across module boundaries exports are monomorphic in v1 — which is exactly what makes the diamond safe
feature flags / conditional compilation across modules they would change the bytes; only the choice of dependency may do that

The public registry is a rollout, not a mechanism: the packaging, the pinning, the aggregation and the verification are all built. What follows v1 is deploying the place where strangers publish to strangers.

See also