Skip to content

feat(sync)!: write lockfile v2 and replay locked archives - #569

Merged
zeroshade merged 23 commits into
columnar-tech:mainfrom
eitsupi:feat/lockfile-v2
Nov 10, 2026
Merged

zeroshade merged 23 commits into
columnar-tech:mainfrom
eitsupi:feat/lockfile-v2

Conversation

@eitsupi

@eitsupi eitsupi commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Closes #400

Builds on the archive hash verification from #567. This PR adds lockfile v2 and archive-based reuse to dbc sync.

  • Lockfile v2: records each driver’s registry, per-target archive SHA-256, structured target, format, and typed location. Optional size is checked on downloads. Evidence and host requirements can be read, but registry resolution does not produce them and sync does not enforce them. Invalid or ambiguous secondary targets are omitted; current-target failures remain errors. v1 locks upgrade automatically, retaining compatible pins and checking applicable library proofs. dbc 0.3.1 rejects v2; 0.3.0 and earlier ignore v2 metadata, install pinned names and versions without archive hash checks, and rewrite the lock as v1. All collaborators must upgrade to a v2-supporting release before sharing an upgraded lock.
  • Replay and registry binding: a compatible lock entry for the current target avoids registry index lookup. Its credential-free registry identity must match exactly one configured registry; configured registries differing only by userinfo or query are ambiguous. Missing targets cannot be added when the matching registry URL has a query string. HTTPS registries cannot use HTTP artifact URLs.
  • Reuse and install: the config.CheckInstalledPackage API checks installed registration, receipt, library, and archive digest before reuse, and re-verifies signatures unless --no-verify is set. If a lock file cannot be opened because of permissions or a read-only filesystem, it checks an optimistic read-only snapshot; observed changes prevent reuse. The config.InspectPackageArchive API checks v1 library proofs. Otherwise sync downloads and checks required archives before saving the lock and installing. Installation failures retain the lock and prior successful installs so a later sync retries with the same archives. Concurrent syncs wait up to 10 seconds for the project lock, which remains held through installation.

Normal sync reinstalls drivers previously installed with --no-verify using signature verification; direct dbc install retains its already-installed behavior. Default HTTP clients strip Authorization and Referer across origins and reject HTTPS-to-HTTP redirects. Download filenames omit URL queries.

Network cancellation and local path sources remain deferred; quitting is immediate, and an in-flight download may leave a temporary file.

@eitsupi
eitsupi marked this pull request as ready for review November 6, 2026 17:06

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core logic looks solid: lockfile v2 validation, replay, reuse, downloading and checking every archive before writing the lockfile, project-lock accounting (clean under -race), redirect header stripping, and path-traversal checks on registry-path. The tests are thorough. Four things I'd like fixed before merge:

1. config/install_package_reuse.go:101-109: a no-op sync now fails on installs the user can't write. CheckInstalledPackage always takes the driver and namespace locks, and fslock opens the lock files with O_CREATE|O_RDWR. Suppose an admin set up a conda or venv environment, or a system-level install, and a user without write access runs dbc sync on an unchanged lockfile. On #567 that exits 0. Here it fails with failed to check installed driver …: acquire driver install lock: fslock: open …: permission denied. Suggested fix: if a lock can't be opened with fs.ErrPermission/EROFS, either run the checks without the lock or take a shared lock on a read-only descriptor. Generations are published by rename, so a concurrent writer can only make the result "not reusable". Please add a test for a read-only install.

2. docs/guides/driver_list.md:162 and the PR description: "Older dbc versions cannot read v2" is only true for 0.3.1. v0.3.1 rejects the file with unsupported lock file version 2. v0.3.0 and earlier don't check the version at all. They read name/version, install those versions without any hash check, and then rewrite the file as version = 1 (s.locked.Version = lockFileVersion, v0.3.0 cmd/dbc/sync.go:353). That silently strips the v2 data from a committed dbc.lock. The docs should say so and tell teams to upgrade everyone who shares the lockfile first.

3. cmd/dbc/lockfile_v2.go:141, :425-430: a lockfile URL can send the registry token over plain HTTP. validateLockedURL accepts http, URL-type artifacts are fetched directly, and makereq picks credentials by host only (auth/credentials.go:272). So http://<credentialed-host>/… in dbc.lock gets Authorization: Bearer <token> in cleartext before the hash is checked. That is the downgrade checkRedirect refuses. dbc.toml already allows http registries, so this is defence in depth. Still, dbc.lock is a generated file that reviewers skim. Please reject http URL locations when the matched registry is https.

