Skip to content
Merged
37 changes: 37 additions & 0 deletions .taproot-verify/frames.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"cmd": "scan . --include-env --apply",
"out": "TAPROOT SCAN\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndir: .\n\nruntimes: 2\n node 20.5.0 (pinned)\n python 3.11.4 (pinned)\n\ncontainers: 2\n cache redis:7 \u2192 7\n db postgres:15.3 \u2192 15.3\n\nenv-vars: 2\n DATABASE_URL=postgres://localhost/app\n NODE_ENV=development\n\nskipped 1 value(s) that look like secrets:\n STRIPE_SECRET line 3: key name marks it as a secret\n\napplied: sha256:a3b7c737e463 \u2192 /root/prdemo/proj/.taproot/state.json",
"err": "",
"rc": 0,
"note": "detects the real environment and signs it"
},
{
"cmd": "mount --no-fuse",
"out": "TAPROOT MOUNT\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nrepo: proj\nbase: main@9d2ceaf\nstate: signed \u00b7 sha256:a3b7c737e463\nruntimes: 2\n - node: 20.5.0 (pinned=true)\n - python: 3.11.4 (pinned=true)\ncontainers: 2\n - cache: 7 (redis:7)\n - db: 15.3 (postgres:15.3)\nenv-vars: 2\n\nmount: (none \u2014 materializing a tree)\nhash: a3b7c737e4630052d26fe5987b8335ac1f62cf476b8e920e18495c4faf40955c\n\n(no-fuse \u2014 wrote tree to /root/prdemo/proj/.taproot/mnt)\nenv: /root/prdemo/proj/.taproot/mnt/env (writable \u2014 edit, then run `taproot sync --from-dir`)\nstatus: \u25b6 INHERITED \u2014 ready to work",
"err": "",
"rc": 0,
"note": "materializes the tree, no FUSE needed"
},
{
"cmd": "sync --from-dir .taproot/mnt --dry-run",
"out": "captured drift from .taproot/mnt \u2192 .taproot/state.drift.json\nTAPROOT SYNC\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nbaseline: sha256:a3b7c737e4630052d26fe5987b8335ac1f62cf476b8e920e18495c4faf40955c (/root/prdemo/proj/.taproot/state.json)\ndrift: sha256:498b0fe05504554687057e6ee9e9fe8d643949a39ef4c7f5bd8ff428e9e044a5 (/root/prdemo/proj/.taproot/state.drift.json)\n\ndrift (1 field):\n + env_vars.REDIS_URL\n actual: redis://localhost:6379\n severity: Breaking\n\ndry-run \u2014 nothing adopted.\n[re-run without --dry-run to sign and adopt]",
"err": "",
"rc": 0,
"note": "drift is captured without a kernel mount"
},
{
"cmd": "sync --from-dir .taproot/mnt",
"out": "captured drift from .taproot/mnt \u2192 .taproot/state.drift.json\nTAPROOT SYNC\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nbaseline: sha256:a3b7c737e4630052d26fe5987b8335ac1f62cf476b8e920e18495c4faf40955c (/root/prdemo/proj/.taproot/state.json)\ndrift: sha256:1783901812724a9fc9ca001fef686484adb0c2e48e0b9cce7f4c8f421a5b4136 (/root/prdemo/proj/.taproot/state.drift.json)\n\ndrift (1 field):\n + env_vars.REDIS_URL\n actual: redis://localhost:6379\n severity: Breaking\n\nsigning with key mykey (V3/yzCGoSvV3SY45)\nadopted: sha256:1783901812724a9fc9ca001fef686484adb0c2e48e0b9cce7f4c8f421a5b4136\npath: /root/prdemo/proj/.taproot/state.json\n\nstatus: \u25b6 INHERITED \u2014 ready to work",
"err": "",
"rc": 0,
"note": "review, sign, adopt"
},
{
"cmd": "registry log proj main",
"out": "TAPROOT REGISTRY LOG\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nrepo: proj\nbranch: main\nregistry: /root/prdemo/proj/.taproot/registry\n\n* sha256:178390181272 main@9d2ceaf signed\n\n1 entr(ies), newest first",
"err": "",
"rc": 0,
"note": "history now walks the branch"
}
]
Binary file added .taproot-verify/loop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .taproot-verify/loop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions CD_res/implementation/taproot/taproot-research.md

Large diffs are not rendered by default.

78 changes: 58 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,32 @@ Taproot is the state inheritance fabric between VCS and CI. Environment-as-objec

## The problem

- **68%** of "works on my machine" incidents trace to undocumented environment drift <cite>DevOps Research 2026</cite>
Environment drift is invisible. A project can pin its language versions and still not pin the thing that actually breaks a build:

- **52%** of CI failures are environment-related, not code-related <cite>CircleCI 2025 State of CI</cite>
- New developers take **2.3 weeks** to reach full productivity due to environment setup <cite>Stripe Onboarding Study</cite>
- **52%** of CI failures are environment-related, not code-related <cite>CircleCI 2025</cite>
- Reproducing a colleague's exact dev environment is considered "nearly impossible" by **74%** of engineers <cite>GitHub Octoverse 2026</cite>

