Skip to content

Hardening: close the residues recorded through layer 8 - #184

Open
EiffL wants to merge 9 commits into
clean_rebuildfrom
hardening
Open

Hardening: close the residues recorded through layer 8#184
EiffL wants to merge 9 commits into
clean_rebuildfrom
hardening

Conversation

@EiffL

@EiffL EiffL commented Aug 20, 2026

Copy link
Copy Markdown
Member

The rebuild is functionally complete through layer 8; this PR closes the
recorded residues that do not need Perlmutter access, one commit per
decision. Each choice below was settled explicitly before implementation.

What lands

  • Ambient UV_* scrub (closes Scrub the ambient UV_* namespace before invoking uv #179): project.child_env drops every
    UV_* variable outside a closed plumbing allowlist (cache dir,
    timeouts, TLS, air-gap mode, index credentials, uv's recursion guard),
    so an exported UV_NO_BINARY or UV_PYTHON can no longer steer a sync
    while env_version reports nothing moved. The run verbs warn with the
    names of any non-empty variable dropped, from the same predicate.
  • Machine-level uv.toml advisory (env_version cannot see user- or system-level uv.toml, and uv has no "pyproject.toml only" mode #176, option 2): the scan checks
    uv's two documented user/system config paths for audited
    install-settings keys — key presence, not file presence, because list
    settings concatenate across levels — and reports a hit beside
    sdist_built. Never hashed.
  • Network uncontrolled on every mechanism: the OCI wrap drops
    --network none, its one restriction, so all three mechanisms attest
    network: allowed symmetrically and no consumer can read a promise
    into "containerized". denied stays in the type for a mechanism that
    genuinely emits a denial flag. Retires the Perlmutter spike item about
    --network none hanging on compute nodes.
  • Recipe write scope narrowed to its own output directory (reverses
    the recorded "not narrowed" decision): closes the cross-write residue —
    a concurrent task landing bytes in a sibling's directory before the
    sibling hashed produced a manifest that was self-consistent and wrong,
    undetectable by any checksum, so prevention is the only fix. A probe
    keeps results/ whole (no output id) — the one probe/recipe asymmetry,
    and it excludes exactly the accident being prevented. The new
    enforcement test is mutation-checked through Unavailable().
  • uv_version manifest attestation: the engine-closure decision's
    concrete loss, recorded — probed once per run by the driver and handed
    down (the HEAD discipline), beside lc_version. Attestation, never
    identity.
  • Per-file sha256 in the crate, from annex keys: every output file is
    a File under its dataset's hasPart with sha256/contentSize
    parsed from its SHA256E key — verifiable by sha256sum after a
    git archive deposit, available bytes-free because keys are repository
    state (--include=* is load-bearing). Non-SHA-256 keys yield size and
    no digest; git-carried files hash their bytes. Also fixes a latent
    honesty bug: out-of-tree inputs published lc's framed digest under
    the workflow-run sha256 term; they now publish none. Validator floor
    unchanged at five.
  • crate: line in lc status: the rerun-lag residue made visible by
    comparing the document's own datePublished against the newest
    manifest finished_at the walk already read — no git, no rocrate
    import on status's path.
  • End-of-run edit warning: an edit made while the graph ran (the
    git_dirty window) is now named in the report; still no manifest
    field, by decision.
  • [project.scripts] absence pinned: the recorded review item — a
    metadata test asserts the worker and the shim never become console
    scripts.

Re-examined and deliberately left (recorded in CLAUDE.md)

  • The forged-run-record-subject residue: the threat model excludes
    adversaries, and a copied subject is already deliberate.
  • The validator RECOMMENDED floor stays pinned at five — no
    publication-metadata config surface.

Housekeeping

Closed stale pre-rebuild issues #166, #128, #153 with pointers;
commented triage on #152 and the advisory landing on #176.

Verification

Full suite: 582 passed (sandbox enforcement real on Linux; crate smoke
with the official rocrate-validator, REQUIRED clean and the floor
unchanged). ruff and mypy clean. Cross-write denial mutation-checked
through Unavailable() (rc 0, bytes replaced, unsandboxed).

Closes #179.

🤖 Generated with Claude Code

https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa

EiffL and others added 9 commits August 20, 2026 19:28
… uv.toml

The ambient half (#179): child_env() drops every UV_* variable outside a
closed plumbing allowlist — cache location, timeouts, TLS, air-gap mode,
index credentials, uv's own recursion guard — so an exported UV_NO_BINARY
or UV_PYTHON can no longer steer what a sync installs while env_version
reports nothing moved. The run verbs (materialize, lc run) name any
non-empty variable the scrub dropped, from the same predicate, so the
report cannot disagree with the scrub.

The config-file half (#176, the advisory option): uv merges user- and
system-level uv.toml underneath the project's settings, and list settings
concatenate across levels — so the scan now checks the two documented
paths per platform for audited install-settings keys and reports a hit
beside sdist_built. Reported, never hashed: machine state in env_version
would make one commit answer differently on two hosts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
…the OCI wrap

The containerized backend was the one mechanism restricting the network,
which made "containerized" carry a promise the direct mechanisms never
made — and the direction of the recorded decision is symmetry: nothing
pretends to a control it does not apply. The wrap now emits no --network
flag, the attestation says `allowed` like landlock and seatbelt, and the
`denied` literal stays reserved for a mechanism that genuinely emits a
denial flag. This also retires the Perlmutter spike item about
`--network none` hanging on compute nodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Closes the cross-write residue: a concurrent task landing bytes in a
sibling's output directory before the sibling hashed produced a manifest
that was self-consistent and wrong — undetectable by any checksum, so
prevention is the only fix. exec_policy gains one keyword (output_dir),
handed down from the worker's task; a probe has no output id and keeps
results/ whole, which is now the one probe/recipe asymmetry: the
probe→recipe promise excludes exactly the commands that write outside
their own output directory. All three mechanisms express the narrower
shape natively (the same nested-writable-directory form, one level
deeper), and the OCI mount table inherits it from the write set with no
backend change. Integrity-answering stays data_version's job alone.

The enforcement test is mutation-checked: the same cross-write through
Unavailable() succeeds (verified rc 0, bytes replaced).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The engine-closure decision's concrete loss was that nothing recorded
which uv converged the environment — the one tool between the lock and
the installed artifacts. The driver probes `uv --version` once per run
and hands it down (the HEAD discipline; the rerun entry point probes its
own), and the worker records it beside lc_version. Attestation, never
identity: defaulted empty, outside both hashes, never read by classify,
and an unparseable probe records "" rather than failing a run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
… as sha256

Every file in an output directory now appears in the crate as a File
under its dataset's hasPart, carrying sha256 and contentSize parsed from
its SHA256E annex key — the raw digest `sha256sum` can verify after a
`git archive` deposit, available with none of the bytes fetched because
keys are repository state (dataset.annex_keys; `--include=*` is
load-bearing — bare `find` lists only present files). A non-SHA-256
backend key yields size and no digest, never a wrong one; git-carried
files (the lock, universes, manifests) hash their own bytes. The key map
is injected into render like the writer, so the builder stays git-free
and the render pure.

This also fixes a latent honesty bug: an out-of-tree declared input's
recorded input_versions digest is lc's *framed* hash, and publishing it
under the workflow-run `sha256` term claimed a checksum nothing could
verify. Externals now publish no digest — the manifests keep the full
story, the layer's stated weaker promise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The rerun-lag residue made visible. The render pins datePublished to the
newest manifest finished_at, so status can read lag off the document
itself — a content comparison against the manifests the walk already
read, with no git call and no rocrate import (the crate stays the one
materialize-only dependency on status's path; license_of and
CRATE_FILENAME move to project.py for the same reason). Four states:
not maintained / will be created / up to date / behind, plus an honest
"unreadable" for a corrupted file — and the line lands in --json through
as_dict like the rest of the header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The mid-run edit hole, surfaced: the dirty check runs at start of run
while manifests are written per-output later, so an edit in between left
manifests whose git_sha no longer described the code that ran, silently.
The run now ends with one dataset.status call — the tree started clean
and save/restore keeps results/ clean, so any dirt appeared mid-run —
and warns with the edited paths. Still a warning, never a manifest
field: the spec's git_dirty stays unwritten by decision.

Also closes the recorded review item: a metadata test now pins that no
[project.scripts] entry ever targets lightcone.engine or the sandbox
shim — every entry point is the CLI or a mirrored git-annex executable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The uv identity holes (scrub landed, machine-config advisory), the
crate's raw-digest rule and the injected key map, the status crate line,
and the launcher-scrub note resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
The sweep: dataset.save now commits with the same pathspec it stages —
a partial commit, built from HEAD plus its own paths alone — so work the
user staged while a graph ran stays staged and warned-about, never swept
into an output or crate commit. The nothing-to-commit probe is scoped
the same way, and the annex per-add config rides on the commit too,
since a partial commit takes the paths through the clean filter again.
The stronger move — a frozen execution worktree — is recorded as
deferred to the venue era, not rejected.

The crate's byte fallback re-checks the annex pointer shape before
hashing: annex_keys answers empty for the whole repository when
git-annex cannot answer at all, and a pointer file reads perfectly well,
so one failed `git annex find` would otherwise publish a well-formed
digest of the pointer text for every output file. The key parse splits
from the last tab (git-annex emits ${file} unescaped; keys never contain
tabs), and _file stops hashing the license file twice.

The scrub allowlist keeps four more plumbing variables —
UV_PYTHON_INSTALL_DIR (the interpreter store has no project-level
spelling, so scrubbing it shipped a remedy that does not exist),
UV_LINK_MODE, UV_PYTHON_INSTALL_MIRROR, UV_KEYRING_PROVIDER — and the
warning is composed once (project.uv_scrub_warning) and surfaced by
every uv-acting verb: init and build now say it too, instead of leaving
a corporate-mirror user with uv's raw resolution error. The machine
config probe covers XDG_CONFIG_DIRS, keeping its "complete" claim true.

Honesty edges: the crate status line says "up to date with the outputs"
/ "behind the outputs" — the exact scope of its datePublished proxy,
correct in both directions (a dropped output regresses the newest stamp
as a rerun advances it); the write-denial remedy names the recipe's own
output directory instead of results/; the Landlock/Seatbelt divergence
the narrowing exposed (`rm -rf $OUT` is EACCES on Linux, fine on macOS)
is recorded with its fix — delete the redundant prelude, the worker
resets the directory; the deposit-vs-consumed meaning of a File's sha256
is recorded where the old conflict rule used to be; and a comment that
referenced the spec stands on its own now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CRERrB5zWdVXD6uPv2BRa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant