+--------------------------------------------+
| glacier-bootstrap                          |
+--------------------------------------------+

STATUS: LOCAL MODE IMPLEMENTED 2026-10-06 (glacier-ng/glacier-bootstrap,
tests/bootstrap_alpine.sh; see section 10 for how it differs from the
plan below). Remote mode (-r) and the pinning helper are still a plan.
Section 9 lists the decisions still open.

glacier-bootstrap takes a template index and builds a chrootable
Everest system in a target directory, from either a local directory of
packages or a remote repository. It is the "glacier-bootstrap
TEMPLATE_INDEX TARGET" tool named in SECURITY_DESIGN.txt section 10, and
the `syspkg --root` step of IMAGE_DESIGN.txt's pipeline.

Companion documents: IMAGE_DESIGN.txt (why images are bootstrap OUTPUT,
the sysroot library change), SECURITY_DESIGN.txt (refusal matrix,
syspkg's privilege model), PACKAGE_SCOPES.txt (link routing).


+--------------------------------------------+
| 1. What it does                            |
+--------------------------------------------+

    glacier-bootstrap [-n] [-t] (-d PKGDIR | -r URL) TEMPLATE TARGET

    -d PKGDIR   packages come from a local directory (gbuild/gworld
                output: NAME-VER.gpkg plus NAME-VER.gpkg.sha256)
    -r URL      packages come from a remote repository (section 5)
    -n          dry run: parse, resolve, fetch, verify, print the plan;
                never touch TARGET
    -t          after installing, smoke-test: chroot TARGET /bin/sh -c true

Sequence:

    1. parse TEMPLATE                                       (section 3)
    2. resolve: check the package list is complete          (section 4)
    3. fetch into a private cache; verify every hash        (section 5)
    4. doas syspkg --root TARGET -n                         create the tree
    5. doas syspkg --root TARGET --strict -l PKG...         ONE transaction
    6. doas syspkg --root TARGET -y                        TARGET's indexes
    7. optional smoke test (-t)

Step 5 is one transaction on purpose: TARGET ends up either the whole
system or an empty tree, never half a system.


+--------------------------------------------+
| 2. Trust and privilege                     |
+--------------------------------------------+

glacier-bootstrap NEVER WRITES INTO TARGET ITSELF. Every byte under
TARGET goes through `doas syspkg --root TARGET`.

  - SECURITY_DESIGN.txt section 5: syspkg owns ALL system-scope
    mutation, "one tool to secure, one tool to audit." A bootstrapper
    that extracted packages itself would be the second, unaudited entry
    point that section warns about.
  - glacier-bootstrap runs UNPRIVILEGED. Parsing, resolving, fetching and
    verifying need no root, so it needs no third copy of the hardened
    root-loading preamble (syspkg and gstore already carry it; CLAUDE.md
    flags the duplication). Root is borrowed only for the two syspkg
    calls.
  - Its own verification is early-fail, for good error messages. syspkg's
    is authoritative: syspkg re-hashes every file at the moment it
    reads it (refusal matrix, SECURITY_DESIGN section 4). So a file
    swapped in the cache between bootstrap's check and syspkg's read is
    caught. The cache is the invoking user's (0700). The only party who
    could swap files there is the person who can already run doas
    syspkg.

Where trust comes from, per source:

  local (-d)       The sidecar next to each .gpkg, same as syspkg today.
                   INTEGRITY ONLY, which is all the current tree offers
                   anywhere.

  remote, pinned   The TEMPLATE pins each package's sha256 (section 3).
                   glacier-bootstrap writes the sidecar FROM THE PIN, not
                   from anything the server sent. So syspkg's existing
                   "sidecar matches file" check enforces the template's
                   hash, with no syspkg change needed. The server is an
                   untrusted blob store. Authenticity is exactly as good
                   as the template file's, which you hold locally and can
                   review or diff.

  remote, signed   Later: a signed repo index (the agreed next major item
                   after the checkpoints). Only then may a template name a
                   remote package without a pin.

  remote, unpinned and unsigned: REFUSED. A .gpkg and its .sha256
  fetched from the same server prove nothing (SECURITY_DESIGN section 1:
  the hash travelling with the file).


+--------------------------------------------+
| 3. Template index format                   |
+--------------------------------------------+

Extends IMAGE_DESIGN.txt section 5's KEY=value proposal with one line
per package, so packages can carry pins:

    # templates/minimal.template
    NAME=minimal
    PKG=filesystem 1.0.0
    PKG=musl 1.2.5 sha256:3a5f...
    PKG=busybox 1.37.0 sha256:91c0...

  - Exact versions only, no ranges. A template describes ONE system,
    reproducibly.
  - A pin is optional for -d (if present, it must ALSO match the
    sidecar) and required for -r until signed repos exist.
  - Strict parsing: unknown keys, a duplicate PKG name, a malformed pin
    and an empty package list are all hard errors. SYS_PROFILE is an
    unknown key (9b): nothing could check it until manifests record a
    profile (ROADMAP 3.3). Remember gworld bug
    10: a field silently never parsing, with a default that looks right.
  - A `glacier-bootstrap --pin PKGDIR TEMPLATE` helper that fills pins in
    from local sidecars is the obvious way to produce a pinned template
    from a gworld build. Pinning then becomes a review step, not
    hand-copying hex.


+--------------------------------------------+
| 4. Resolution                              |
+--------------------------------------------+

Manifests already carry PKG_DEPS and PKG_CONFS (name + version), and
libglacier parses both into gl_pkg_t. Nothing consumes them today, and
every existing package leaves them empty. (IMAGE_DESIGN.txt section 5
says glacier has no runtime deps at all. The format exists; the use
doesn't.)

Recommendation: CHECK, don't auto-resolve.

  - Every PKG_DEPS entry of every listed package must itself be listed,
    at that version or later (decided 2026-10-06: the ROADMAP 2.3 rule, so
    the template check and syspkg's commit check agree). Otherwise error:
    "base::busybox requires musl >= 1.2.5, which the template does not
    list." / "..., but the template lists 1.2.4."
  - No two listed packages may conflict (PKG_CONFS).
  - Each manifest's PKG_NAME/PKG_VER must match its template line. This
    catches a mis-named file in -d mode, where there may be no pin.

Why not pull dependencies in automatically: anything auto-added would be
an unpinned package the template's author never reviewed, which defeats
the trust model in section 2. Closure resolution can come with signed
repos.

SYS_PROFILE can't be checked against packages yet: manifests don't
record which profile they were built under (open item in CLAUDE.md).
Once they do, a mismatched package is a resolution error.


+--------------------------------------------+
| 5. Package sources                         |
+--------------------------------------------+

LOCAL (-d PKGDIR): a flat directory, exactly what gbuild emits —
NAME-VER.gpkg, NAME-VER.gpkg.sha256. No index file. The template's
package list says what to take from the directory.

REMOTE (-r URL): the same flat layout over HTTPS:

    URL/NAME-VER.gpkg

  - Fetch by shelling out to curl (`--proto =https --fail`), the same
    way gbuild shells out to git and tar, into
    $XDG_CACHE_HOME/glacier-bootstrap/ (0700) under a temporary name.
    Hash it, and only then rename it into place.
  - Server-provided .sha256 files are never consulted (section 2).
  - Cache hits are re-hashed against the pin on every run. The cache is
    a download optimization, not a trusted store.
  - Later, with signing: URL/repo.idx plus its signature, listing name,
    version, repo and hash. The fetch path stays the same.


+--------------------------------------------+
| 6. What "chrootable" requires              |
+--------------------------------------------+

Found while planning, by reading the code and the packages in pkgs/.
Each one is a hard blocker for `chroot TARGET /bin/sh` working at all,
independent of glacier-bootstrap itself:

  a. [FIXED 2026-10-04 — extraction + linking, with both defenses
     below; tests/test_package_symlinks.lua] SYMLINKS IN PACKAGES ARE
     DROPPED. The extraction loop in
     gl_install_pkg handles only AE_IFDIR and AE_IFREG (istoreutils.c);
     symlink entries never reach the store. gl_link_pkg likewise links
     only regular files. Real packages need symlinks:
       - musl ships lib/ld-musl-x86_64.so.1 -> /usr/lib/libc.so. That is
         the dynamic loader every musl binary asks the kernel for. It is
         currently never installed; Alpine-based tests passed only
         because Alpine provides its own.
       - shared-library soname links (libz.so.1 -> libz.so.1.3.1);
       - busybox applets.
     Fix: extract symlink entries verbatim into the store, and link
     them like regular files. SECURITY TRAP to design around: an archive
     shipping `x -> /etc` followed by `x/passwd` writes through the
     symlink, as root, into the real /etc (classic tar-slip via
     symlink). Extraction must never write through a symlink: create
     symlinks only after every regular file, and/or refuse any entry
     whose parent path is a symlink in the store.

  b. [DONE 2026-10-04 — gl_create_fs_tree, run by syspkg -n; takes a
     root prefix ready for --root; tests/test_fs_tree.lua] NO TOP-LEVEL
     TREE. Base packages link only under /usr
     (PACKAGE_SCOPES section 3), and a leading "usr/" is the only prefix
     stripped. So musl's lib/ld-musl... would land at /usr/lib, while
     PT_INTERP says /lib/ld-musl-x86_64.so.1. TARGET needs merged /usr:
         /bin -> usr/bin   /lib -> usr/lib   /sbin -> usr/bin
     plus the mountpoints and dirs nothing ships: /dev /proc /sys /tmp
     (1777) /run /var /root /home /etc. No package can provide these
     under the current link rules (everything base goes under /usr).
     Recommendation: `syspkg -n` creates this skeleton as part of
     system init (under --root, and on a real system). Creating the
     system's skeleton is system-scope mutation, so it belongs to
     syspkg, not to the bootstrapper.

  c. [DONE 2026-10-04 — decision 9c, option 1; tests/test_config_files.lua]
     /etc HAS NO STORY. A base package's etc/passwd would link to
     /usr/etc/passwd. And config files can't simply be symlinks into
     the store anyway: editing them would modify the store and break
     its hashes. This needs its own decision (section 9c). For the first
     chroot milestone, the skeleton step can write a minimal
     /etc/passwd and /etc/group (root only), and /etc/profile.

  d. NO SHELL. The only packages that exist are musl, kilo and sbase,
     and sbase is coreutils with no sh. A busybox (or mksh/dash) recipe
     in base is needed.

  e. REPO TIERS. sbase's manifest says PKG_REPO = "core", which is not
     a tier (base / extra / community). Under system scope it would link
     into /glacier/sys/links, not /usr/bin. Rebuild it as base.

  f. [sidecars generated for the existing archives 2026-10-04] NO
     SIDECARS. None of pkgs/*.gpkg has a .sha256. They were built
     before checkpoint 2. Rebuild them with the current gbuild.


+--------------------------------------------+
| 7. Prerequisites, in order                 |
+--------------------------------------------+

  1. [DONE] Hash verification at install time (SECURITY_DESIGN
     section 4). Pins mean nothing until syspkg actually checks sidecars.
  2. [DONE] Symlink extraction + linking, with the write-through-symlink
     defense (6a). Needed for anything to run.
  3. [DONE 2026-10-05] sysroot in the library (IMAGE_DESIGN section 3), plus the "host
     /usr and /glacier untouched" test.
  4. [DONE 2026-10-05, as -R/--root] syspkg --root DIR (IMAGE_DESIGN section 4), --strict (any link
     refusal fails the transaction), and the skeleton on -n (6b) [the
     skeleton is DONE; --root and --strict are not].
  5. [DONE: ~/Projects/everest builds musl, busybox, linux and
     everest-bootscripts, all base, with sidecars.] Packages: busybox
     recipe (base), sbase rebuilt as base, every
     package rebuilt with sidecars (6d-f).
  6. [DONE 2026-10-06: Everest's `everest.sh initramfs` now builds its
     tree this way, and the result boots in QEMU.] glacier-bootstrap,
     local mode.
     MILESTONE: `glacier-bootstrap -t -d pkgs templates/minimal.template
     /mnt/everest` succeeds, `chroot /mnt/everest /bin/sh` gives a
     working shell, and the host is byte-identical before and after.
  7. Remote mode with pinned templates, plus the --pin helper.
  8. Signed repo index (separate design pass), then unpinned remote.

1 and 2 are independent and can go in either order. 3 and 4 are the
bulk of the library/syspkg work. 6 itself is small: it's mostly
orchestration over things that have to exist anyway.


+--------------------------------------------+
| 8. Implementation shape                    |
+--------------------------------------------+

  - LuaJIT, in glacier-ng, alongside gworld. It shares gworld's
    KEY=value parsing (factor that into lib/ rather than copy it), and
    reads manifests through libglacier's existing parser via FFI, so
    PKG_DEPS etc. parse exactly the way syspkg will see them.
  - Unprivileged, so the CWD-relative require convention is fine here.
  - Shells out to: curl (remote), doas syspkg, chroot (for -t, via doas).
  - Exit codes distinguish parse / resolve / fetch / verify / install
    failure, so an image build script can tell them apart.


+--------------------------------------------+
| 9. Open decisions                          |
+--------------------------------------------+

  a. IMAGE_DESIGN.txt section 9a still stands: images are bootstrap
     OUTPUT, never install INPUT. Everything here assumes yes.
  b. DECIDED 2026-10-06: NAME= plus one `PKG=name version [sha256:hex]`
     per package; no SYS_PROFILE until manifests record one (section 3).
  c. DECIDED 2026-10-04: option 1, copy on install, .new on conflict.
     Implemented in istoreutils.c (install_config / prune_config): a
     system-scope package's top-level etc/ is copied into /etc, never
     linked. An admin-modified, admin-deleted or not-glacier-installed
     file is never overwritten; package changes to it are offered once
     as FILE.new. Unmodified files are updated in place and removed with
     their package. User scope still links etc/ as before (open).
  d. DECIDED 2026-10-04: syspkg -n creates the top-level tree itself
     (6b). Implemented as gl_create_fs_tree(root, verbose), called by
     gl_init_sys before the index is written.
  e. DECIDED 2026-10-05: automatic closure, once there is a source to
     resolve from (remote repos, ROADMAP 3.1). Until then a missing
     dependency refuses; the resolver is written so 3.1 plugs in. The
     index already records auto vs explicit installs (ROADMAP 2.3).
     Section 4's template check still applies to bootstrap: a template
     lists what it installs.
  f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld
     (which already knows what it just built).


+--------------------------------------------+
| 10. As implemented (local mode, 2026-10-06)|
+--------------------------------------------+

    glacier-bootstrap [-hvntV] -d PKGDIR TEMPLATE TARGET

Where it differs from, or adds to, sections 1-8:

  - syspkg is the one on the caller's PATH (decided 2026-10-06),
    resolved to an absolute path first and run through doas, or
    directly when already root. -t runs `doas chroot`.
  - Order: parse, then verify, then resolve. Manifests are read only
    from VERIFIED copies: each PKGDIR/NAME-VER.gpkg is copied into a
    private mktemp -d while being hashed, checked against its sidecar and
    the pin, and given a sidecar written from that hash. syspkg is handed
    the copies, so a pin binds syspkg too, and a file swapped in PKGDIR
    later is never read. The copies are removed on every exit.
  - TARGET must be absolute, not /, and pass gl_check_sysroot (checked
    read-only up front). It must be fresh, or initialised by syspkg -n
    with nothing installed: then -n is skipped, so a run declined at
    syspkg's prompt can simply be repeated. TARGET with packages is
    refused.
  - syspkg's prompt is passed through (it's the review step). It exits 0
    when declined, so afterwards TARGET's index is read (it's world-
    readable) and every template package must be listed.
  - Every problem in a phase is reported before failing. Exit status:
    1 usage or target, 2 template, 3 verification, 4 resolution,
    5 installation, 6 smoke test, 7 repositories.
  - Repositories (2026-10-07; the user: bootstrap must leave a
    semi-functional system, so it configures repositories, pacstrap
    style). The configuration comes from a PACKAGE in the template
    (Everest: everest-repos, shipping /etc/glacier/repos.conf and
    keys/*.pub), so bootstrap still never writes into TARGET. After the
    install check, if TARGET's repos.conf gives any repository a url,
    it runs `syspkg -R TARGET -y`: syspkg reads TARGET's OWN keyring
    under -R for repository operations (REPO_DESIGN section 6, "Keyrings
    under -R"), fetches into TARGET/glacier/sys/repos, and so sets
    TARGET's rollback floor at build time. A failed fetch: exit 7,
    TARGET stays installed, no "Bootstrapped" line. No repos.conf, or
    no url: `[!] No remote repositories are configured in TARGET.` and
    exit 0. Copying the host's repos.conf (pacstrap's mirrorlist copy)
    is left out for now (user, 2026-10-07).
  - Its own lib/ is found from its realpath (package.path and ffi.load),
    not the current directory; no root-ownership checks (unprivileged).
  - Not implemented: -r (remote) and --pin (9f still open).
