feat(scan): detect the real environment, and make the drift loop work without FUSE - #3
Merged
kridaydave merged 8 commits intoSep 27, 2026
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
taproot scanfills a state file in from what a project actually declares, andmount --no-fusematerializes 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,
initis the fallback path rather than the only one.The loop, end to end
Every command above exits 0. Nothing here needs
/dev/fuse.Commits
Each one is independently useful and builds on the last.
88ecdc4verify_fails_on_wrong_keytest did not test what its name said. It built a scenario, commented in prose that it would pass, discarded it, and asserted something else.dd43373check --baseline X --state-path Xcompared a file to itself and reported no drift with exit 0. CI was never affected, sincerun.shuses a separate temp file, but the CLI gate was a no-op for anyone who passed the same path twice.341b994--no-fusewrote 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-dirreads it and delegates to the existing sign-and-adopt path.8dedf6escanitself. Reads.tool-versions,.mise.toml,DockerfileFROM lines,package.jsonengines, and compose files.5ea2398registry logreturned one entry and a comment admitted there was no chain.pushnow links each object to the one it superseded.d48a66845cb09bTwo decisions worth arguing with
scannever reads the process environment. A state file gets committed, so harvesting whatever happens to be exported would publish credentials..envreading 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_URLis not a secret;sk_live_andghp_prefixes and PEM private key blocks are. Step 01 above showsSTRIPE_SECRETbeing refused.A tag that names no version is not a pin.
FROM postgresandgolang = "latest"are skipped rather than recorded aslatest, since a floating tag is not an inherited environment. Tool names are canonicalized, sonodejsin.tool-versionsandnodeinpackage.jsondo not become two runtimes for one tool.Verification
scanand 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
45cb09btouches 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 loghistory 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/fuseis blocked in this container, so everything above runs through the materialization path. The FUSE code path itself is unchanged apart frommount_readonlynow taking&Pathat an unchanged call site.