4. cmd/dbc/sync_plan.go:417, :484-489: the short-secret redaction fallback hides unrelated errors. If any query or userinfo value in a configured registry is shorter than 4 characters (for example ?v=1 or ?tenant=eu), nearly every prepare error matches it and becomes "registry request failed; details are hidden…". A hash mismatch and a TOML syntax error in dbc.lock both get replaced this way. Please redact only registry and download errors. URL stripping already removes userinfo and queries.

Nits:

  • cmd/dbc/sync_plan.go:296-310: if one of another platform's packages can't be recorded (malformed hash, a URL with ?sig=, a duplicate target), fresh resolution fails on every platform. That contradicts "just the affected package is rejected" in drivers.go:328-330. Consider skipping those packages for non-current platforms.
  • config/package_receipt.go:60-62: the TODO about needing source evidence before strict locked-artifact reuse is stale now that this PR implements that reuse.
  • cmd/dbc/sync.go:341: warnings are written with fmt.Fprintln(os.Stderr, …) while the TUI is drawing, which may garble the output (not checked visually). tea.Printf would avoid that.
  • Docs: a second concurrent sync waits only 10s for the project lock and then fails. "Serializes" suggests it waits until the first finishes. Also, two registries that differ only in query or userinfo now make sync fail as ambiguous, and that isn't documented.

CI: the ubuntu-latest failure is the sudo -E issue that #568 fixes.

@eitsupi

eitsupi commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

All items are addressed. The branch also merges #567's follow-up (7fe261b) and main.

  1. Read-only installs (33836e7): when the driver or namespace lock file can't be opened because of fs.ErrPermission or EROFS, CheckInstalledPackage falls back to an optimistic read-only check. It re-verifies the signature on a copy taken from the pinned generation, then re-checks the receipt, files and registration, and any change makes the driver not reusable. Contention, timeouts and cancellation are still errors. New tests run a no-op sync against a read-only install.
  2. Older dbc and v2 (caafbd8): the docs and the PR description now say that 0.3.1 rejects v2, while 0.3.0 and earlier install the pinned versions without hash checks and rewrite the file as v1. Teams should upgrade everyone who shares the lockfile first.
  3. HTTP artifact URLs (a550de6): when the registry is HTTPS, an http URL location is rejected when the lock is generated, when it is read and again right before replay, so no request is ever sent. HTTP registries are still allowed.
  4. Redaction scope (e6da1aa): secret-value replacement, including the short-secret fallback, now applies only to errors from the registry and credential boundaries (requests, authentication, URL resolution). Every error still has userinfo and queries stripped from its URLs, and typed errors such as hash mismatches keep their message.

Nits:

  • Secondary targets (e6da1aa): the current target is resolved first. Other targets that can't be recorded, or that collide with another target, are skipped, so fresh resolution no longer fails on every platform.
  • I removed the stale receipt TODO (33836e7).
  • Warnings in text mode now go through Bubble Tea commands. In JSON modes they still go to stderr (e6da1aa).
  • The docs (caafbd8) now give the 10-second project-lock wait and say that registries differing only in userinfo or query are ambiguous.

@eitsupi
eitsupi requested a review from zeroshade November 7, 2026 13:45

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four points are addressed, thanks. The read-only fallback re-checks the right things after the verifier (directories, receipt, library digest, signature bytes, registration), and contention and cancellation are still errors. The downgrade docs are accurate now, HTTP artifact URLs are rejected at validation, recording and replay, and hash and lockfile errors are no longer hidden. I re-ran go test ./... and -race on cmd/dbc and config, and the read-only tests run and pass as non-root.

One non-blocking nit inline, fine to do in a follow-up.

Comment thread cmd/dbc/sync_plan.go
pkg, err = drv.GetPackage(nil, p.target, spec.Prerelease == "allow")
}
if err != nil {
return plannedDriver{}, redactRegistryError(err, p.registries, sourceURL)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking): GetPackage/GetWithConstraint errors (here and at :209) and newLockedArtifact/lockArtifacts errors (:213, :272) are built locally, so URL stripping is enough; value-based redaction still hides them. With a registry like https://r.example/?v=1, both driver mysql: no package for version 1.4.0 on linux_amd64 and driver mysql: cannot record artifact location: invalid hash "sha256:01ab" become "registry request failed; details are hidden…". Keeping redactRegistryError for the index fetch (:71, :201) and downloads only would avoid that.

