Skip to content

feat(scan): detect the real environment, and make the drift loop work without FUSE - #3

Merged
kridaydave merged 8 commits into
Epoch-AI-Lab:mainfrom
kridaydave:feat/scan-and-fuseless-drift
Sep 27, 2026
Merged

kridaydave merged 8 commits into
Epoch-AI-Lab:mainfrom
kridaydave:feat/scan-and-fuseless-drift

Conversation

@kridaydave

Copy link
Copy Markdown
Contributor

What this is

taproot scan fills a state file in from what a project actually declares, and mount --no-fuse materializes the mounted tree into a real directory so the v0.1.0 drift loop runs without /dev/fuse.

The signed-envelope machinery was already here and works well. What was missing was the two ends: nothing produced a state worth signing, and the drift loop could not be exercised anywhere without a kernel module. With those in place, init is the fallback path rather than the only one.

The loop, end to end

taproot scan, fuseless mount, drift capture, sync, and registry history

the same loop as an animated GIF

Every command above exits 0. Nothing here needs /dev/fuse.

Commits

Each one is independently useful and builds on the last.

88ecdc4 The verify_fails_on_wrong_key test did not test what its name said. It built a scenario, commented in prose that it would pass, discarded it, and asserted something else.
dd43373 check --baseline X --state-path X compared a file to itself and reported no drift with exit 0. CI was never affected, since run.sh uses a separate temp file, but the CLI gate was a no-op for anyone who passed the same path twice.
341b994 --no-fuse wrote nothing, so the drift loop was unreachable in any container or CI runner, and the README documented it as the way to try the tool. It now writes the same tree the kernel mount serves; sync --from-dir reads it and delegates to the existing sign-and-adopt path.
8dedf6e scan itself. Reads .tool-versions, .mise.toml, Dockerfile FROM lines, package.json engines, and compose files.
5ea2398 registry log returned one entry and a comment admitted there was no chain. push now links each object to the one it superseded.
d48a668 README: three quickstart commands did not work as written.
45cb09b README: dropped an unverifiable stat.

Two decisions worth arguing with

scan never reads the process environment. A state file gets committed, so harvesting whatever happens to be exported would publish credentials. .env reading is opt-in via --include-env, and any value whose key name or shape reads as a credential is skipped and reported rather than stored. DATABASE_URL is not a secret; sk_live_ and ghp_ prefixes and PEM private key blocks are. Step 01 above shows STRIPE_SECRET being refused.

A tag that names no version is not a pin. FROM postgres and golang = "latest" are skipped rather than recorded as latest, since a floating tag is not an inherited environment. Tool names are canonicalized, so nodejs in .tool-versions and node in package.json do not become two runtimes for one tool.

Verification

cargo test    106 passed, 0 failed   (was 69)
cargo clippy  clean except one pre-existing warning in fabric.rs
cargo fmt     clean

scan and the fuseless loop are covered by integration tests, including that a secret never reaches the state file. The whole flow was also run end to end against the release binary; the output above is that run, not a reconstruction.

Tests went 69 to 106. Nothing existing was removed or weakened.

The last commit

45cb09b touches the stats citations. The 68% figure was attributed to "DevOps Research 2026", which I could not find, so it is gone. The other three now name the organizations they credit. If any of them are equally unverifiable, that commit drops cleanly on its own.

Not done

registry log history is local-only. There is no migration for refs that already exist, so branches pushed before this change have no parent link and still show a single entry. Content-addressed objects are never rewritten, so existing history is safe; the chain just starts from the first push after this lands.

Real FUSE mounting is untested here. /dev/fuse is blocked in this container, so everything above runs through the materialization path. The FUSE code path itself is unchanged apart from mount_readonly now taking &Path at an unchanged call site.

kridaydave and others added 8 commits September 27, 2026 23:17
The test built a scenario, commented in prose that it would pass, discarded
it, and then asserted a different property. It never exercised a signature
mismatched against a public key.

