sigildocs

(sigil env lock)

(sigil env lock) - Evaluation-free env.lock parsing and reification plans.

This module is intentionally a leaf over core data/filesystem/store APIs. It must never import the environment authoring engine, resolver, evaluator, channel, registry, or authoring-library loader.

Exports

Construct a locked-package struct.

Test if a value is a locked-package struct.

Get the name field of a locked-package struct.

Get the version field of a locked-package struct.

Get the recipe field of a locked-package struct.

Get the output field of a locked-package struct.

Get the package-name field of a locked-package struct.

Get the package-identity field of a locked-package struct.

Get the package-identity field of a locked-package struct.

Construct a locked-authoring-library struct.

Test if a value is a locked-authoring-library struct.

Get the name field of a locked-authoring-library struct.

Get the version field of a locked-authoring-library struct.

Get the source field of a locked-authoring-library struct.

Get the source field of a locked-authoring-library struct.

Construct a locked-authoring-channel struct.

Test if a value is a locked-authoring-channel struct.

Get the name field of a locked-authoring-channel struct.

Get the origin field of a locked-authoring-channel struct.

Get the requested-ref field of a locked-authoring-channel struct.

Get the revision field of a locked-authoring-channel struct.

Get the source field of a locked-authoring-channel struct.

Get the snapshot-path field of a locked-authoring-channel struct.

Get the snapshot field of a locked-authoring-channel struct.

Get the catalogue field of a locked-authoring-channel struct.

Get the catalogue field of a locked-authoring-channel struct.

Construct a locked-authoring-load-path struct.

Test if a value is a locked-authoring-load-path struct.

Get the path field of a locked-authoring-load-path struct.

Get the source field of a locked-authoring-load-path struct.

Get the source field of a locked-authoring-load-path struct.

Construct a locked-sealed-recipe struct.

Test if a value is a locked-sealed-recipe struct.

Get the name field of a locked-sealed-recipe struct.

Get the recipe field of a locked-sealed-recipe struct.

Get the bytes field of a locked-sealed-recipe struct.

Get the derived-from field of a locked-sealed-recipe struct.

Get the provenance field of a locked-sealed-recipe struct.

Get the provenance field of a locked-sealed-recipe struct.

Construct a locked-runtime-member struct.

Test if a value is a locked-runtime-member struct.

Get the name field of a locked-runtime-member struct.

Get the version field of a locked-runtime-member struct.

Get the output field of a locked-runtime-member struct.

Get the search-paths field of a locked-runtime-member struct.

Get the search-paths field of a locked-runtime-member struct.

Construct a reification-plan struct.

Test if a value is a reification-plan struct.

Get the version field of a reification-plan struct.

Get the snapshot field of a reification-plan struct.

Get the toolchain-id field of a reification-plan struct.

Get the toolchain-version field of a reification-plan struct.

Get the toolchain-output field of a reification-plan struct.

Get the packages field of a reification-plan struct.

Get the closure field of a reification-plan struct.

Get the runtime field of a reification-plan struct.

Get the base-search-paths field of a reification-plan struct.

Get the system field of a reification-plan struct.

Get the roots field of a reification-plan struct.

Get the priority field of a reification-plan struct.

Get the authoring-provenance field of a reification-plan struct.

Get the authoring-channels field of a reification-plan struct.

Get the authoring-load-paths field of a reification-plan struct.

Get the authoring-libraries field of a reification-plan struct.

Get the sealed-recipes field of a reification-plan struct.

A (VAR SUBDIR) export pair, shared by base-search-paths and by a runtime member's own row. SUBDIR is joined onto a store path, so it must be relative and must not climb out -- the same shape-check discipline as valid-profile-name?, for the same reason: a lock is a user-editable file and this value becomes a filesystem path.

THE VARIABLE NAME IS VALIDATED HERE BECAUSE THE EMITTER CANNOT DEFEND ITSELF. print-exports renders export NAME='VALUE'${NAME:+":$NAME"}, and POSIX gives no syntax for quoting the NAME half -- quoting it would change what gets assigned rather than escape it. So a name like X; rm -rf ~; Y is not a mangled variable, it is a SECOND COMMAND, the moment the output is sourced -- which is the documented use of sigil env exports. There is no downstream repair available for it: validation at this boundary is not the cheaper of two options for THIS half, it is the only one.

