glia cache push, pull and gc: a signed parse cache over a directory or HTTPS object store, whole-layout objects, trust modes and the CI recipe.
A build keeps the parse of every source file in <repo>/.glia/graph/parse_cache.bin
and reparses only the files whose content changed. A fresh checkout (a new clone,
a CI runner, a second worktree) starts with no sidecar and parses everything.
The shared cache lets such a checkout fetch the parses another machine already
made for the same bytes.
glia cache push <REPO> <STORE> [--unsigned] [--key-file <FILE>] [--layout] [--json]
glia cache pull <REPO> <STORE> [--unsigned] [--key-file <FILE>] [--verify <N|all>] [--jobs <N>] [--layout] [--json]
glia cache gc <STORE> [--keep-stamps <N>] [--max-bytes <BYTES>] [--json]
<STORE> is a directory, or an https:// URL (see HTTPS store);
gc takes a directory only.
--layout also moves the whole finished layout of a clean checkout (see
Whole-layout objects), so a fresh clone of a commit CI
already built skips the build entirely, not just the parse.
The build stays offline. glia build never reads a store. pull is a separate step:
it writes the sidecar, and the next build reuses it through the same checks it
applies to its own cache (build stamp, repo identity, go.mod module set, content
hash, MODULE form). The engine and the glia-py wheel contain no transport. They
only produce keys and payloads and run the verified import
(glia_engine::shared_cache). Moving and authenticating bytes is done by the
glia binary alone, and it is the only glia artefact that links an HTTP
client.
Each cached parse is stored under a 32-byte blake3 content address
(shared_cache::file_key). The address hashes every input the parse depends on,
and nothing else:
<release>+p<parser stamp>), which moves with any parser or
extractor change;Two checkouts share an object only when all of these match. A checkout with different content, another identity or another release asks for different keys. The key is an address, not a secret and not a signature.
<store>/v1/<stamp>/LAST mtime = the stamp's last upload (gc's recency)
<store>/v1/<stamp>/<aa>/<key>.gpc one object per content address; <aa> = the key's first two hex chars
<store>/v1/<stamp>/<aa>/<key>.gpc.<pid>.tmp a push's staging file, renamed into place
Each stamp gets its own directory, so gc can drop a whole release at once. An object is:
b"GLIAPC01" | flags u8 (bit 0 = signed) | key [32] | payload_len u64 LE | payload | mac [32] (signed only)
The payload is the sidecar entry's own bytes: the file's content hash, its
language and its parse, in canonical order. The MAC is
blake3::keyed_hash(signing key, every byte before the MAC).
A store holds objects written by other machines, so pull trusts nothing it can
check.
Keyed store (the default). Every object carries a MAC under a 32-byte key
shared by the machines allowed to push. The key comes from --key-file <FILE>
(64 hex characters, surrounding whitespace ignored) or GLIA_CACHE_KEY. A pull
refuses an object that:
This defends against a store that anyone else can write to: a forged, altered,
swapped or replayed-under-another-key object is refused before import. The
engine's import then checks every payload against the checkout (decodes it
within bounds; checks content hash, language and MODULE form) and writes only
what passes. With a key the default re-parse sample is 0, because the MAC
already authenticates the writer. --verify N|all adds a sample anyway. A key
holder can still push a wrong parse. The key is the trust boundary.
Unsigned store (--unsigned). Objects are written without a MAC and read
without checking one. Use this only for a store no one else can write to (a
directory on your own machine). The import's checks still run, and a pull
re-parses a random sample of the accepted entries with the build's own parse
code (default 32, --verify N|all) and compares the bytes. One mismatch exits 1,
and nothing is written. A sample catches bulk poisoning with high probability.
It catches a single targeted entry only with probability sample / files, so
--verify all is the full check.
Without a key, push and pull need --unsigned explicitly and otherwise exit 2
with no cache key: set GLIA_CACHE_KEY or pass --key-file (or --unsigned to
trust the store as-is). When a key is set, it takes precedence over --unsigned:
objects are signed and checked, and one note line says so.
Key hygiene: the key is never printed, and no error message contains it. On
unix, a key file that other users can read triggers a warning (chmod 600 it).
Generate a key with, for example, openssl rand -hex 32 > ~/.config/glia/cache.key.
A directory store works on a local disk or on any shared filesystem mount
(NFS, SMB). A push stages each object as <file>.<pid>.tmp and renames it into
place, so a reader never sees a partly written object. Two pushers writing the
same key write identical bytes, because the payload is canonical.
export GLIA_CACHE_KEY=$(cat ~/.config/glia/cache.key)
# on a machine that has built the repo
glia build ~/src/shop
glia cache push ~/src/shop /mnt/team/glia-cache
# on a fresh checkout of the same commit
glia cache pull ~/work/shop /mnt/team/glia-cache --jobs 16
glia build ~/work/shop # [incremental] shop: reused N, reparsed 0, evicted 0
push uploads only the entries that a build of the checkout as it is now would
reuse. Stale entries are counted (stale=) and skipped, and objects already in
the store are counted as present=. pull fetches only the files the
checkout's cache lacks (local_hits= counts the rest). It runs --jobs fetches
in parallel (default 8), and the result does not depend on how they are
scheduled.
Exit codes: 0 ok; 1 a store or verification failure (a failed pull writes
nothing); 2 a usage error (a bad store, no key or a bad key, a repo that is not
a directory). --json prints the summary as one JSON object.
Markers (stderr), after the engine's [cache] export / [cache] import lines:
[cache] push store=<dir|url> repo=<label> entries=<n> uploaded=<u> present=<p> stale=<s> signed=<yes|no>
[cache] pull store=<dir|url> repo=<label> files=<n> local_hits=<h> fetched=<f> missing=<m> rejected=<r> verified=<v> signed=<yes|no>
[cache] rejected <v1/...>: <reason> (first 20 per pull)
[cache] gc store=<dir> stamps_kept=<k> stamps_removed=<r> objects_removed=<o> bytes=<before>-><after>
In the pull marker, rejected covers both transport rejections (size, format,
key, MAC) and the import's own rejections.
glia cache gc <STORE> prunes a directory store:
LAST file (a push
touches it on every upload; a stamp without LAST uses the directory's
mtime; ties are broken by name). It keeps the newest --keep-stamps
(default 2: this release and the previous one) and removes the rest whole.
A release never reads another stamp's objects.--max-bytes <BYTES> (a count, or with a K / M / G suffix in
powers of 1024), it deletes the kept objects oldest first (by mtime, then by
path) until the total is at most the budget.*.tmp) older than an hour, left by a crashed
push. A newer one belongs to a push still in progress and is left alone.Only .gpc objects count toward the byte totals. Whole-layout parts
(layout/*.gla) go with their stamp and are outside --max-bytes.
Sharing parses saves only the parse phase: the walk, the per-language graph
builds, the 15 resolvers and the post-passes still run. A clean checkout at a
commit another machine already built can skip all of them. push --layout
uploads the finished .glia/graph/ layout, and pull --layout installs it
before the per-file pull runs (so a later edit still starts warm).
# CI, after building the commit
glia build "$REPO" && glia cache push "$REPO" /mnt/team/glia-cache --layout
# a fresh clone of the same commit
glia cache pull ~/work/shop /mnt/team/glia-cache --layout
# [cache] layout pull repo=shop key=<12 hex> tree=<12 hex> result=hit
Clean work trees only. A layout is keyed, pushed and installed only when
git status (tracked and untracked files, .glia excluded) prints nothing, so
the tracked files are exactly HEAD^{tree}. Otherwise the result is dirty and
the step is skipped; the per-file push or pull still runs.
What the key covers. A blake3 derive_key("glia layout object v1") over:
HEAD tree id;.glia input fingerprint (the map the manifest records: overlay, docs,
history and test snapshots, by content);overlay=on (the default layout dir always holds the overlay-applied graph);target/, an empty dist/, a
checked-out submodule), the project roots, and every compile_commands.json
in the repo root, a project root or a child directory of either (the C/C++
build reads it from gitignored build dirs). A source or doc file the walk
reads but git does not track (hidden from git status by .git/info/exclude
or a global excludes file) makes the checkout dirty.Not covered: other reads of gitignored files outside the walk (a tsconfig
extends resolved into node_modules). Submodule content is not walked.
What travels. manifest.json and every file it names (the shards,
cross_stack.gmap, foreign shards), each checked against its manifest hash
when exported. The parse cache travels per file, the timeline sidecar names
commits rather than a tree and stays home, and .gitignore is written by the
install. push refuses (result=stale) a layout that is missing, older than
the checkout (is_gmap_stale), mid-write, or not this repo's alone (a
multi-repo or merged layout).
Install. The engine re-derives the key from the checkout and refuses
another one (key mismatch), accepts only plain manifest.json / *.gmap
names the manifest names (no path can leave the layout dir), and rewrites the
manifest's repos to what a local build writes: this checkout's label (its
directory name, which differs between machines), its root relative to the
layout dir, and its HEAD commit. The files are staged in
.glia/graph.pull.<pid>.tmp/ and moved in shards first and manifest.json
last; every file they replace or orphan is kept aside there, so the disk holds
two layouts until the check. The check is a full load with rebuilding off
(load_or_rebuild(dir, repo, false)): build stamp, .glia inputs, source
mtimes, every shard's hash, every CODE span. If it fails, the new files are
removed, the old ones restored byte for byte, and the result is rejected.
Object. The layout is one body split into parts, each an ordinary signed cache object, so a layout larger than the 64 MiB object bound still moves:
body = b"GLIALY01" | entry_count u32 LE | per entry: name_len u16 LE | name | data_len u64 LE | data
part i = part_count u32 LE | body_len u64 LE | the i-th 48 MiB of the body,
stored as the object of key blake3 derive_key("glia layout object part v1", key | i u32 LE)
store = <store>/v1/<stamp>/layout/<key>.gla (part 0), <key>.<i>.gla (i >= 1)
A part's key binds it to its layout and index, so the MAC refuses a part
served for another layout or index; every part repeats the part count and body
length, so a dropped or truncated part is refused; a push writes part 0 last.
Caps are checked before any length is used: at most 4096 files, 128-byte
names, 512 MiB per file and 1 GiB in all. Without a key, --layout needs
--unsigned like any pull, and then the store is trusted as-is: there is no
re-parse sample for a layout, only the install's checks.
Markers (stderr), before the per-file line:
[cache] layout push repo=<label> key=<12 hex|-> tree=<12 hex|-> result=<pushed|present|stale|dirty>
[cache] layout pull repo=<label> key=<12 hex|-> tree=<12 hex|-> result=<hit|miss|rejected|dirty>
[cache] rejected v1/<stamp>/layout/<key>.gla: <reason>
--json adds a layout object (result, key, tree, reason, files,
bytes, parts). A layout that is stale, dirty, miss or rejected is not
a failure: the exit code is the per-file step's. A store error, or an install
that could not put the previous layout back, exits 1. The two steps are
independent: a layout installed before a per-file pull fails stays installed
(it was verified on its own).
Any HTTP server that serves files on GET / HEAD and stores a request body
on PUT is a store: nginx with the WebDAV module's PUT, a static file server
with an upload handler, or an S3 bucket behind a proxy that adds the bucket
credentials. The object at v1/<stamp>/<aa>/<key>.gpc (or a layout part,
v1/<stamp>/layout/<key>[.<i>].gla) is <url>/<that path>, the same names a
directory store uses.
glia cache push <REPO> https://cache.example/glia
glia cache pull <REPO> https://cache.example/glia --jobs 16
| Operation | Request | Answer |
|---|---|---|
| does the store hold it (push) | HEAD <url>/<path> |
200 yes, 404 no |
| upload (push) | PUT <url>/<path>, Content-Type: application/octet-stream |
200, 201 or 204 |
| fetch (pull) | GET <url>/<path> |
200 the object, 404 missing |
Any other status, a redirect (3xx, never followed) or a connection failure is a store failure: the command exits 1, and a failed pull writes nothing. Every object is checked exactly as from a directory: the body is read at most one byte past the 64 MiB bound, and a server answering 200 with an error page is refused as malformed, never parsed.
Authentication. GLIA_CACHE_TOKEN, when set, is sent as
Authorization: Bearer <token> on every request (printable ASCII, no spaces;
anything else is a usage error). Every request also carries
User-Agent: glia/<release>. The token is never printed. Credentials in the
URL (https://user:pass@host) are refused; so are a query string and a
fragment. The token decides who may read and write the store; the cache key
(GLIA_CACHE_KEY) still decides which objects a pull trusts, whoever wrote
them.
Plain http. http:// is accepted only for a loopback host (127.0.0.1,
localhost, [::1], with an optional port), such as a local server or a test.
Any other host exits 2 with refusing plain http:// to non-loopback host <host>
before any request is made, because the token and the objects would travel in
cleartext. Any other scheme (s3://, ftp://) exits 2 with
unsupported store <scheme>://.
Timeouts: 10 s to connect, 60 s per read or write. --jobs pull workers share
one connection pool. A slow store makes pull slow; it never slows a build,
which never reads a store.
GC. glia cache gc prunes a directory only; a URL exits 2 (gc a remote
store on the server). On the server, prune the same way gc does: drop whole
v1/<stamp>/ directories of old releases (a release never reads another
stamp's objects), or expire objects by age.
Marker (stderr), last, once per push or pull through an HTTP store, whether it succeeded or not. It names the scheme and host only, never the path or the token:
[cache] store=<scheme>://<host> transport=<https|http-loopback> requests=<n> (get=<g> head=<h> put=<p>) status_4xx=<a> status_5xx=<b>
A push's HEAD of an object the store lacks counts as a 4xx.
CI builds each commit and pushes; developers and other jobs pull. Store
GLIA_CACHE_KEY (64 hex characters) and GLIA_CACHE_TOKEN as CI secrets.
# CI, after checkout (GLIA_CACHE_KEY and GLIA_CACHE_TOKEN from secrets)
glia build . && glia cache push . https://cache.example/glia
# a developer or a later job, with the same key and a token that can read
glia cache pull . https://cache.example/glia
glia build . # [incremental] <repo>: reused N, reparsed 0, evicted 0
Add --layout to both commands to move the whole layout of a clean checkout
too (see Whole-layout objects). A token that can only
read belongs on developer machines; the one that can PUT stays in CI.