Packages (kola.pack)
A single .kl file is a module. Once a program grows past one file, or you want to share code with other programs, you put the files in a pack: a directory with a kola.pack manifest. The manifest gives the pack a name and a version, lists its dependencies, and states in a few lines how much of the outside world the pack's code may reach. kola audit checks that statement against the code. This page covers the layout, the manifest, the audit, dependencies, and publishing.
What a pack is
A pack is a directory that holds .kl files and a kola.pack manifest written in TOML, a simple configuration format of [sections] and key = value lines. The manifest's [pack] name becomes the import root for the files inside it, the same way std is the import root for the standard library. Inside a pack named framing, the file src/frame.kl is the module framing::frame.
A file that has no kola.pack above it can only import from std. To use code from another directory, that code has to be a pack, and your own file has to live in a pack whose manifest declares it as a dependency.
Modules work the same inside a pack as outside one: pub, use pack::module;, impl blocks, all as described in Modules, the standard library, and fn references. A pack is a boundary drawn around modules, not a new kind of module.
The layout
A pack is flat: the directory is the pack. The manifest sits at its root, the sources under the directory that [pack] src names, and beside them the optional examples/ and docs/ directories and the usual README.md, CHANGELOG.md, LICENSE and NOTICE. kola pack build archives exactly this tree.
framing/
kola.pack identity, budget, dependencies
src/
frame.kl use framing::frame;
examples/
main.kl a program built against the pack
docs/ shipped in the archive
README.md CHANGELOG.md LICENSE NOTICEkola pack new <name> [--license <spdx-id>] [--bin] writes this skeleton for you, with a commented manifest, one module, one test and one example.
The source root is decided in a fixed order. An explicit [pack] src wins. Without it, the directory of [pack] entry is used. Without either, the sources sit at the pack root. kola never probes the disk for a likely directory, so creating an empty src/ changes nothing. If src and entry disagree about the root, the manifest is refused. The source root affects only how use resolves: every path written inside kola.pack itself, such as [embed] files, is relative to the pack root.
A library usually declares src and no entry. entry names the file where main lives, and many libraries have no single root module, so there is nothing true to put there. A program pack (kola pack new --bin) names its entry, and kola audit checks that the file exists.
An older nested layout, <name>/<name>/kola.pack, still resolves. A directory that both has its own kola.pack and contains a child pack is refused, because the tool cannot tell whether it is a pack or a collection of packs.
Files under examples/ are targets of the pack, not packs of their own. They compile with the pack in scope and need no manifest. kola check . and kola pack build . compile them, so an example that stops compiling fails the build; kola run examples/hello.kl runs one.
The manifest
Here is a complete manifest for a small library:
[pack]
name = "widget"
version = "0.1.0"
src = "src"
description = "one line for the registry card"
license = "Apache-2.0"
[caps]
allowed = ["Env"]
[caps.tests]
allowed = ["Rng"]
[caps.examples]
allowed = ["Sys", "log"]The [pack] section accepts exactly these keys: name, version, src, entry, description, license, source and c_abi. source is an informational pointer to where the code lives (a git URL, say); kola prints it and never fetches it, so a manifest inside a downloaded archive cannot redirect where your next download comes from. c_abi lists the modules that C code calls into, which is covered in Embedding kola in a C program.
An unknown key is an error, not something to ignore. A manifest line that nothing checks is a claim nobody verifies, and the manifest exists to hold claims a reviewer can trust. The diagnostic lists the known keys and suggests the nearest one:
error[pack manifest]: malformed manifest: `[pack] vresion` is not a key this compiler knows — ... Known keys: c_abi, description, entry, license, name, source, src, version. Did you mean `version`?There is no key for the language version. The card a pack builds against is the compiler's own (kola --version prints it), and a card = ... line is refused with a note telling you to delete it.
The other sections are:
Section | What it holds |
|---|---|
| the capability budget of the library code |
| extra budget for |
| dependencies, with exact versions |
| dependencies that only examples or only tests may use |
| static files compiled into the binary |
| an optional, enforced region-leak budget |
| per-architecture C or assembly files for |
|
|
A manifest that does not parse cleanly (an unknown key, a duplicate section, a retired spelling) is error[pack manifest], and every command refuses it, kola check and kola test included. The checker stops before reading your code, because a budget it cannot read unambiguously is not a budget it can enforce.
The capability budget
The heart of the manifest is [caps] allowed, the capability budget. In kola, a function can reach the outside world (the clock, files, the network) only through a capability, a value that represents permission to do it. main receives the whole set, Sys, and hands parts of it down. The budget lists every capability type the pack's code may obtain from Sys. A pack that lists Clock may read the time; a pack that lists Dial may open outbound connections; a pack whose list is empty can do neither, and no amount of code inside it changes that.
[pack]
name = "kolanative"
version = "0.1.0"
[caps]
allowed = []That is a real manifest: a code generator that builds machine instruction words into a buffer. With allowed = [] it cannot touch a file, a socket or the clock, and a reviewer does not need to read its encoder functions to believe that. Parsers, protocol engines and data structures should look like this. When a library needs the time, it can usually take now: u64 as a parameter instead of a Clock; when it needs the network, it can drive a connection its caller opened.
The names the list accepts are the world capabilities of SPEC-CARD §7: Args, Clock, Cpu, Dev, Dial, Env, Fs, Inherited, Listen, Mem, Mmio, Netlink, Ping, Proc, RawIp, Rng, Sim, Stdin, Stdout, Sys, Tty, Window, plus one lowercase entry, log. A few deserve a comment:
Fscovers the whole filesystem family. You do not listFileorDirseparately, because you cannot obtain one withoutFs.StdoutandStdinare separate lines, because writing a program's output and reading its input are different audit questions.Pingis separate fromDial, because the ability to sweep a network with ICMP is not implied by the ability to open a connection.Devis a granted character device, a peer with its own protocol.Mmiois for register windows only, so that on a manifest it keeps meaning exactly that.
log is an effect, not a capability: it names the !{log} row (SPEC-CARD §6). No capability supplies it, so it is listed on its own. A log sink is write-only, but it is still a channel out of the program, and "what does this pack log" should be its own line. kola audit prints it as log (effect).
Sys in the list grants Sys and nothing else. If it covered everything, then allowed = ["Sys"] (the natural line for any pack with a main(sys: Sys)) would say nothing about what the pack does, and every other entry would be redundant. So a pack that reaches sys.clock must list Clock as well, and the audit says so when it doesn't.
What the budget measures
The budget limits what a pack may take out of the root set. It does not limit what the pack is handed. If a function receives a Conn as a parameter, someone else spent Dial or Listen to create that connection, and the authority belongs to the caller. An HTTP server library that serves requests over sockets its caller accepted correctly declares no Listen at all.
The same holds for every value that was minted elsewhere: linear handles (Conn, File, Device, UdpSock, ...), granted roots (Dir, DirRo), hardware window tokens (MmioWindow, RamWindow, RamWindowRo, IoWindow), Cpu, and fn references whose effect row carries io, block or hw. kola has no closures, so a fn reference is how you hand a library a seam into the world (q: fn(&[u8]) -> u64 !{io, block}); the library that calls it holds nothing. A pack that only drives what it is handed audits clean with allowed = [], and that is the correct manifest for it. Declaring Dial there would make the budget claim more than the pack can do.
The budget therefore does not answer "what can this pack touch". The audit answers that question separately, on its reaches: line (below).
Named grants
Some authority is granted by name: a directory, a device, a binary to run, a hardware window. The operator binds the name when starting the program (--fs-root db=/srv/db, --proc-bin ffmpeg=/usr/bin/ffmpeg), and the program asks for it by name (sys.fs.root(b"db")). The manifest lists the names, not just the capability type:
[caps]
allowed = ["Fs", "Mmio", "Dev", "Proc", "Dial"]
fs_roots = ["db", "zones:ro"] # sys.fs.root(b"db") / sys.fs.root_ro(b"zones")
mmio_windows = ["uart0"] # sys.mmio.window(b"uart0")
ram_windows = ["ring0"] # sys.mmio.ram(b"ring0")
io_windows = ["com1"] # sys.mmio.io_window(b"com1"), x86 port I/O
devices = ["widget"] # sys.dev.open(b"widget")
proc_bins = ["ffmpeg"] # sys.proc.spawn(b"ffmpeg", ..)
dial_hosts = ["api.example.com:443"] # dial.to_host(b"api.example.com", 443)So the audit reports "wants db and zones" rather than "wants the filesystem". A name the code asks for that is not on the list is a violation, and so is a read-write request (root) for a root the manifest marks :ro. The full set of name lists is fs_roots, mmio_windows, ram_windows, io_windows, devices, proc_bins, dial_hosts, extern_syms, reserves, netlink and perturbs.
Budgets for tests and examples
A pack's tests and examples usually need more than its library code. A test may run a loopback server; an example's main(sys: Sys) holds the whole root set. Putting that on [caps] allowed would tell every reviewer that the library itself may do those things.
So each target gets its own section:
[caps]
allowed = []
[caps.tests]
allowed = ["Dial", "Listen"]
fs_roots = ["fixtures"]
[caps.examples]
allowed = ["Sys", "log"]Three rules apply. The key inside a scoped section has the same name as in [caps]: allowed, and every other key from the list above. A scoped budget adds to [caps], so a capability already granted there is not repeated. And the split follows reachability, not file position: a private helper whose only callers are test items is test code, while a private function nothing calls at all is treated as library code.
The older spelling, tests = [...] as a key inside [caps], is refused with a message that names the section to write instead.
Running kola audit
Take the widget manifest above, with allowed = ["Env", "Rng"], and this library module:
use std::text::utf8;
use std::net;
// Reads one environment variable: spends the Env the manifest grants.
pub fn home_dir(env: &Env) -> Option[&heap str] !{io, alloc(heap)} {
match env.get(&b"HOME") {
Some(bytes) => utf8::to_str(bytes),
None => None,
}
}
// Writes to a connection the caller already opened.
pub fn serve_one(conn: &mut Conn, out: &[u8]) -> Result[(), (NetErr, OsErr)] !{io, block} {
net::send_all(conn, out)
}The audit prints prose, because a person reads it:
$ kola audit widget
audit: caps within budget — pack `widget` (used=["Env"] ⊆ allowed=["Env", "Rng"])
audit: examples — used=["Env", "Sys"] ⊆ allowed=["Sys", "log (effect)"] (additive to `[caps] allowed`)
warning[pack caps unused]: `widget` declares `Rng` in `[caps] allowed` but nothing in the pack needs it — not its sources, not a `test` item, not an example target. ...
audit: reaches: Conn — already-minted authority value(s) on this pack's pub fn signatures ...
audit: leaks — ...
audit ok: 0 violation(s) — every budget checked in this run is within its declared limitsThere are four things to read here. The first line is the verdict for the library: it uses Env, which is allowed. The second does the same for the example targets. The warning says that Rng is granted but nothing uses it; that is a warning rather than an error, because a pack may reserve budget on purpose, but a budget wider than the code misleads a reviewer. The reaches: line names Conn as authority that crosses the pack's public signatures without being minted inside it. It enforces nothing; it answers the blast-radius question that the budget cannot.
The leaks lines report the region-leak census (places where memory may accumulate in a region that is not freed until much later) for the pack's own sources, for its dependencies, and for its examples; Region leaks explains them. The last line is the verdict, and only a passing run prints audit ok. A failing run ends with audit: FAILED (n violation(s)) and exits with status 1.
For scripts, --json (or --format json) adds one JSON object per pack, with caps_used and caps_budget, the same pairs for every named list and every scope (fs_roots_used, caps_used_tests, ...), the effects, authority_reached for the reaches: set, and a leaks object. The prose goes to stderr, so stdout under --json is clean JSON. If you parse audit output in CI, pin --json.
kola audit on a directory that contains several packs audits each one as itself and prints one verdict over all of them. kola pack ci writes a CI workflow (a script your git server runs on every push) that runs the audit for you. A budget nobody audits is only a comment.
Errors in both directions
The audit compares the code against the manifest, and the manifest against the set of real capabilities. This pack makes three mistakes at once:
[pack]
name = "badcaps"
version = "0.1.0"
src = "src"
[caps]
allowed = ["Sys", "Io"]use std::log;
pub fn tick(sys: &Sys) !{io, log, block} {
log::info("tick");
sys.clock.sleep(1u64);
}$ kola audit badcaps
error[pack budget]: `Io` in [caps] allowed is not a capability this compiler knows — ... Known names: Args, Clock, Cpu, Dev, Dial, Env, Fs, Inherited, Listen, Mem, Mmio, Netlink, Ping, Proc, RawIp, Rng, Sim, Stdin, Stdout, Sys, Tty, Window, log. `Io` was RETIRED as a capability ... add whichever of `Stdout`/`Stdin`/`log` the pack actually reaches
error[pack budget]: capability `Clock` used on the production surface but not in the pack budget [caps] allowed = ["Sys", "Io"] — note `Sys` no longer bundles sub-capabilities ...
error[pack budget]: pack performs a `log` effect (!{log}) but its budget does not grant it — add `log` to [caps] allowed = [...]. ...
audit: 3 budget violation(s) in pack `badcaps`
audit: FAILED (3 violation(s)) — see the `error[...]` line(s) above; `kola audit` exits 1Io was once a capability and no longer exists. An entry that names nothing is an error, not a warning, because to a reviewer it looks exactly like an entry that names real authority. The diagnostic is specific about the common wrong names: a retired one is told what replaced it; a handle such as Conn or File is told to declare the capability that mints it, "or nothing at all if the handle only ever arrives as a parameter"; a derived capability such as Dir or MmioWindow is told the minting capability and the name list that goes with it. The second error shows that Sys does not cover sys.clock, and the third that logging must be declared.
The direction matters for severity. A capability the code uses but the manifest lacks is an error. A real capability the manifest grants but nothing uses is a warning, computed by removing each entry in turn and re-running the same enforcement, so the warning can never disagree with what actually fails.
kola check runs the same budget analysis but reports violations as warnings and keeps compiling, so you see your type errors too. kola run and the native executors refuse a pack that is over budget. A green check therefore says nothing about audit; run both.
Dependencies
A pack lists the packs it imports under [deps]:
# app/kola.pack
[pack]
name = "app"
version = "0.1.0"
src = "src"
entry = "src/main.kl"
[caps]
allowed = ["log"]
[deps]
mid = { version = "0.1.0", caps = [] }// app/src/main.kl
use mid::calc;
use std::log;
fn main() -> u8 !{log} {
let a = calc::answer();
log::info(f"answer={a}");
0
}
test chain_resolves {
assert calc::answer() == 42;
}(This file only checks inside its pack, next to the mid pack it imports.)
The version is compared with the dependency's own [pack] version by exact string equality. Many package managers accept a range such as "any 1.x newer than 1.2" and run a solver to pick versions that satisfy every pack in the tree; kola has no ranges and no solver. If the two disagree, the build stops before reading any source:
error[card §1]: /.../app/kola.pack: `[deps] mid = { version = "0.2.0", .. }` does not match the pack's own `[pack] version = "0.1.0"` (/.../mid/kola.pack) — kola v1 uses literal version equality, not semver ranges
= help: update `[deps] mid`'s version to "0.1.0" in /.../app/kola.pack, or bump the depended-on pack's own version to matchA pack that lists itself, a duplicate key, and a name declared in two dependency sections are also refused.
The caps list in the entry restates the dependency's own [caps] allowed. The audit checks that your list is a superset of what the dependency declares, so a dependency that starts wanting a new capability breaks your build until you grant it in your own manifest. A dependency entry may carry the named lists as well (fs_roots, proc_bins, and so on); a key the compiler does not know is reported.
Transitive dependencies
Dependencies resolve transitively. If app depends on mid and mid depends on leaf, then app declares only mid. It never imports leaf, never calls it, and has no reason to know its name.
The rule that makes this work is about ownership. A use root::... in a module is legal when root is std, the pack that owns the module, or one of the owning pack's own [deps]. So mid may import leaf because mid's manifest says so. If mid forgot to declare it, the error names mid, the only pack that could fix it:
error[card §1]: `use leaf::core` in `mid::calc` (a module of pack `mid`): `leaf` is not `std`, `mid`'s own modules, or one of `mid`'s declared `[deps]` (none declared) — a dependency pack must declare its OWN dependencies; add `leaf = { version = "..." }` under `[deps]` in `mid`'s kola.pack and it resolves transitively from thereVersions are checked at every hop. A cycle (a depends on b, which depends on a) is an error that prints the cycle. A diamond, where two of your dependencies both depend on the same third pack, resolves that pack once.
Dependencies for examples and tests
An example sometimes needs a pack the library does not. A client example may need a server to talk to. Declaring that server under [deps] would make every consumer of your library inherit a dependency the library never uses, so it goes under [deps.examples]. Entries there have the same shape and rules as [deps], resolve only from files under examples/, and are refused from src/.
[deps.tests] does the same for tests. Because a test item lives in a library file and shares its use lines, the scope is enforced by the call graph: every function that calls into a test-only dependency must be a test item or a function that only tests reach. A pub fn, main, or anything a shipping path reaches is a violation:
error[pack budget]: `leaf` is declared under `[deps.tests]`, and `seeded` (tk/src/lib.kl) calls `core::base` while a shipping path reaches it — a test-only dependency may be called only from `test` items and from fns EVERY path to which starts in one ... Move the call into a `test`-only helper, or move `leaf` to `[deps]` if the library really depends on it — that is a claim every consumer inheritsNeither scope is inherited: a consumer of your pack never sees your test or example dependencies.
Where a dependency is found
kola looks for a dependency in four places, in this order: a kola.work workspace file, sibling directories, the local cache in ~/.kola/packs, and finally a registry, a server that stores published packs, over the network.
A kola.work file, found by walking up from the pack, lists working trees under [workspace] use. It is how you edit a dependency and its consumer together. kola validates it on every check, build and audit, even for a pack with no dependencies: an entry that is not a directory, has no readable kola.pack, or declares a name another entry already declares is an error.
error[card §1]: /.../kola.work: `[workspace] use` entry `./nowhere` does not resolve to a directory (/.../nowhere): No such file or directory (os error 2) — delete the entry, or fix the path if the checkout moved
= help: fix the kola.work workspace file, or delete the offending `use` entryA stale workspace file is a resolution rule nobody can predict, so kola refuses to work around it. When a workspace is active and a dependency falls through to the sibling scan instead, the audit prints which dependency it was and the path it resolved to.
The sibling scan looks in the directories next to your pack for a kola.pack whose [pack] name matches the dependency. A scratch file in /tmp cannot import a pack at all; put it inside one.
The audit tree
A supply-chain attack is one that reaches you through code you depend on rather than code you wrote. Most of that risk sits inside dependencies, so kola audit walks the whole resolved tree and prints one block per pack:
$ kola audit app
audit: caps within budget — pack `app` (used=[] ⊆ allowed=["log (effect)"])
audit: dependency tree (1 declared dep(s) of `app`):
dep `mid` (/.../mid):
audit: caps within budget — pack `mid` (used=[] ⊆ allowed=[])
dep `leaf` (/.../leaf):
audit: caps within budget — pack `leaf` (used=[] ⊆ allowed=[])
audit ok: 0 violation(s) — every budget checked in this run is within its declared limitsThe composition follows a few rules:
Each pack is checked against its own files and its own manifest, never merged with an importer's code, and each is visited once even if several paths reach it.
At every edge, the importer's
capslist for a dependency must cover that dependency's own[caps] allowed. A violation names the immediate importer, the pack whose manifest holds the decision, not the root several hops up.Budgets do not add up. A dependency's grant is never pooled where a sibling dependency could draw on it, and there is no "sum of every capability in the tree" check. Without ambient authority, a capability a dependency uses on your behalf can reach your code only through a value or effect in your own signatures, which step 1 already checks.
A dependency's test budget (
[caps.tests]) is settled against that dependency's own manifest. You never run its tests, so you never grant what they use.
If granting leaf the clock in its own manifest is not mirrored by mid, the audit fails at that edge:
error[pack deps budget]: dependency `leaf` (pack `leaf`) declares `[caps] allowed` includes `Clock`, but `mid`'s `[deps] leaf` sub-declaration does not list it — a dependency update that starts wanting a new capability is a build error until the importing pack's manifest explicitly grants itThe audit also warns about a [deps] entry that no module imports, and about a caps grant the dependency no longer asks for. The exit status is nonzero if any pack in the tree fails its own budget or any edge fails the superset check.
Embedded files
A pack can compile static files (HTML, CSS, images) into its binary instead of reading them from disk at run time:
[embed]
files = ["web/index.html", "web/app.css"]use std::embed;
pub fn index_page() -> &heap [u8] !{} {
embed::bytes("web/index.html")
}embed::bytes takes a string literal, resolved at check time against the list (an unlisted or missing file is a compile error). It needs no capability and has no effect. The audit prints each embedded file with its size and SHA-256, and warns about a listed file that no embed::bytes call reads, since it still ships in the archive. (The snippet only checks inside a pack that lists the file.)
The [leaks] budget
[leaks] is optional. Without it, the audit's leak lines are informational. With it, the audit enforces two things over the pack's own sources: candidate leak sites must be acknowledged by key (acknowledged = [...]), and not_analyzed_pct_cap = N caps the percentage of the pack's non-test items that fall outside the verified IR (the subset of code the leak analysis can follow). Dependencies and examples are reported but never budgeted, because you cannot acknowledge a site you did not write. A [leaks] section in a pack where no analyzed function allocates is refused, because such a budget could never fire. Region leaks covers the details.
Archives, locks and the local registry
A manifest and a verdict are only as good as the bytes they describe. A .kpack archive holds the pack tree in a deterministic form: the same tree always produces the same bytes. A SHA-256 hash is a 32-byte fingerprint of the content; changing any byte changes it, and nobody can practically construct different content with the same hash. So the archive's SHA-256 serves as the identity of that pack version.
$ kola pack build widget -o widget.kpack
{"name":"widget","version":"0.1.0","description":"a demo pack","license":"Apache-2.0","files":9,"bytes":14053,"sha256":"f2a8c469...","out":"widget.kpack","source":""}
$ kola pack verify widget.kpack
{"ok":true,"files":9,"sha256":"f2a8c469..."}Before writing anything, kola pack build runs the same gates as a publish: the manifest against the files it describes (the source root exists, entry exists, a shipped LICENSE is named), a clean kola check of sources and examples, the budget audit, and a self-containment lint (no comment or manifest line may point at a document the archive does not ship, and no ../ may leave the pack). kola pack check <dir> runs the same gates without writing an archive, over one pack or a directory of packs, and prints one ok or FAIL line per pack.
The license gate exists because a registry indexes on [pack] license, which holds an SPDX identifier, the standard short name for a license such as MIT or Apache-2.0, rather than parsing the text. A pack that ships a LICENSE file without naming the license would show up as unlicensed while carrying one:
error[pack license]: lic/kola.pack: this pack ships lic/LICENSE — `kola pack build` puts it in the archive — but `[pack] license` is absent, so the manifest does not say which licence that text IS. ... Add `license = "<SPDX id>"` under `[pack]` (e.g. `license = "Apache-2.0"`), or delete LICENSE if this pack really carries no licence (design-packages.md §4.1). The SPDX id is NOT compared against the file's text.The reverse is allowed: a manifest may name a license with no file beside it.
kola pack lock <dir> writes kola.lock, a lock file that records the exact version and content hash of each dependency, so that later builds can verify they use the same bytes. From then on, kola audit fails the moment a dependency's content changes:
error[pack lock]: dependency `mid` resolved to content hash 03d0393d... but kola.lock pins 84cbd16b... — drift (re-run `kola pack lock` if the bump is intended)The hash covers content, not releases, so any edit to a dependency (a comment included) invalidates the lock; you regenerate it rather than edit it. A pack with [deps] and no lock draws warning[pack lock] on every audit. Two limits are worth keeping in mind. Only kola audit enforces the lock; check, test and build succeed on a drifted dependency. And the audit enforces the root pack's lock only: a dependency's own lock is checked when you audit that dependency.
The local registry is a directory, ~/.kola/registry by default (override it with $KOLA_REGISTRY, or name more registries in ~/.kola/settings.toml, one [registry.<name>] table each):
kola pack push widget local # publish, after the same gates as build
kola pack fetch widget@0.1.0 local # unpack into ~/.kola/packs, hash-verifiedA published version is immutable. Publishing identical bytes again does nothing; publishing different bytes under the same version is refused. kola pack sync <dir> fetches a whole dependency tree into the cache in one pass, and kola pack outdated <dir> lists direct dependencies with newer versions upstream.
Remote registries and signatures
The production registry at pack.kola.works accepts signed publishes over HTTPS. Publishing is signed: the publisher's private key produces a signature over the archive, and anyone holding the matching public key can check that this publisher released these exact bytes. You log in with kola pack login, create an ed25519 signing key with kola pack key generate (stored locally, or with --backend conclave inside a Conclave Transit vault so the private key never leaves it), and kola pack push <dir> [registry] [--key <name>] signs on the way out.
Checking that downloaded bytes match the hash the registry supplied proves only that the transfer worked, since the registry chose both sides of the comparison. So a fetch also verifies the publisher: the first time kola sees a pack name it records the publisher's key, and later fetches must be signed by it.
You rarely run fetch by hand. When check, run, test or build needs a dependency that no workspace, sibling or cache entry supplies, and the dependency has a pin (a kola.lock entry or an exact [deps] version the registry can hash), kola fetches it into the cache. A downloaded archive that fails its hash is an error naming both hashes. A dependency with no pin never touches the network; the error tells you to run kola pack lock. --offline (or KOLA_OFFLINE=1) forbids all dependency downloads, for hermetic builds. kola security check <dir> compares the resolved tree against the registry's published advisories.
What packs do not have
Some things are missing on purpose:
Binary dependencies. kola-to-kola dependencies are always source. Contracts, effects and linearity are checked from source, and a binary pack could only be trusted, never re-checked. There is no
unsafeorexternin the language to escape through either.Version ranges. Versions match exactly, at every hop, so there is nothing to solve.
Install-time code. A pack is inert data until it is compiled. There are no build scripts and no install hooks.
All the rules on this page fail closed. A missing [caps] section is an empty budget, not an unlimited one. A manifest the parser cannot read unambiguously is an error, not a best guess. And enforcement runs on the types and effect rows the checker proved, not on a text search that a differently formatted line could slip past.
The design reasoning behind the audit tree is in docs/design-packages.md §3 in the repository; kola help kpack is the command-line reference.