SCOPED DELIBERATELY TO THE NAME. An earlier draft of this comment said "do not harden the emitter and believe this class is closed", which over-generalised a true statement about the NAME into a false one about the pair. The VALUE half sits in the same emitted line, carries the same lock-controlled data, and DOES have a canonical downstream repair (' -> '\''). Saying otherwise steered the next reader away from the second vector, which was live at the time and is handled below. If you are hardening the emitter, harden the VALUE; the NAME still cannot be.

Before v2 these five names were hardcoded constants and no lock content could reach that emitter. base-search-paths is what made a variable name attacker-controllable, so the check arrives with the field that opened the hole. Gate: test/integration/test-env-search-path-injection.sh drives the EMITTER, because the reader-side assertion in test-env-surface.sh does not cover it and passed throughout.

AND THE SUBDIR NEEDS THE SAME TREATMENT, FOR THE OPPOSITE REASON.

The subdir is emitted INSIDE the single quotes: export NAME='<store>/<SUBDIR>:<store>/<SUBDIR>'. Being quoted is not protection on its own -- quoting only contains a value if the QUOTE CHARACTER ITSELF cannot appear in it. A subdir of bin'; rm -rf ~; :' closes the string and appends a command, once per closure member.

RECORDED BECAUSE THE MISTAKE REPEATED ITSELF HERE: the first version of this fix validated the VARIABLE (whose destination is obviously syntax) and left the SUBDIR alone on the reasoning that it is quoted -- which is the SAME asymmetry that caused the original defect, where the author validated the subdir as a PATH and left the variable alone. Both halves of this pair reach a shell; neither is safe by position.

The comment above once claimed the subdir already got "the same shape-check discipline as valid-profile-name?". It did not: valid-profile-name? rejects control characters, DEL, ', " and \ and the subdir check applied none of them. We cannot reuse that predicate wholesale because it also rejects /, which a subdir needs (share/man), so the character rule is restated here rather than referenced.

Render a plan AND prove the bytes parse back before anyone writes them.

env-lock->string stays a pure renderer: tests legitimately serialize deliberately-invalid plans to exercise the reader's refusals, and a renderer that refuses cannot build those fixtures.

The guard belongs where the RISK is -- immediately before a file replaces a user's lock. Without it, a plan missing a required v2 field writes a lock THIS SAME BUILD REFUSES TO READ, after atomic-write has already destroyed the previous one. That is not hypothetical: this change's own headline bug was a second plan constructor silently dropping a field, and the mitigation written beside it -- "prove it survives by reading the written lock back" -- was a discipline note rather than code. This is that note, executed.

It validates the BYTES, not the plan: validate-plan requires canonical order, which only the serialized form has (the in-memory closure is accumulated unsorted and the writer sorts as it emits), so validating the plan directly would refuse every legitimate lock.

VALIDATE BEFORE WRITING, not only when reading.

validate-plan used to run at the reader only, so a plan missing a required v2 field wrote a file THIS SAME BUILD REFUSES TO READ -- after atomic-write had already replaced the user's lock. That is not theoretical: this change's own headline bug was a second plan constructor silently dropping a field, and the mitigation written beside it ("prove it survives by reading the written lock back") is a discipline note, not code. One call turns the whole class into a refusal before any bytes hit the disk.

FULL VERIFICATION: ignore the verification cache and re-hash every output against the lock. The cache lives in the store, which its owner can write, so anything running as that user can rewrite a record to match altered bytes (review 2026-09-23, reproduced). The cached check catches accidental and naive change; sigil env verify --full is the one that trusts nothing but the lock's hashes. The re-hash also rewrites the record, so a forged record is corrected, not just ignored.

"12.3 s" since START (a current-jiffy value). Integer arithmetic only, so the text never depends on how a float prints.

The first re-hash in a process announces the phase; each re-hash then reports its item and time. Cache hits say nothing, so an ordinary sigil env shell entry (0.7 s) stays silent and only a slow first entry -- or verify --full -- talks.

Returns (member . realpath) pairs so a caller can compose a member's own declared exports as well as the base set -- the pairs are on the member, not derivable from the path.

VERIFIES THE FULL CLOSURE, not just the members it returns. Composing commands would otherwise verify a strict SUBSET of what reify, apply and fetch verify, so sigil env shell would succeed on a store whose build-input outputs are missing or corrupt where it previously exited

  1. Narrowing verification as a side effect of composing fewer paths is not a trade anyone chose; it reuses env-lock-reify so the two cannot drift.

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)