The environment is the code that git forgot. Taproot inherits it like an object, not a recipe.

## The wedge

A FUSE mount CLI that lazily materializes git repos as signed environment snapshots:
A mount CLI that materializes git repos as signed environment snapshots:

```bash
$ taproot scan .

TAPROOT SCAN
─────────────────────────────────────────
dir: .

runtimes: 2
node 20.5.0 (pinned)
python 3.11.4 (pinned)

containers: 1
db postgres:15.3 → 15.3

$ taproot mount ~/projects/myapp

TAPROOT MOUNT
Expand All @@ -26,14 +40,14 @@ $ taproot mount ~/projects/myapp
base: main@9f3a2c1
state: signed · sha256:b2c1...
materialized: 2.4 GB (lazy)

python: 3.11.4 (pinned)
node: 20.5.0 (pinned)
postgres: 15.3 (container, signed)
env-vars: 12 loaded from baseline

status: ▶ INHERITED — ready to work

[s]ync · [f]ork · [d]etach
```

Expand All @@ -45,10 +59,12 @@ We are building the wedge primitive:
- [x] State serialization engine (Rust)
- [x] FUSE mount CLI (read-only, v0.0.1)
- [x] GitHub Action + baseline check (`taproot check` strict, composite action)
- [x] Signed state registry (local content-addressed, `taproot registry push/pull/list`)
- [x] Signed state registry (local content-addressed, `taproot registry push/pull/list/log`)
- [x] Key management (`taproot keys generate/list/rotate`)
- [x] Managed fabric + registry API (`taproot serve`, `taproot remote`, `taproot fabric` audit/policy/tokens)
- [x] Drift loop (v0.1.0): writable `env` file in the mount, drift captured on unmount, `taproot sync` to review, re-sign, and adopt
- [x] Environment capture (`taproot scan`): reads `.tool-versions`, `.mise.toml`, `Dockerfile`, `package.json`, and compose files
- [x] Registry history: every push links to the state it superseded, so `registry log` walks a branch back to its first commit

## Open source

Expand All @@ -60,30 +76,52 @@ Taproot's mount CLI, protocol format, and state schema are MIT-licensed. The man
git clone https://github.com/Epoch-AI-Lab/taproot.git
cd taproot
cargo build --release
./target/release/taproot keys generate --id mykey
./target/release/taproot init --repo myapp --branch main --commit 9f3a2c1
./target/release/taproot registry push
./target/release/taproot registry list --repo myapp
./target/release/taproot mount --no-fuse ~/projects/myapp # requires existing dir; omit --no-fuse for real FUSE
./target/release/taproot status
./target/release/taproot verify
./target/release/taproot check --baseline .taproot/baseline.json --json # strict drift check
T=./target/release/taproot

# 1. keypair (private stays local, public is shareable)
$T keys generate --id mykey

# 2. detect the real environment, then sign it
$T scan . --include-env
$T scan . --apply

# or hand-write a state if the project declares nothing
$T init --repo myapp --branch main --commit 9f3a2c1
$T registry push
$T registry list myapp
$T registry log myapp main

# 3. mount. omit --no-fuse for a real FUSE mount
$T mount --no-fuse # writes .taproot/mnt, works without /dev/fuse
$T mount ~/projects/myapp # real FUSE, env file writable

# 4. the drift loop, without FUSE
echo 'DATABASE_URL=postgres://localhost/app' >> .taproot/mnt/env
$T sync --from-dir .taproot/mnt --dry-run
$T sync --from-dir .taproot/mnt # review, sign, adopt

# 5. verify and gate in CI
$T status
$T verify
$T check --baseline .taproot/baseline.json --json

# remote fabric
./target/release/taproot serve --addr 127.0.0.1:3000 &
./target/release/taproot remote push --remote http://127.0.0.1:3000
./target/release/taproot fabric audit
$T serve --addr 127.0.0.1:3000 &
$T remote push --remote http://127.0.0.1:3000
$T fabric audit
```

`mount --no-fuse` writes the same tree a FUSE mount serves (`env`, `state.json`, `hash`, `version`, `runtimes/`, `containers/`) into a real directory. Only `env` is writable. That makes the whole drift loop testable in a container or CI runner, where `/dev/fuse` is usually unavailable.

`scan` never reads your process environment. It reads files the project already commits, and it skips any value that looks like a live credential rather than storing it in a file you are about to commit.

## Contribute

We need:
- Systems engineers who have fought environment drift
- DevOps engineers who have automated onboarding
- Anyone who has ever lost a day to "works on my machine"

See [CONTRIBUTING.md](./CONTRIBUTING.md).

## Cite the research

All figures in this README are verbatim from the [Developer Workflow Bottlenecks](https://github.com/Epoch-AI-Lab/research) corpus (23 bottlenecks, 21 sources, compiled 2026-08-08).
Expand Down
Loading
Loading