Skip to content

Validate the AVF control socket path, and publish to crates.io from CI - #6

Merged
einarfd merged 3 commits into
mainfrom
avf-socket-path-and-crates-io
Aug 27, 2026
Merged

einarfd merged 3 commits into
mainfrom
avf-socket-path-and-crates-io

Conversation

@einarfd

@einarfd einarfd commented Aug 27, 2026 •

Copy link
Copy Markdown
Owner

Three unrelated changes that came out of the same review pass.

Validate the AVF control socket path length

The QEMU backend already rejects a QMP socket path that won't fit in
sockaddr_un.sun_path. The AVF backend had the same exposure and hits it
sooner — avf-control.sock is 8 bytes longer than qmp.sock, so the limit
lands at a shorter VM name.

The consequence is worse on the AVF side. agv tells the runner where to bind,
and every later stop, suspend, and status connects to that path. An
over-long one leaves a booted VM that nothing can control, with the failure
surfacing as an opaque RPC timeout well after start returned.

Extracted the existing check into vm::ensure_socket_path_fits rather than
copying the SocketAddr::from_pathname call, and called it from both backends
— QEMU where it already was, AVF at the top of start before the runner config
is written or the runner spawned. The unit test deliberately doesn't pin a byte
count; the limit is platform-defined (104 on macOS, 108 on Linux), which is why
the check delegates to from_pathname instead of hardcoding one.

README corrections

Two claims were wrong once AVF became the macOS default.

The crates.io entry sat alongside the install script as though they were
equivalent. cargo install can only lay down cargo's own bin targets, and
agv-avf-runner is a Swift binary, so on macOS Apple Silicon a cargo install
produces an agv whose default backend has no runner to spawn. That's a
property of cargo, not something to fix later.

The from-source paragraph then claimed that without the sibling runner
agv create "falls back to QEMU". No such fallback exists —
config::default_backend returns "avf" on macOS aarch64 unconditionally and
locate_avf_runner is only reached from LocalAvfBackend::start, so create
builds the VM and then fails to boot it. Now describes the real behaviour and
points at agv doctor and --backend qemu.

Publish to crates.io from the release workflow

crates.io was at 0.3.0 while install.sh served 0.4.1, because cargo publish
was a manual step nobody remembered. The README advertises cargo install agv,
so the drift was user-visible.

The new publish job runs last in the DAG, after release. Publishing is
irreversible (yank hides it, nothing deletes it), so it shouldn't happen until
all three build legs are green and the GitHub Release exists — a failed macOS
leg must not leave a crates.io version pointing at a release that was never cut.

Auth is crates.io Trusted Publishing (RFC 3691), not a stored API token.
GitHub proves the workflow's identity over OIDC and crates-io-auth-action
exchanges that for a token valid ~30 minutes, revoked by its post step when the
job ends. A long-lived registry secret would be a standing credential readable
by anything running in the job, with a blast radius of everyone who runs
cargo install agv. The auth step is gated on the same condition as the
publish, so a run that skips publishing never mints a token at all.

Two guards in the decide step:

  • Pre-release tags skip. crates.io would accept a semver pre-release and
    cargo install would correctly ignore it, but the version number is then
    burned forever for a build whose only audience is the tarball channel.
  • Already-published versions skip. Makes a workflow re-run on the same tag
    a no-op instead of a hard failure, so a transient upload error is retryable
    from the Actions UI.