@zeroshade

Copy link
Copy Markdown
Member

@eitsupi #567 has been squash-merged, so this branch now conflicts with main. Could you rebase onto main (dropping the #567 commits)? I'll merge once CI is green on the rebased head.

CheckInstalledPackage reports whether a runtime ID's current primary
registration is a dbc-owned generation installed from the expected archive.
Under the driver and registration namespace locks it re-reads the
registration and requires the receipt, registration fingerprint, installed
library bytes, archive digest, version and, when requested, signature
evidence to agree. Any other state reports no match so the caller can
reinstall; the check never creates or changes anything.
…identity

Lockfile v2 records each driver's version, its registry source and one
archive per target with a sha256 hash. The registry is identified by a
canonical URL without userinfo, query or fragment, and artifacts are stored
as registry-relative paths (or credential-free absolute URLs), so secrets in
configured registry URLs never reach the lockfile. On replay, the identity
is bound back to exactly one configured registry to rebuild the request URL.

The reader decodes v2 strictly and fails closed on unknown fields, duplicate
drivers or targets, malformed hashes and paths that could escape the
registry; v1 files keep using the existing reader. The writer sorts entries
and replaces the file atomically. Sync does not use v2 yet.
…ests

InspectPackageArchive extracts an archive into a private temporary
directory with the same safety checks as InstallPackage and reports the
archive and driver library digests, so callers can verify an archive before
changing any installation. CheckInstalledPackage can now also require the
installed library to have a given digest, compared within the same locked
check.
`dbc sync` now plans every driver before it changes anything: drivers
locked in a v2 lockfile are replayed from their recorded archive URL and
hash without contacting the registry index, a missing target is completed
from the same registry at the same version, and new drivers record every
target whose hash the registry publishes (the current target is measured
when no hash is published). A v1 lockfile is migrated: its version stays
pinned while it satisfies the constraint, and its library checksum is
verified on the same platform or dropped with a warning otherwise.

All needed archives are downloaded and verified first, then the new lock
is written atomically, and only then are drivers installed. An installed
driver is reused without downloading only when its receipt proves it came
from the locked archive; a driver installed with --no-verify is reinstalled
with signature verification by a normal sync. Archive and v1 checksum
mismatches are reported as checksum_failed, and registry credentials are
kept out of the lockfile, output and errors.

Download file names no longer include the URL query.

BREAKING CHANGE: `dbc sync` writes lockfile version 2, which older dbc
versions reject.
Credentials are chosen per URL host, including the port, but Go's default
redirect policy forwards the Authorization header to the same hostname on a
different port. The default HTTP clients now remove the header whenever a
redirect leaves the original host:port, so a registry credential is not
sent to another server, for example when replaying an archive URL from a
lockfile.
…esult

The install worker removed its archive in a deferred call after sending the
result, so when the last driver finished the program could exit first and
leave the temporary directory behind. Remove it as soon as InstallPackage
returns.
Skip tests that expect a reuse match where CheckInstalledPackage always
reports none (js), compare registration locations by the files they resolve
to (the check reads them from the symlink-resolved directory), and close
archives after InspectPackageArchive so Windows can remove temp dirs.
Use structured targets and typed artifact locations, retain optional archive sizes, and accept evidence and host requirement metadata for future resolvers. Keep registry identities and credential rebinding unchanged.

BREAKING CHANGE: Unreleased flat v2 lockfiles must be regenerated. Version 1 migration remains supported.
Check optional archive sizes only when downloading, and classify size mismatches as checksum_failed in JSON output.
CheckInstalledPackage reads the registration from its symlink-resolved
location, so on macOS (/var -> /private/var) the read-only fallback test saw
a different FilePath than GetDriver returned. Share the resolved-path
comparison already used by the strict reuse test.
@eitsupi

eitsupi commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review!
I've rebased onto main and dropped the #567 commits, so the branch now contains only the lockfile v2 changes on top of the squash-merged #567.

For the redaction nit, I opened #572 to handle it in a follow-up PR.

@zeroshade
zeroshade merged commit 12ec34a into columnar-tech:main Nov 10, 2026
22 checks passed
@eitsupi
eitsupi deleted the feat/lockfile-v2 branch November 10, 2026 16:16
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.

Record the hashes of all available binaries in the lock file

2 participants