Skip to main content

proto v0.60 - Scoped lockfiles, safe concurrent installs, and new WASM path APIs

· 8 min read
Miles Johnson
Founder, developer

Our lockfile implementation gets a major overhaul in this release, alongside concurrency-safe installs, simpler plugin configuration, and a friendlier path API for plugin authors.

proto has been steadily marching towards an official v1 release, and we're almost there! As part of this journey, we're making some breaking changes now — like the configuration and WASM API changes in this release — so that plugins, and their authors, have ample time to migrate before v1 lands.

If you're using a community WASM plugin, they will most likely need to update to the latest APIs and release a v0.60 compatible version. If you're a plugin author, please read the breaking changes below for the full migration list.

Scoped lockfiles

When we introduced lockfiles in v0.51, enabling the settings.unstable-lockfile setting would lock that directory and all of its subdirectories, similar to how package manager workspaces function. Versions defined in nested .prototools configs were absorbed into the top-most .protolock file, and defining another lockfile deeper in the tree was a hard error.

This model sounded nice on paper, but in practice it caused more friction than it solved — especially in monorepos, where nested projects define their own .prototools, but a lockfile at the root would claim them all. In this release, we've reworked the model entirely: a lockfile is now scoped to the config in which it was enabled, and only that config.

.prototools
[settings]
unstable-lockfile = true

In practice, this means the following:

  • A .protolock file only tracks tools with versions defined in its sibling .prototools config. Tools installed ad-hoc (without a configured version) are tracked by the closest config.
  • Versions defined in nested configs are no longer included in a parent's lockfile. To lock a nested config, enable the setting in that config as well.
  • Nested lockfiles no longer trigger an error, as each lockfile operates independently.

Lockfiles are still unstable, so if you run into any issues, please report them!

Lockfile-aware commands

The scoping rework is only half of the lockfile story. Until now, lockfiles were primarily the concern of proto install — created and updated during installs, but invisible everywhere else. With this release, lockfiles are now deeply integrated across the command line.

  • proto outdated and proto status now display a "Locked" column (when applicable), showing the version locked for each configured spec. Both commands also include a new locked_version field in their --json output.
  • proto outdated --update now updates matching lockfile records when writing new versions to their respective configs. Since these versions haven't been installed yet, checksums are removed from migrated records, and will be re-populated on the next install.
  • proto pin (and proto install --pin) now migrates matching lockfile records to the newly pinned spec, while proto unpin removes them entirely.
  • proto versions now labels versions that have a lockfile record with "locked", and includes a locked field in its --json output.
  • proto debug config now renders .protolock files alongside their sibling configs, and includes them in a locks field in its --json output.
  • proto clean no longer removes records when cleaning stale versions, as the version may still be required by a config, or in use on other machines.

And to round it out, proto diagnose now audits the health of your lockfiles, reporting warnings for records that are missing a resolved version, duplicate records for the same spec, and stale records that no longer match any configured version.

Safe concurrent installs

proto is increasingly being executed concurrently — task runners fan out processes that hit shims simultaneously, CI pipelines install tools in parallel jobs, and AI agents run many commands at once. If multiple of these processes tried to install the same tool and version at the same time, they would all download and unpack into the same directory, clobbering each other and sometimes leaving a corrupt install behind.

In this release, installs of the same tool and version are now serialized through a file lock. The first process acquires the lock and performs the actual install, while the remaining processes wait for it to finish, detect that the version was installed concurrently, and reuse the result. This works across any number of processes, no matter how they were spawned.

Inlined plugin locators

Third-party plugins have always been configured through the [plugins.tools] and [plugins.backends] tables, which keeps plugin locators separate from the settings of the tool itself. This separation works, but it always felt a bit disconnected.

In this release, we've added new plugin fields to the [tools.*] and [backends.*] tables, allowing the locator and its settings to be defined side-by-side. Instead of this:

.prototools
[plugins.tools]
example = "github://your-org/example"

[tools.example]
custom-setting = true

You can now write this:

.prototools
[tools.example]
plugin = "github://your-org/example"
custom-setting = true

Both styles are supported, so use whichever layout you prefer.

Pre-built Ruby binaries

Installing Ruby with proto has always meant building it from source — a slow process that requires a compiler toolchain and a handful of system dependencies. Starting with this release, the Ruby plugin will install pre-built binaries from the jdx/ruby project when one is available for your platform, and only fall back to building from source when one isn't. Installs that previously took minutes now finish in seconds.

Breaking changes

A new path API for WASM plugins

WASM plugins run in a sandboxed environment, where directories on the host file system (real paths) are mounted into the guest under fixed virtual paths. Until now, our VirtualPath type was modeled as an enum that tried to capture both worlds at once — it worked, but it was clunky to use, and didn't compose with the wider Rust ecosystem.

In this release, we've rebuilt this layer from the ground up:

  • VirtualPath is now a simple newtype wrapper around PathBuf, giving it access to all PathBuf methods, something that wasn't possible before.
  • A new RealPath type — also a newtype wrapper around PathBuf — represents real paths on the host file system, making it explicit in function signatures which side of the sandbox a path belongs to.
  • Converting between the two no longer requires calling a host function across the WASM boundary. The new convert_to_virtual_path and convert_to_real_path functions (and the VirtualPathExt and RealPathExt extension traits) perform the conversion directly in the guest, using the host-to-guest path mappings.

If you maintain a WASM plugin, refer to the breaking changes below for the full migration list.

// Before
use extism_pdk::*;
use warpgate_pdk::{VirtualPath, into_virtual_path, virtual_path};

#[host_fn]
extern "ExtismHost" {
fn from_virtual_path(path: String) -> String;
fn to_virtual_path(path: String) -> Json<VirtualPath>;
}

let path = into_virtual_path(value); // with func
let path = virtual_path!(buf, value); // or with macro
// After
use warpgate_pdk::{VirtualPath, VirtualPathExt};

let path = VirtualPath::create(value)?;

Split built-in plugin settings

The settings.builtin-plugins setting has been split into two new settings, builtin-tools and builtin-backends, allowing built-in tools and built-in backends to be toggled independently. Both accept the same values as before: true to enable all (the default), false to disable all, or a list of names to enable a select few.

.prototools
[settings]
builtin-tools = ["node", "bun"]
builtin-backends = false

The builtin-plugins setting is now deprecated, but will remain as an alias for builtin-tools for backwards compatibility.

Removed WASM path APIs

As part of the path rework above, the following breaking changes were made to the WASM API:

  • Removed the from_virtual_path and to_virtual_path host functions, the into_real_path and into_virtual_path wrapper functions, and the real_path! and virtual_path! macros. Use the new conversion utilities instead.
  • Removed the PluginUnresolvedContext.tool_dir and version fields.
  • Removed the LocateExecutablesOutput.exes_dir field, use exes_dirs instead.
  • Updated CommandInstruction.cwd to be a VirtualPath.
  • Updated ExecCommandInput.paths to be PathBufs.

Other changes

View the official release for a full list of changes.

  • Updated plugin function calls to run on a separate blocking thread, instead of blocking the main thread.
  • Checksums are now only removed from migrated lockfile records when the resolved version changes, instead of always.