The release docs move with it. CONTRIBUTING.md stated the opposite policy
outright ("cargo publish runs from a trusted local machine, not CI, on
purpose") and both it and release-check.sh listed a manual cargo publish as
the last step, which would now double-publish or fail on a version CI had
already uploaded.

Two details that bite if missed, noted in both files: naming any entry in
permissions drops the rest to none, so contents: read is spelled out
alongside id-token: write or checkout fails; and crates.io trusts the repo by
(owner, repo, workflow filename), so renaming release.yml breaks publishing
until the trusted publisher is updated to match.

Testing

  • cargo clippy --all-targets clean, full cargo test green (418 lib tests
    plus integration).
  • cargo publish --dry-run --locked packages clean (101 files, 371 KiB
    compressed).
  • Workflow YAML parses and the embedded shell script passes bash -n.
  • Version-exists check run against the live API: 0.3.0 → 200 (skip),
    0.4.1 → 404 (publish).

The publish path itself is untested until a real tag — cutting 0.4.2 is the way
to exercise it. crates.io will jump from 0.3.0 to that version; 0.4.0 and 0.4.1
stay unpublished. Cosmetic, since nothing depends on agv as a library.

🤖 Generated with Claude Code

einarfd and others added 3 commits August 27, 2026 22:04
The QEMU backend already rejects a QMP socket path that won't fit in
`sockaddr_un.sun_path` before handing it to QEMU. The AVF backend has
the same exposure and hits it sooner: `avf-control.sock` is 8 bytes
longer than `qmp.sock`, so the limit lands at a shorter VM name.

The consequence is worse on the AVF side. agv tells the runner where
to bind, and every later `stop`, `suspend`, and `status` connects to
that path. An over-long one leaves a booted VM that nothing can
control, with the failure surfacing as an opaque RPC timeout well
after start returned.

Extracted the existing check into `vm::ensure_socket_path_fits` rather
than copying the `SocketAddr::from_pathname` call, and called it from
both backends — QEMU where it already was, AVF at the top of `start`
before the runner config is written or the runner spawned.

The unit test doesn't pin a byte count: the limit is platform-defined
(104 on macOS, 108 on Linux), which is the reason the check delegates
to `from_pathname` instead of hardcoding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two claims in the install section were wrong once the AVF backend
became the macOS default.

The crates.io entry sat alongside the install script as though the two
were equivalent. They are not: `cargo install` can only lay down cargo's
own bin targets, and `agv-avf-runner` is a Swift binary, so on macOS
Apple Silicon a cargo install produces an `agv` whose default backend
has no runner to spawn. That is a property of cargo, not an oversight
to fix later, so the README now says which platform gets a complete
tool from which channel.

The from-source paragraph then claimed that without the sibling runner
"`agv create` falls back to QEMU". No such fallback exists.
`config::default_backend` returns "avf" on macOS aarch64
unconditionally, and `locate_avf_runner` is only reached from
`LocalAvfBackend::start` — so `agv create` builds the VM and then fails
to boot it. Someone following the old text would conclude their install
was broken in some other way. It now describes the real behaviour, and
points at `agv doctor` and `--backend qemu`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
crates.io was at 0.3.0 while install.sh served 0.4.1, because the
release workflow only built binaries and cut the GitHub release —
`cargo publish` was a manual step nobody remembered. The README
advertises `cargo install agv`, so the drift was user-visible.

The job runs last in the DAG, after `release`. Publishing a version is
irreversible (yank hides it, nothing deletes it), so it should not
happen until all three build legs are green and the GitHub release
exists; a failed macOS leg must not leave a crates.io version pointing
at a release that was never cut.

Auth is crates.io Trusted Publishing (RFC 3691) rather than a stored
API token. GitHub proves the workflow's identity over OIDC and
crates-io-auth-action exchanges that for a token valid ~30 minutes,
revoked by its post step when the job ends. A long-lived registry
secret would be a standing credential readable by anything running in
the job, with a blast radius of everyone who runs `cargo install agv`.
Nothing long-lived is stored, so there is nothing to leak or rotate.
The auth step is gated on the same condition as the publish, so a run
that skips publishing never mints a token at all.

Two guards in the decide step. Pre-release tags skip: crates.io would
accept a semver pre-release and `cargo install` would correctly ignore
it, but the version number is then burned forever for a build whose
only audience is the tarball channel. Already-published versions skip
too, which makes a workflow re-run on the same tag a no-op instead of
a hard failure, so a transient upload error is retryable from the
Actions UI.

Publishing only ships `agv`; cargo cannot carry the Swift runner.

The release docs move with it. CONTRIBUTING.md stated the opposite
policy outright — "`cargo publish` runs from a trusted local machine,
not CI, on purpose" — and both it and `release-check.sh` listed a
manual `cargo publish` as the last step, which would now double-publish
or fail on a version CI had already uploaded. The ordering is the part
worth writing down: the tag push is the point of no return, which makes
the dry-run in `release-check.sh` the last chance to catch a crate that
won't package.

Two details that bite if missed, noted in both files. Naming any entry
in `permissions` drops the rest to none, so `contents: read` is spelled
out alongside `id-token: write` or checkout fails. And crates.io trusts
the repo by (owner, repo, workflow filename), so renaming release.yml
breaks publishing until the trusted publisher is updated to match.

`cargo publish --locked` pins the published build to the committed
Cargo.lock. Check 4 of `release-check.sh` already fails a release whose
lock disagrees with Cargo.toml, so this asserts something the flow
guarantees rather than introducing a way to fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@einarfd
einarfd force-pushed the avf-socket-path-and-crates-io branch from 7b8e6b5 to ea3a350 Compare August 27, 2026 20:06
@einarfd
einarfd merged commit 303860d into main Aug 27, 2026
4 checks passed
@einarfd
einarfd deleted the avf-socket-path-and-crates-io branch August 27, 2026 21:00
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.

1 participant