It now signs with one keypair and verifies against a different public key, and
asserts the specific InvalidSignature error rather than a bare is_err. Adds a
second case for a signature present with no public key, which is a mismatched
envelope rather than a valid state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`taproot check --baseline X --state-path X` compared a file to itself and
reported no drift with exit 0. The gate was a no-op for anyone who passed the
same path twice, and nothing in the output hinted at it.

CI was never affected: scripts/run.sh always writes the baseline to a separate
temp file. This closes the CLI hole.

Reuses the existing ensure_distinct helper, which canonicalizes so a symlink or
a different spelling of the same path is caught too. That helper reported
through TaprootError::Mount, which printed "mount failed" for a check problem,
so it gets its own InvalidPaths variant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`mount --no-fuse` validated the mountpoint and exited without writing
anything. The writable `env` file only ever appeared through a real FUSE
mount, so the v0.1.0 drift loop was unreachable in any container or CI
runner, and the README documented --no-fuse as the way to try the tool.

--no-fuse now materializes the same tree the kernel mount serves: env,
state.json, hash, version, README.taproot, plus runtimes/ and containers/.
env is the only writable file. `sync --from-dir` reads that file and hands
off to the existing review-and-sign path, so adoption, --force gating, and
signing all stay in one place rather than being reimplemented.

`mount`'s positional PATH is now optional, since the no-fuse path has no
mountpoint to give.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Everything downstream of a state file was built, but nothing filled one in.
`init` took only repo, branch, and commit, so the runtimes, containers, and
env vars in the README's example output were whatever a human typed. The
product was a signed envelope for a JSON file authored by hand.

`taproot scan` reads what the project already declares: .tool-versions,
.mise.toml, Dockerfile FROM lines, package.json engines, and the four compose
filenames. `scan --apply` folds the result into the state file and signs it,
creating the state if none exists and taking branch and commit from git.

Two decisions worth stating:

It never reads the process environment. A state file is committed, so
harvesting whatever happens to be exported would publish credentials. .env
reading is opt-in via --include-env, and any value whose key name or shape
reads as a credential is skipped and reported rather than stored. DATABASE_URL
is not a secret; sk_live_ and ghp_ prefixes and PEM private key blocks are.

A tag that names no version, like `FROM postgres` or `golang = "latest"`, is
not a pin and is not recorded. Tool names are canonicalized, so nodejs and
node do not become two runtimes for one tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`registry log` returned a single SignedState and a comment admitted there was
no history chain. For a product whose pitch is a signed, auditable state
history, that is the feature missing.

push now records the hash a ref previously pointed at as the new object's
`parent`, and log follows that chain newest-first. A re-push of the same hash
does not self-parent, which would make the walk loop forever, and the walk is
bounded at 10,000 hops so a corrupted link cannot spin.

`parent` is skipped when absent, so existing objects and any state file
written by an older version still deserialize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The quickstart had three commands that do not work as written. `registry list
--repo myapp` fails, because the subcommand takes a positional. `mount
--no-fuse` required a mountpoint it then never used, and wrote no files, so
the drift loop could not be tried at all. `check --baseline` is required and
was shown without one.

Rewrites the quickstart around what the tool now does, adds the no-fuse flow
so the drift loop is reachable without FUSE, and notes that scan skips
values which look like live credentials.

Also drops the link to CONTRIBUTING.md, which the README pointed at and the
repo does not contain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 68% "works on my machine" figure was cited to "DevOps Research 2026",
which does not correspond to anything I can find. It is removed.

The other three are kept and attributed to the organizations they name, so
each figure points at a source a reader can go look for rather than at a
generic research label. If any of them turn out to be equally unverifiable,
this is the commit to drop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real output from a release-binary run, committed so the PR body can render it.
frames.json is the raw capture the images are rendered from.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kridaydave
kridaydave merged commit 7afec62 into Epoch-AI-Lab:main Sep 27, 2026
2 checks passed
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