fix: Keep turbo setup's tools working after a filesystem restore (#14983)
`turbo run` and `turbo exec` now keep using the tools `turbo setup`
installed when the repository is restored, copied or remounted on a
different device at the same path, as a snapshot or volume restore, a
container layer, or a copied disk does. Until now the setup record
stopped matching after such a restore, and every run exited 1 with `no
matching trusted setup record`, even with the tool store and
`turbo.lock` intact.
- A setup record binds the canonical path, the inode, the `turbo.lock`
digest, and the environment. The device id is still written, for
diagnostics, but no longer compared.
- Windows treats it the same way: the file index still binds the
directory, the volume serial number no longer does.
- Still rejected: a re-clone at the same path (new inode), a changed
`turbo.lock`, a different canonical path, and a different environment.
- Security trade, stated plainly: a different filesystem mounted at the
same path, whose repo root has the same inode number and the same
`turbo.lock`, is now trusted. The device id was the only thing that
rejected that.
- The record format does not change (schema 2, same fields), so existing
records still parse, and an older binary still reads a new record and
still requires the device to match.
Validation:
- `cargo fmt --check`; `cargo clippy -p turborepo-cli --all-targets --
-D warnings`; the trust and authorization tests (21 passed); `cargo test
-p turbo --test setup` (37 passed).
- New tests: a directory restored on another device is accepted by the
strict consumer and by the shell hook; a new inode, a changed
`turbo.lock`, and a different root are still rejected.
- Loop-mounted ext4 images, a stand-in for a volume restore: install on
device 1792, then mount the copy alone on 1793 at the same path with
inode 12. `origin/main` rejects `turbo exec` and `turbo run`; this
branch runs both. A copy of the directory at the same path (new inode)
and an edited `turbo.lock` are still rejected.
<details><summary>Before and after</summary>
### Identity
Before: a record's directory identity was the pair `(st_dev, st_ino)` on
Unix and `(dwVolumeSerialNumber, file index)` on Windows, compared for
equality on every read of the record.
After: only `st_ino` (the file index on Windows) is compared. `st_dev`
(the volume serial number) is still recorded, unchanged, as a
diagnostic: it is what changes when a filesystem is restored, copied or
remounted on another device, so keeping it in the record leaves that
visible without trusting it.
### The record
Unchanged, byte for byte, for a given directory: schema 2, `{schema,
root, device, inode, lock_sha256, env}`, plus `selection` for a
`--no-lock` clone. From the repro after the restore:
```json
{"schema":2,"root":"/mnt/work/repo","device":1792,"inode":12,"lock_sha256":"544dd825f4a1b0e21192ea51731f47a9ef6933223c38d5055e6ae8ad69763bc2","env":"38f07e7cdb09eb1f5fbee72d8fc2640fa9187de3da9b34e0417244a46258235c"}
```
`st_dev` is 1793 here, and the record still says 1792.
Backward compatibility: new code reads a record written by a released
canary (`2.11.11-canary.2`) as written, because the fields are unchanged
and `deny_unknown_fields` is still satisfied. A record this code writes
is readable by that binary too, which then enforces the device itself,
so a downgrade is stricter, never looser. No schema bump is needed.
### Flow
Before and after: `turbo setup` installs tools, then
`TrustStore::write_record` reads `identity(root)` and writes the record.
`turbo run` and `turbo exec` call `Authorization::inspect`/`authorize`,
which capture the lock plan and call `TrustStore::authorize`; the shell
hook calls `TrustStore::recorded`; both call `same_directory`, then
`store.activate`. Only `same_directory` changed: it compares
`record.inode` instead of `(record.device, record.inode)`.
### Receipts
Install on the first mount, device 1792, inode 12:
```
$ stat -c "%d %i %n" /mnt/work /mnt/work/repo /mnt/work/.turbo-store
1792 2 /mnt/work
1792 12 /mnt/work/repo
1792 8193 /mnt/work/.turbo-store
```
The image copied while its loop device stays attached, and the copy
mounted alone at the same path:
```
$ stat -c "%d %i %n" /mnt/work /mnt/work/repo /mnt/work/.turbo-store
1793 2 /mnt/work
1793 12 /mnt/work/repo
1793 8193 /mnt/work/.turbo-store
```
`origin/main`:
```
$ turbo exec -- node --version
x managed setup validation failed: managed tools have no matching trusted
| setup record; run turbo setup to repair the installation (turbo.lock is
| unchanged)
EXIT=1
$ turbo run build
x managed setup validation failed: managed tools have no matching trusted
| setup record; ...
EXIT=1
```
This branch:
```
$ turbo exec -- node --version
v24.0.0
EXIT=0
$ turbo run build
Tasks: 1 successful, 1 total
EXIT=0
```
Rejections on this branch, same restored mount. A copy of the directory
at the same path (`mv repo repo.old && cp -a repo.old repo`, inode 8194
instead of 12) is rejected. Restoring the original inode with `mv
repo.old repo` is accepted again, and appending a newline to
`turbo.lock` (digest `04db87e9…` instead of `544dd825…`) is rejected for
both `exec` and `run`.
Logs:
`/tmp/vts-trust/{install-main.log,stat-before.txt,stat-after.txt,before-restore.log,after-restore.log,reject-inode.log,reject-lock.log}`.
</details>
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>