Repository navigation
fix(msb): block unsafe 0.7 migration versions - #510
Conversation
…-0-7-0-0-7-2 # Conflicts: # docs/KNOWN_FAILURE_MODES.md # test/bats/70-msb-backend.bats
|
The upstream msb distribution situation is kind of a disaster. There's no way to get anything but the latest version through their |
basiliskus
left a comment
There was a problem hiding this comment.
Reviewed at f611522. Draft PR.
This change blocks msb 0.7.0-0.7.2 at runtime in acq_backend_prepare, and reworks install.sh to report duplicate msb binaries on PATH, install a "pinned safe" msb 0.6.18, and fail closed when a blocked binary still shadows it. Docs record the blocked range, the safe pin, and the PATH-shadowing failure mode.
Intent: The runtime block matches the stated intent and is well tested. The installer half does not: the "pinned safe version" is not pinned, and the new fail-closed gate fires on paths where nothing was installed.
Findings (3)
- Blocking:
install_msb_safe_versiondoes not installmsb 0.6.18. The URL it fetches is only the installer asset attached to thev0.6.18tag — that script has no version pin, no env override, and no argument parsing: it callsget_latest_version()againstapi.github.com/repos/superradcompany/microsandbox/releases/latestand installs whatever that returns. Thev0.6.18andv0.7.2assets are byte-identical (diff -q→ identical), somsb_install_script_urlis indistinguishable fromhttps://install.microsandbox.dev. During the blocked window this installs a blocked release and links it into~/.local/bin, after whichverify_active_msb_safedies blaming PATH shadowing — misdiagnosing a problem the installer just created. It also violates the project's own dependency rule: "Version pinning: Exact versions only, no floating ranges". The same non-pin is published as user guidance indocs/howto/msb.md:68anddocs/KNOWN_FAILURE_MODES.md:2240. A real pin needs the versioned bundle URL (releases/download/v0.6.18/microsandbox-<target>.tar.gz+checksums.sha256), not the tag's installer asset.
install.sh:242 - Blocking:
verify_active_msb_saferuns unconditionally afterinstall_msb_safe_version || INSTALL_MSB=0, so it fires even when no install happened. Two regressions against pre-change behavior:sh install.sh --dry-run --yeson a host with nomsbnow exits 1 witherror: msb 0.6.18 was installed, but no msb is active on PATH— nothing was installed, andREADME.mddocuments--dry-runas "show what it WOULD do, changing nothing" (old: exit 0). And when the download fails,INSTALL_MSB=0is set intending the "Skipping msb" degrade atinstall.sh:739-743, but the gate dies first, so a successfulacqinstall reports total failure (old: warn, exit 0). That degrade path is unreachable on the failure path. No test covers either: every--dry-runtest also passes--no-msb, and no test stubs a failingcurl.
install.sh:733 - Should fix: New
## 35.collides with the existing## 35. Re-attach Heal Loop Warns Every Time on sbx 0.38at line 1866, and lands out of sequence between39and40— it should be41.MD024is disabled in.markdownlint-cli2.yaml, so CI will not catch it; commit e43a647 in this branch's own history is a dedicated fix for exactly this collision class.
docs/KNOWN_FAILURE_MODES.md:2208
Below the bar: acq_backend_prepare's "msb not found" branch (acq.backends/msb.sh:774) still recommends bare install.microsandbox.dev / brew install with no caveat, unlike the too-old branch this PR updated — an internal inconsistency, though its bite has lapsed now that upstream v0.7.3 is published and passes the block.
Verdict: needs changes
Assisted-by: OpenCode [claude-opus-5] team-code-review
…ve verification Interim checkpoint of the msb blocked-version work. The installer side and the live verification are complete; docs, ADR-0031, and the bats installer stubs are still outstanding (7 tests in test/bats/05-install.bats still assert the removed upstream-installer path and currently fail). msb 0.7.3 shipped mid-change with the upstream cross-version compatibility fix (microsandbox#1634, closing microsandbox#1616), which is the reason the 0.7.0-0.7.2 range was blocked at all. That changes the recommended recovery from "roll back to 0.6.18" to "move forward to 0.7.3", so: - Add MSB_FIXED_VERSION=0.7.3 to install.sh and acq.backends/msb.sh, and offer `msb self update` FIRST for a blocked msb whose catalog was already migrated. The migration sets are additive, so the fixed line reads an already-migrated catalog with no rollback, no backup dance, no data-affecting step, and none of the refusals a downgrade can hit. The rollback to the pinned version remains as the fallback. The result is re-checked rather than assumed, because `msb self update` targets whatever is newest. - Drop the ">= 0.7.3 once available" phrasing throughout; it is available. Guard the wedge discovered while verifying the recovery. A `msb self downgrade` attempted by too-old an msb fails AND leaves an operation journal that makes every later msb command from every version refuse, with no upstream flag to abort or resume it. So: - recover_migrated_catalog now refuses to drive a rollback with a binary from older than the blocked line, making the wedge unreachable through acq. - msb_stale_downgrade_journal / clear_stale_downgrade_journal detect a pre-existing wedge and offer to remove only the operation directory, never the database, retained backups, or sandbox state. This runs before any msb probe, since a wedged install would otherwise make msb_catalog_ahead misread. Close the Windows gap. install.ps1 had no msb version guard at all, so Windows hosts could land on a blocked release via the unpinned one-line installer: - Add the version policy constants, a component-wise comparator (not [version], so a build suffix cannot throw mid-install), and blocked/too-old/unparseable handling that mirrors install.sh, including the forward-to-0.7.3 path. - Install the pinned msb from its release bundle verified against the release's own checksums.sha256, rather than piping the upstream installer into Invoke-Expression and trusting it to honor an environment variable we cannot verify. The Windows bundle is two files, so placing them directly is checkable. - Check for an in-use msb.exe before copying, since Windows cannot overwrite a running image, and fail with an actionable message instead of a sharing violation mid-copy. - Verify the active msb after every install path, including WinGet, whose package version is outside our control. - Extract Add-UserPath from Ensure-Path so the msb bin dir gets the same ask-before-changing-PATH treatment as the acq install dir. Add scripts/verify-msb-pin, which verifies all of the above against real msb binaries instead of mocks. It runs each check against a throwaway MSB_HOME *and* HOME -- both are required, because `msb self downgrade` writes command symlinks into $HOME/.local/bin and would otherwise replace the caller's real msb link. It boots no VM, so unlike the other verify-* scripts it needs no virtualization and can run inside a sandbox. Verified 24/24 on linux-aarch64 and darwin-arm64: - the upstream installer cannot pin (per-release install.sh assets are byte-identical and resolve releases/latest at run time) - the pinned bundle verifies against its published checksum and runs - 0.6.18 -> blocked 0.7.x -> 0.6.18 reproduces the reported stranding - 0.7.3 reads that already-migrated catalog directly - `self downgrade` driven BY the blocked binary restores it, retaining a backup - the same attempted by the older binary fails, wedges every msb command, and is cleared only by removing the journal directory - a keg-only formula's layout runs by path, with no LD_LIBRARY_PATH and no signature-invalidating binary edit, and can drive the recovery unlinked Also stop probing keg-only brew formulae in msb_candidate_paths: they are never symlinked into the prefix and so cannot shadow anything, and reporting them would turn a correct side-by-side install into a spurious "multiple msb binaries" warning. Refs: #510 Refs: GSA-TTS/homebrew-tap@475cce3
Interim checkpoint pushed (
|
…with upstream's migration guide Completes the msb blocked-version work checkpointed in 97226fa. That commit landed the installer and the live verification but left the ADR unwritten, the docs stale, and 7 tests asserting a code path it had removed. Upstream also published a v0.6->v0.7 migration guide in the meantime, which changes what this can claim and what it must reconcile against. ADR-0032 (renumbered from 0031, which PR #500 already owns) records the version policy, the pinned+checksummed install, the forward-first recovery, and the wedge guards. It cites upstream's guide as primary evidence where one now exists, and keeps the migration-prefix analysis as independent corroboration -- re-derived from upstream source at each tag rather than carried over: the applied migration lists are strict prefixes (0.6.18 has 25, 0.7.2 adds split_snapshot_identity and snapshot_groups, 0.7.3 adds migrate_secret_config), which is why the fixed line opens an already-migrated catalog with no rollback. Reconcile the journal guard with upstream's instruction. The guide says "Do not delete the catalog or edit migration history to bypass the refusal", and clear_stale_downgrade_journal removes a directory under $MSB_HOME/db. That is a different action and the code, the ADR, and the failure-mode doc now say so explicitly rather than leaving a reader to infer it: the journal is a lock whose `phase != complete` means an operation is in flight, msb's own downgrade path retires it on success, and removing it leaves a bit-identical catalog. What upstream warns against -- deleting the database or editing the applied-migrations table -- acq never does. Fix the 7 failing tests in test/bats/05-install.bats. They asserted the upstream-installer path that install_msb_pinned_tarball replaced, so they were testing a string that no longer exists rather than the behavior. They now build a stand-in release bundle plus a matching checksums.sha256 and drive the real install: platform-derived bundle name, the real libkrunfw filename shapes (.so.5.6.1 / .5.dylib) so the ABI-derivation code is exercised, and a curl stub that hard-errors on any unexpected URL. Three new cases cover the fail-closed paths that had none: a checksum mismatch, a release with no checksum entry for our asset, and a source-level assertion that no fetched artifact is handed to a shell. Six defects found reviewing the checkpoint: - MSB_ALREADY_SUPPORTED was assigned in two branches and read nowhere. - verify_active_msb_pinned reported "msb 0.6.18 was installed" after the forward path had installed 0.7.3. Renamed to verify_active_msb_supported and de-versioned its messages. - msb_report_candidates accumulated an unused `versions` string and probed --version on every candidate before the single-candidate early return. - scripts/verify-msb-pin was committed non-executable, so `./scripts/verify-msb-pin` -- the command install.sh and msb.sh both print -- failed with Permission denied. - verify-msb-pin gave a misleading failure on a small TMPDIR: it needs ~1.2GB, and exhausting the filesystem surfaced as `msb self downgrade` reporting "failed to unpack libkrunfw", indistinguishable from a real defect in the path under test. It now checks up front and says so. - The version-drift check reached its expected value through `eval`, tripping SC2154. It now passes values directly, and covers MSB_BLOCKED_VERSION_MAX as well. Also document the 0.7.3 interaction this PR is what makes reachable: upstream enabled --net-strict by default there, so a hostname allow rule requires inspectable request authority. acq's tiers are hostname-based and acq enables --tls-intercept by default, so the default path is correct -- but ACQ_MSB_NO_TLS_INTERCEPT=1 on 0.7.3+ now makes every allowed host unreachable rather than merely unsubstituted. verify-msb-pin gains check 5b for the snapshot-group refusal, which is the load-bearing reason the rollback cannot be treated as always-available: every 0.7 capture creates a group by default, and a group blocks the downgrade even when empty. Verified fail-closed too -- no backup written, no journal left, msb still usable. Verification (linux-aarch64): - bats: 528/528 pass (was 521/528) - shellcheck --severity=warning, all shell + .bats files: clean - pre-commit run --all-files: all 11 hooks pass - PSScriptAnalyzer on install.ps1: no new findings vs origin/main; the PSAvoidUsingInvokeExpression finding on main is gone with the piped installer - pwsh: install.ps1 parses, -Help and -DryRun run, Compare-MsbVersion correct on 10 cases, Get-MsbVersion fails closed on nonzero exit and on a stale $LASTEXITCODE, the checksum regex matches the real published manifest and rejects decoys, and Expand-Archive handles the real Windows bundle (it is a zip: msb.exe + libkrunfw.dll) - ./scripts/verify-msb-pin: 29/29 live against real 0.6.18/0.7.2/0.7.3 binaries Windows remains a preview path, still unexercised on a real Windows host. Refs: #510
`--dry-run` installs nothing, so verifying the active msb afterwards checks a
state the run deliberately did not create. On a host with no msb, a plain
`sh install.sh --dry-run --yes` therefore printed every step as OK and then
died on the final check:
error: msb was installed, but no msb is active on PATH.
That is a false alarm on the exact command a cautious user runs first, and it
exits non-zero, so it also fails any script that smoke-tests the installer.
install.ps1's Assert-MsbSupported has always had this guard (`if ($DryRun) { return }`);
the shell side did not. Found while writing reviewer instructions for the PR --
the first command those instructions suggest is the one that failed.
Every other dry-run test in the suite passes --no-msb, which is why none of them
caught it. The new regression test deliberately does not, and also asserts that a
dry run writes nothing.
Refs: #510
Reviewer finding (F2, second half): a FAILED msb download used to die in verify_active_msb_supported before reaching the 'Skipping msb' degrade, turning a successful acq install into a total-failure exit -- and no test stubbed a failing curl, so nothing caught it. The gate is already conditional on install_msb_pinned succeeding (fixed earlier in this branch); this adds the missing coverage: a curl that fails the bundle download must leave acq installed, print 'Skipping msb', write no msb, and exit 0. Refs: #510
|
@basiliskus — thanks for the review at F1 (blocking) — the pin is now a real pinYou were right that fetching the tag's Rewritten to your prescription: it now fetches On the doc guidance you flagged in F2 (blocking) — the gate no longer fires when nothing was installedBoth halves fixed, and both now tested — your "no test stubs a failing curl" point was exactly right, nothing was covering either path.
F3 (should fix) — heading collisionThe section is renumbered to Below the bar — msb-not-found guidance
One correction I owe youI initially described the downgrade wedge as "refuses EVERY command." Verifying against real binaries, Verification (linux-aarch64)
Two things still worth a skeptical eye, both called out in the PR description: the journal-removal argument (the one place Assisted-by: OpenCode [claude-opus-4] |
Now verified on my home Windows machine:
|
|
Reviewed at 437d07f using the This change now does the main install-side work it set out to do: the shell installer no longer uses the unpinnable upstream installer asset, checksum verification gates writes, and the dry-run / failed-download regressions from my earlier review are fixed. I still found one user-visible regression in the new runtime guard path, plus a few smaller accuracy issues in the new version-policy docs. Findings (4)
Prior review resolution status F1, unpinned “pinned” install: resolved. F2, dry-run/no-msb and failed-download gate: resolved. F3, heading collision: resolved for the known-failure heading; partially resolved overall because ADR-0032 has the stale §35 link above. Below-the-bar note from the earlier review: resolved. Below the bar The journal-removal argument looks acceptable to me: I am not treating the Windows Validation
Verdict: needs changes Assisted-by: OpenCode [gpt_5_5_default_v2] |
|
@basiliskus — thanks for the follow-up review. Addressed the four findings and the below-the-bar wording cleanup in cf672cb. What changed:
Verification:
Note: the commit is intentionally unsigned because this worktree has SSH commit signing enabled, but the signing agent socket is unavailable ( Assisted-by: OpenCode [gpt_5_5_default_v2] |
basiliskus
left a comment
There was a problem hiding this comment.
Reviewed at cf672cb4e7c0b898669dee56a8aca0591163c06a using the change-review skill.
Thanks — the runtime fixes hold up. Unparseable and non-final msb --version output now fails closed with an actionable diagnostic instead of exiting mute, msb 0.7.3 linux-arm64 still parses as final 0.7.3 (the platform suffix is after a space, so the new capture group can't reach it), and the two new tests genuinely fail at the prior head. One new gap, introduced by this round's fix.
-
Major, confirmed: the installers still accept prerelease/build-suffixed versions that
acqitself now refuses.
install.sh:271-272,install.ps1:303The runtime parser was widened to capture a
[+-]suffix and_acq_msb_version_is_finalrejects it. Neither installer changed:install.shstill greps[0-9]+\.[0-9]+(\.[0-9]+)?, andinstall.ps1'sGet-MsbVersionstill matches\d+\.\d+(\.\d+)?. Same payloads, two answers:msb --version installer parses runtime parses runtime accepts? 0.7.3-rc1 -> 0.7.3 0.7.3-rc1 NO 0.7.3+build.1 -> 0.7.3 0.7.3+build.1 NO 0.7.3 linux-arm64 -> 0.7.3 0.7.3 YES 0.7.3 -> 0.7.3 0.7.3 YESConcrete failure: a host with
msb 0.7.3-rc1runsinstall.sh.active_msb_versionis set atinstall.sh:1208to0.7.3, which is parseable, not too old, and not blocked — so theelifchain falls through took " msb is already installed"atinstall.sh:1253-1254and changes nothing. The nextacq lsrefuses the exact binary the installer just blessed. That's a worse state than the original bug: the tool whose job is to repair the install reports no problem.This also contradicts ADR-0032's claim that
install.ps1carries the same blocked/too-old/unparseable handling as the POSIX path. Theinstall.ps1half is code-traced only —pwshisn't available here. -
Minor, confirmed: the "every msb command" wording cleanup is partial.
install.ps1:529,docs/KNOWN_FAILURE_MODES.md:2357,scripts/verify-msb-pin:40,:422,:438The POSIX runtime and install text now say "catalog access" / "catalog-opening commands", but five copies still claim the interrupted downgrade blocks every command — including one users actually see, the thrown error at
install.ps1:529, and the KFM symptom heading "3. Every msb command refuses".msb --versionstill works, which is the whole reason the narrower phrasing matters.
Below the bar: the PR body still says "No Windows host has run this code." (line 62) and "Still untested on a real Windows host" (line 215), while the durable docs now say it was smoke-tested on a real host and your comment reports the same. The docs are right; please update the body so reviewers aren't working from stale risk information.
Prior findings: mute exit — resolved. Prerelease accepted as final — partially resolved (runtime rejects, installers don't). ADR-0032 §35→§42 — resolved, now matches docs/KNOWN_FAILURE_MODES.md:2334. Windows durable-doc status — resolved. "Every msb command" wording — partially resolved, see finding 2.
Validation. Ran: full suite ./scripts/test-acq-bats → 534 ok / 0 not ok / 2 skips (both skips are pwsh not available, so the PowerShell parse tests never execute locally or in CI); the two new 70-msb-backend.bats tests against the prior head 437d07f, where both fail with the old acq: warning: could not determine msb version (need >= 0.6.9); continuing. / RC=0 — not vacuous; stubbed msb dev-build → acq ls/stop/rm all exit 1 with the new diagnostic; 0.7.3-rc1 → exit 1, 0.7.3 and 0.7.4 → exit 0; bash -n on acq, acq.backends/msb.sh, install.sh, scripts/verify-msb-pin; git diff --check; gh pr checks 510 → all 7 pass.
Not run: ShellCheck — not installed in this environment, so it did not run (CI's pre-commit covers it and is green). ./scripts/verify-msb-pin and ./scripts/verify-backends need a sandbox-capable host. install.ps1 behavior is code-traced only; the real-Windows smoke test remains author-reported from my side.
Verdict: request changes — the installer/runtime finality mismatch should be closed in both install.sh and install.ps1 before this merges, since the two halves now disagree about what a supported version is.
Assisted-by: OpenCode [gpt_5_5_default_v2] change-review
|
@basiliskus — addressed the latest review feedback and pushed Fixed:
Verification: local Please re-review when you have a chance. Assisted-by: OpenCode [gpt_5_5_default_v2] |
|
Reviewed at The round-3 finding is fixed and I verified it by building the agreement table rather than reading the diff — the installers and the runtime now reach the same verdict on every payload I could construct, including the platform-suffix case I was most worried about regressing. One new issue in the same family, on a different axis.
Round-3 finding: resolved. Installer and runtime agreement, measured payload by payload: The platform-suffix row is the one I flagged as the regression risk when the regex was widened to capture Post- "Every msb command" wording: resolved. All five copies from round 3 now say catalog-opening/state commands — Below the bar: Validation. Ran: full suite Not run: One correction to my own evidence, since it affects how you read the table: my first pass showed Verdict: request changes — item 1 only. It is a one-line fix in each POSIX path (capture the status, or drop the Assisted-by: OpenCode [gpt_5_5_default_v2] change-review |
|
@basiliskus — addressed the latest finding and the below-the-bar PowerShell test note in Fixed:
Verification:
Also ran an adversarial sub-agent review before push. It found only the missing Bats-level PowerShell output assertion, which is now fixed. Note: the commit is intentionally unsigned because this worktree has SSH commit signing enabled, but the signing agent socket is unavailable ( Assisted-by: OpenCode [gpt_5_5_default_v2] |
…-0-7-0-0-7-2-2 # Conflicts: # docs/BACKEND_GUIDE.md # docs/KNOWN_FAILURE_MODES.md
|
Merged Conflict resolution:
Verification:
Note: the merge commit is intentionally unsigned because this worktree still has SSH commit signing enabled but the signing-agent socket is unavailable ( Assisted-by: OpenCode [gpt_5_5_default_v2] |
basiliskus
left a comment
There was a problem hiding this comment.
Reviewed at 346993d7b0f49754036c7060bda934a5810421ba.
The finding is fixed, and I reproduced the exact case rather than reading the patch. Clearing my CHANGES_REQUESTED.
stub prints "msb 0.7.3" then exits 42:
at 73b919b: acq ls -> exit=0, output "sb1" (accepted)
at 346993d: acq ls -> exit=1, "error: acq could not determine a
supported final msb version."
same stub exiting 0:
at 346993d: acq ls -> exit=0, output "sb1" (still accepted)
The fix is the right shape in both places — capture the status and bail rather than papering over it with || true:
# acq.backends/msb.sh
if ! raw=$(_acq_msb_cli --version 2>/dev/null); then
return 0
fi
# install.sh
_raw=$("$1" --version 2>/dev/null) || return 0Returning empty on a failed probe routes into the existing unparseable path, so the diagnostic is the one users already get rather than a new message. And POSIX now matches install.ps1, which already rejected non-zero $LASTEXITCODE — the three implementations agree again, which was the point.
Full agreement matrix re-measured at this head, installer parser against live acq ls:
msb --version output install.sh acq runtime
msb 0.7.3 (rc=0) accept 0.7.3 accept
msb 0.7.3 (rc=42) reject(unparseable) reject(exit 1)
msb 0.7.3-rc1 reject(non-final) reject(exit 1)
msb 0.7.3+build.1 reject(non-final) reject(exit 1)
msb 0.7.4 accept 0.7.4 accept
msb 0.7.3 linux-arm64 accept 0.7.3 accept
msb dev-build reject(unparseable) reject(exit 1)
<empty> reject(unparseable) reject(exit 1)
msb 0.6.9 accept 0.6.9 accept
Nine rows, no disagreement in either direction. The platform-suffix row still resolves to final 0.7.3, so widening the regex to capture [-+] has not regressed into a false rejection.
The new tests are not vacuous. Copied onto the prior head 73b919b and run there:
not ok msb: failed version probe fails closed even with parseable output
not ok install: failed active msb version probe is replaced with the pinned version
ok (skipped) windows PowerShell: msb version parser preserves suffix policy # pwsh not available
Both executable assertions fail without the fix. The installer test is the better of the two — it asserts the consequence (Active msb is 0.6.18, the pinned binary actually in place) rather than just an error string, so it would survive a reworded diagnostic.
You also took the below-the-bar note: test/bats/06-windows-install.bats now asserts the AST-extracted Get-MsbVersion returns empty for a non-zero probe, so the PowerShell and POSIX halves are pinned to the same policy in both directions. It still skips here — pwsh is absent — so that assertion is unexecuted by me and will only ever run where pwsh exists.
The §42 collision was caught and resolved correctly. main added a host-port-publishing §42 while this branch had an msb version-policy §42. Renumbered to §43, and I checked every durable reference agrees: docs/adr/0032:339, docs/BACKEND_GUIDE.md:170, docs/howto/acq.md:326, docs/howto/msb.md:97 all say §43, and KNOWN_FAILURE_MODES.md:2380 is the §43 heading. No stragglers — which is the failure mode that bit this PR back in round 2, so worth noting it did not recur.
Validation. Ran at 346993d: full suite ./scripts/test-acq-bats → 622 ok / 0 not ok / 6 skips (3 pwsh not available, 3 sbx not on PATH — grammar acceptance UNVERIFIED, 1 msb authorize); the nine-row matrix above through live acq ls; the prior-head comparison for the two new tests; bash -n over acq, the adapters, install.sh, scripts/verify-msb-pin; the repo ShellCheck batch per AGENTS.md (clean, and shellcheck is genuinely installed in my environment so that is a real pass); npm run lint:md → 52 files, 0 issues; git diff --check clean; gh pr checks 510 → all 7 pass.
Not independently verified, and worth stating plainly on a PR whose whole subject is refusing bad installs: install.ps1 behaviour is a code trace only, since pwsh is absent here and its three tests skip. ./scripts/verify-msb-pin and ./scripts/verify-backends need a sandbox-capable host. The real-Windows smoke test remains your report, not my observation. Everything I ran was stubbed — no real msb binary was involved at any point, so the live recovery path through an actually-migrated catalog is unexercised by me.
Four rounds is a lot for one PR, and each round's finding was real and distinct — unpinned install, a mute failure, an installer/runtime suffix split, and now an exit-status split. The version-policy surface turned out to have more edges than it looks like from the outside, and the matrix above is the artifact I would keep.
Verdict: approve.
Assisted-by: OpenCode [gpt_5_5_default_v2] change-review
🤖 I have created a release *beep* *boop* --- ## [4.0.0](v3.1.0...v4.0.0) (2026-10-06) ### ⚠ BREAKING CHANGES * **kits:** acq no longer installs podman/OCI tooling by default. Users who need docker, docker compose, or rootless podman when the base image doesn't provide it must opt into the oci-engine kit with `acq configure`, `--kit`, or `ACQ_EXTRA_KITS`. * **kits:** acq no longer performs native OpenCode npm fallback installation. OpenCode is supplied by the pinned opencode agent kit from agentic-coding-patterns v1.12.0, currently delivering OpenCode v2.0.22. If the kit is unavailable or invalid, acq fails closed instead of installing opencode-ai from npm. * **kits:** existing acq sandboxes with adapter-installed podman are not migrated in place. Recreate affected sandboxes or add the oci-engine kit before relying on OCI behavior after stop/start. ### Features * **acq:** choose the host port per sandbox, and let --publish pin it ([#526](#526)) ([410f332](410f332)) * **acq:** export ACQ_WORKSPACE and ACQ_CLONE into the guest on both backends ([#457](#457)) ([e0b9a3b](e0b9a3b)) * **acq:** interactive `acq configure` UI for extra kits + token-scoping defaults ([#500](#500)) ([13d9d3a](13d9d3a)) * **kits:** adopt pinned agent and capability kits ([#474](#474)) ([6a67063](6a67063)) * **secrets:** add Windows-native DPAPI secret backend ([#464](#464)) ([907f404](907f404)) * **windows:** add acq preview install path ([#463](#463)) ([d6d76f4](d6d76f4)) ### Bug Fixes * **acq:** use sandbox workspace for github scoping ([#476](#476)) ([1610835](1610835)) * **ci:** bump js-yaml to 4.3.2 to clear a high-severity CPU-DoS advisory ([#454](#454)) ([3217b49](3217b49)) * **ci:** bump smol-toml to 1.7.1 to clear an infinite-loop DoS advisory ([#459](#459)) ([c2291bf](c2291bf)) * **ci:** centralize markdownlint coverage ([#548](#548)) ([a27c31e](a27c31e)) * **ci:** pin markdownlint-cli2 once, clear GHSA-253c, unify file selection ([#527](#527)) ([e712685](e712685)) * **ci:** unblock unrelated PRs from the bash32 required-check deadlock ([#513](#513)) ([187aa57](187aa57)) * **install:** keep curl | sh stdin intact and tap msb formula first ([#462](#462)) ([c808d74](c808d74)) * **install:** trust the microsandbox-acq dependency before brew install ([#534](#534)) ([ffac0fa](ffac0fa)) * **kits:** land collapsed kit stack implementation ([#549](#549)) ([bcd5b9c](bcd5b9c)) * **msb:** allow Playwright browser downloads ([#477](#477)) ([0c399c5](0c399c5)) * **msb:** block unsafe 0.7 migration versions ([#510](#510)) ([3634046](3634046)) * **msb:** carry every origin URL, keep the scratch private, fail closed on a copy miss ([#488](#488)) ([babcd49](babcd49)) * **msb:** carry the source checkout's git identity into the --clone scratch ([#443](#443)) ([320d784](320d784)) * **msb:** follow the host resolvers and disable DNS rebind protection in the guest ([#444](#444)) ([d2f4bdc](d2f4bdc)) * **msb:** point the --clone scratch's origin at the source checkout's remote ([#455](#455)) ([7f48b94](7f48b94)) * **msb:** refresh ssh authorization for port publish ([#446](#446)) ([72a6bd5](72a6bd5)) * **msb:** session parity for exec/attach/shell (cwd, terminal identity, login shell) ([#427](#427)) ([6261eda](6261eda)) * **msb:** skip the recursive home chown when the image baked the agent user ([#432](#432)) ([d8f66a4](d8f66a4)) * **msb:** thread the merged kit env onto kit lifecycle commands ([#519](#519)) ([2248a92](2248a92)) * **msb:** translate kit wildcard network rules ([#451](#451)) ([e22bbc3](e22bbc3)) * **msb:** verify post-hoc port forward listener ([#530](#530)) ([8ca50e1](8ca50e1)) * **msb:** warn on broadened kit wildcard rules ([#460](#460)) ([6ded3ce](6ded3ce)) * **msb:** warn when ssh-agent route is missing ([#448](#448)) ([3a767a4](3a767a4)) * **sbx:** emit valid setup.install shape for the git-identity mixin ([#492](#492)) ([8e6160c](8e6160c)) * **sbx:** raise the version floor to 0.39.0, where sbx create --env first exists ([#491](#491)) ([30f9ecf](30f9ecf)) * **sbx:** wait for startup barrier before attach ([#512](#512)) ([33c83c5](33c83c5)) * **secret:** reject multiline test secret values ([#447](#447)) ([f2da950](f2da950)) * **secrets:** propagate rotation across backends ([#509](#509)) ([9641958](9641958)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Bret Mogilefsky <[email protected]>
Summary for reviewers
In one line:
msb0.7.0-0.7.2 silently destroy the ability to read your own sandboxes, and installingmsbthe documented way is enough to land there. This PR makesacqrefuse those versions, install a known-good one verifiably, and recover a host that is already stuck.Three things are worth knowing before reading the diff:
msb self update,brew upgrade, andcurl … install.microsandbox.dev | shalike. Worse, each release publishes a byte-identical copy ofinstall.shas a release asset, so a URL like.../releases/download/v0.6.18/install.shlooks like a version pin and installs whatever is newest. An earlier revision of this branch used exactly that URL and was wrong to. That is whyacqnow places the artifact itself and verifies it against the release's published checksum.acqoffersmsb self updatefirst, and only falls back to rolling back to the 0.6.18 pin. Upstream's migration guide, published while this PR was open, independently recommends the same order.msb self downgraderun by too old a binary doesn't just fail — it leaves a journal after which every catalog-openingmsbcommand from every version refuses, with no abort flag.acqnow refuses to attempt that, and can clear the journal if you already hit it.Everything above was verified by running real 0.6.18 / 0.7.2 / 0.7.3 binaries against throwaway state, not by reading release notes — see
scripts/verify-msb-pin(29/29) below.Scope:
install.sh,install.ps1, themsbadapter's version gate, docs, tests, and one new ADR. No sandbox-provisioning behavior changes. The one genuinely risky item is the journal removal, called out under Reviewer focus.Trying it before this merges
The installer is a single self-contained
shscript with no dependency on the rest of the repo, so you can run this branch's copy directly against the releasedacq. Nothing below needs the PR merged.Everything is consent-gated and
--dry-runchanges nothing, so start there.macOS / Linux
If you already have a blocked msb (0.7.0-0.7.2) installed, that is the interesting case and the one worth a volunteer. Run step 2 and it will detect whether your catalog was already migrated, then offer the forward path first. Back up
~/.microsandboxfirst if you have sandboxes you care about — the recovery paths are the parts least exercised outside the verification script:To verify the underlying claims yourself rather than taking the PR's word for it, the live verifier needs only
curl,tar, and ~1.2GB of scratch. It boots no VM, touches neither your real~/.microsandboxnor your~/.local/bin, and needs no virtualization:Add
--installto have it place the verified pinned msb on your host for real; it prompts first and declines by default with no terminal.Windows (preview)
The Windows-specific logic has now had a real-host smoke test for pinned msb install, blocked-version forward recovery, in-use
msb.exehandling, version parsing, and fail-closed behavior. Broader Windows coverage is still thinner than the POSIX path, so additional Windows review remains useful.Requires Windows Hypervisor Platform already enabled — the installer checks but deliberately never enables features, elevates, or reboots.
What is most worth reporting back, in rough priority order:
msb --versionthen report0.6.18?msb self update?msb.exerunning (start a sandbox, leave it up), does the install fail with the actionable in-use message rather than a mid-copy sharing violation?-NoPathcorrectly decline?-DryRunand-Helpare safe to run anywhere, including on macOS/Linuxpwsh, and are the cheapest possible smoke test.What this fixes
msb(microsandbox) 0.7.0-0.7.2 migrate 0.6.x sandbox state one-way into a format the 0.6.x line cannot read. Two users hit it, and the sequence needs no mistake on their part — installingmsbthe documented way is enough, becausemsb self update,brew upgrade, andcurl -fsSL https://install.microsandbox.dev | shall resolve to the newest release:Every 0.6.x
msbon that host then refuses, including read-onlymsb list. The state at risk is the user's work.What changed
Runtime guard.
acqrefuses msb 0.7.0-0.7.2, accepts>= 0.6.9and>= 0.7.3. Split intoacq_backend_check_version(presence + policy; onemsb --version) andacq_backend_prepare(that plus themsb doctorprobe), so verbs that only touch existing state (acq ls,stop,rm, …) are guarded too. They previously reachedmsbunguarded and surfaced the raw upstream error — exactly when a stranded user most needs acq's explanation.Pinned, verified install.
install.shinstalls msb 0.6.18 from the release bundle verified against that release's ownchecksums.sha256. This exists because neither upstream channel can express a version: the one-liner takes no version argument and readsreleases/latest, and every release publishes a byte-identical copy ofinstall.shas an asset — so.../releases/download/v0.6.18/install.shlooks like a pin and installs whatever is newest. An earlier revision of this branch used exactly that URL and was wrong to. Thelibkrunfwname and ABI are derived from the artifact; hardcoding it is the live bug in upstream's own formula (libkrunfw.so.5.2.1, stale since 0.6.8).Forward-first recovery. For an already-migrated catalog, acq offers
msb self update(to 0.7.3) before the rollback, and re-checks the result rather than assuming it. Upstream's new migration guide independently agrees: "upgrade to v0.7.3 and retry first. A v0.7.2 database can upgrade directly; no downgrade to v0.6 is needed."Wedge guards.
msb self downgraderun by too old a binary leaves an operation journal, after which every catalog-opening msb command from every version refuses, with no abort flag.recover_migrated_catalogrefuses to drive a rollback from older than the blocked line (making the wedge unreachable through acq) and points at the keg-only formulae — which is also upstream's documented remedy.clear_stale_downgrade_journaldetects a pre-existing wedge and removes only that operation directory.Windows.
install.ps1had no msb version guard and installed via unpinnedirm … | iex. It now mirrorsinstall.sh, with a component-wise comparator (not[version], so a build suffix cannot throw mid-install) and an in-use-msb.execheck before copying.ADR-0032 records all of it. It partly supersedes ADR-0026, whose "one remaining unpinned leg" is no longer unpinned.
Reconciling with upstream's "do not delete the catalog"
Upstream's guide says "Do not delete the catalog or edit migration history to bypass the refusal."
clear_stale_downgrade_journalremoves a directory under$MSB_HOME/db, so this deserves to be stated rather than inferred:$MSB_HOME/db/*.db)db/self-downgrade/<id>/— one incomplete operation's journalThe journal is a lock, not schema state:
phase != completemeans an operation is in flight, and msb's own downgrade path retires it on success. Removing it abandons an operation that never began mutating the database; the catalog left behind is bit-identical and no schema check is bypassed. What upstream warns against — deleting the database, or editing the applied-migrations table, to force an older binary to accept a newer catalog — acq never does.Verification
Live, on linux-aarch64.
verify-msb-pinruns real 0.6.18 / 0.7.2 / 0.7.3 binaries against throwaway state — a mocked pass would prove none of these claims. It isolatesMSB_HOMEandHOME, the latter becausemsb self downgradewrites symlinks into$HOME/.local/binand would otherwise replace the caller's realmsblink (observed during development). It boots no VM, so it needs no virtualization and runs inside a sandbox../scripts/test-acq-batspwshand live sbx grammar tests skip when unavailableshellcheck --severity=warning(all shell +.bats)pre-commit run --all-filesPSScriptAnalyzeroninstall.ps1main;main'sPSAvoidUsingInvokeExpressionis gonepwshbehavioral probes./scripts/verify-msb-pinverify-msb-pin— 29/29Windows probes under
pwsh7.5.4 (not a Windows host)Functions extracted from
install.ps1via its own AST and exercised directly:Compare-MsbVersioncorrect on 10 cases including0.7vs0.7.0,0.7.10vs0.7.2, and0.7.2-rc1.Test-MsbVersionBlockedblocks exactly0.7.0-0.7.2(and0.7,0.07.2); does not block0.7.3,0.7.10,0.8,1.0.0.Get-MsbVersionfails closed: empty on nonzero exit even when a version is printed, empty on unparseable output, empty (no throw) on a nonexistent path, and unaffected by a stale$LASTEXITCODEfrom a prior command.checksums.sha256regex matches the real published manifest formicrosandbox-windows-x86_64.zipand rejects decoy lines (not-<bundle>,<bundle>.sig).Expand-Archivehandles the real Windows bundle — confirmed it is a zip containing exactlymsb.exe+libkrunfw.dll, and the published checksum verifies.Also smoke-tested on a real Windows host for pinned install, blocked-version recovery, in-use
msb.exehandling, version parsing, and fail-closed behavior. Treat as preview until it has broader user coverage.Defects found reviewing the checkpoint
All fixed in
28265b8:MSB_ALREADY_SUPPORTEDwas assigned in two branches and read nowhere.verify_active_msb_pinnedreported "msb 0.6.18 was installed" after the forward path installed 0.7.3. Renamedverify_active_msb_supported, messages de-versioned.msb_report_candidatesbuilt an unusedversionsstring and probed--versionon every candidate before its single-candidate early return.scripts/verify-msb-pinwas committed non-executable (100644), so./scripts/verify-msb-pin— the commandinstall.shandmsb.shboth print — failed with Permission denied.verify-msb-pinfailed misleadingly on a smallTMPDIR. It needs ~1.2GB; on a 512MB/tmptmpfs, exhaustion surfaced asmsb self downgradereporting "failed to unpack libkrunfw" — indistinguishable from a real defect in the path under test. Now checked up front.eval(SC2154). Now passes values directly, and also coversMSB_BLOCKED_VERSION_MAX.--dry-runfailed on a host with no msb (fbd20a3).verify_active_msb_supporteddid not exempt dry runs, so it verified a state the run deliberately never created: every step printed OK, thenerror: msb was installed, but no msb is active on PATHand a non-zero exit.install.ps1has always had this guard; the shell side did not. Found while writing the reviewer instructions above — the first command they suggest is the one that failed. Every other dry-run test in the suite passes--no-msb, which is why none of them caught it; the new regression test deliberately does not, and also asserts a dry run writes nothing.Two corrections to claims the checkpoint made:
msb --versionis not wedged by the journal — only commands that open the catalog are. The ADR's "refuses EVERY command" was too strong and is narrowed. The guard ordering inreplace_active_msb(clear the journal beforemsb_catalog_ahead) is still required and unchanged.mainhas since added §41 (fix(ci): unblock unrelated PRs from the bash32 required-check deadlock #513).git merge-treeconfirms a clean merge.Also newly documented
msb 0.7.3 enables
--net-strictby default, so a hostname allow rule requires inspectable request authority. acq's balanced/strict tiers are hostname-based and acq enables--tls-interceptby default, so the default path is correct on 0.7.3. The combination that breaks isACQ_MSB_NO_TLS_INTERCEPT=1on 0.7.3+: every allowed host becomes unreachable rather than merely losing secret substitution. This PR is what makes 0.7.3 reachable, so it is this PR that exposes it.Rollback
Revert the two commits (
97226fa,28265b8).acqreturns to accepting the blocked range, andinstall.shreturns to the upstream installer — i.e. back to the reported failure. No migration or on-disk state is created by this change, so there is nothing to undo beyond the code.Release-cut note for reviewers:
Formula/acq.rbin the tap pins the release tarball, so Homebrew users do not get the fixedinstall.shuntil release-please cuts a release. Not a blocker for merge, but the fix is not in users' hands at merge time.Security impact
PSAvoidUsingInvokeExpressiononmainis gone). Artifacts are verified against the release's publishedchecksums.sha256before anything is written, and ABI validation completes before the first write so a malformed bundle cannot half-install a runtime.rm -rfreaches only paths derived from$MSB_HOMEglobbing plus a$$-suffixed scratch dir; the journal removal is consent-gated and prints the exact path.Reviewer focus
install.shclear_stale_downgrade_journal, ADR-0032 §4). It is the one place acq deletes something under$MSB_HOME/db, against an upstream instruction that superficially reads as covering it. If the reasoning is wrong, that is the finding that matters most.recover_migrated_catalog's ordering guard — it is what makes the wedge unreachable, and it is a negation over a version range.install.ps1, especially beyond the real-host smoke-tested paths; Windows coverage remains newer and narrower than POSIX coverage.install.sh,install.ps1,acq.backends/msb.sh,scripts/verify-msb-pin). Only the Windows copy is drift-tested, againstinstall.sh.🤖 AI-assisted: written with OpenCode (claude-opus-4). All verification output above is from real runs, not reconstructed.