Skip to content

feat: two-gem split — SDK-only microsandbox-rb + microsandbox-rb-binaries runtime gem - #58

Merged
ya-luotao merged 6 commits into
mainfrom
feature/binaries-gem
Aug 23, 2026
Merged

feat: two-gem split — SDK-only microsandbox-rb + microsandbox-rb-binaries runtime gem#58
ya-luotao merged 6 commits into
mainfrom
feature/binaries-gem

Conversation

@ya-luotao

Copy link
Copy Markdown
Owner

What

Clean-start implementation of the two-gem split agreed in upstream superradcompany/microsandbox#1305 — one thing only: microsandbox-rb becomes SDK-only, and a new microsandbox-rb-binaries gem carries the runtime. Supersedes the evidence-gathering prototype in #55 (closed in favour of this).

  • microsandbox-rb is now SDK-only. The core crate is pulled with default-features = false, features = ["keyring", "net", "ssh"]: the SDK crate's prebuilt feature — whose build.rs downloaded msb + libkrunfw into ~/.microsandbox at compile time — is off. The guest agentd is still embedded: that comes from microsandbox-filesystem/prebuilt, kept via a direct microsandbox-runtime = { features = ["prebuilt"] } dep (upstream bundles both downloads under one umbrella feature; without this the build fails on a missing build/agentd). Nothing is provisioned at gem build/install time anymore.
  • New companion gem microsandbox-rb-binaries (binaries/): one platform gem each for arm64-darwin, x86_64-linux-gnu, aarch64-linux-gnu, shipping vendor/bin/msb + vendor/lib/libkrunfw.* + vendor/manifest.json. rake -C binaries vendor[<platform>] downloads the upstream release bundle named by Microsandbox::Binaries::RUNTIME_VERSION and verifies it fail-closed against the release's checksums.sha256 as committed in binaries/checksums/<tag>.sha256 (reviewed in at runtime adoption; the live file must agree — a GitHub release is mutable). Unknown bundle entries, missing digests, symlinks and partial trees abort. build[<platform>] re-verifies the staged tree against the manifest and sets the payload explicitly from it (Gem::Specification.load caches by path, so the gemspec glob can't be trusted across a vendor:all run). No executables; no dependency edge in either direction (RubyGems has no optional deps; cloud-only users skip ~50 MB).
  • Resolver tier in the SDK. require "microsandbox" tries the companion gem once, eagerly (like the Node SDK's platform package at module load, so every msb-spawning entry point sees it — a lazy hook in ensure_runtime! would only cover Sandbox.create/start): lockstep gem "…", "= VERSION" pin → require "microsandbox/binaries" → the provider must be the companion gem, this gem's own tree, or a bare load path (a bare require would otherwise activate any gem shipping that feature path) → RUNTIME_VERSION must match (else warn + skip, never a stale-msb wire mismatch) → msb and libkrunfw present → Native.set_runtime_msb_path. Never raises. Effective ladder: MSB_PATH → binaries gem → config → ~/.microsandboxPATH. ensure_runtime! skips the installer when the gem's msb is what resolves; runtime_path= warns that the set-once slot is already claimed (use MSB_PATH). The first-use download into ~/.microsandbox stays as the lowest tier (npx parity, per #1305).
  • Integration-spec gate changed (spec/spec_helper.rb): examples now skip only when Microsandbox.runtime_path does not resolve to a file, instead of on installed? — which checks ~/.microsandbox only, so with the runtime coming from the gem (or MSB_PATH) every example skipped and the suite went green having booted nothing.
  • Lockstep guard: spec/unit/version_spec.rb asserts Binaries::VERSION == VERSION and Binaries::RUNTIME_VERSION == RUNTIME_VERSION.
  • CI: new binaries job (vendor + build all three platform gems, smoke-run the host one from the installed gem, upload artifact); package job installs source gem + binaries gem with plain gem install and asserts the resolver lands in the gem, msb runs, installer is skipped; integration now runs a provisioned lane (fallback tier) and a bundled lane that boots real microVMs from the gem's vendored runtime (RUBYLIB=binaries/lib, auto-install disabled so a silently inactive tier can't be masked). Both lanes assert that examples actually ran and none were pending (rspec exits 0 when everything is skipped).
  • Gemfile: gemspec glob: "{,*}.gemspec" — Bundler's default glob evaluated the nested gemspec in every bundle exec process.

Zero changes to the Rust extension sources; no CLI shim, no demo, no empty ruby-platform fallback gem (appcypher: moot post-split).

Not in this PR (follow-ups)

  • Publishing microsandbox-rb-binaries: needs a pending trusted publisher for the new gem name on rubygems.org first (OIDC, like the main gem), then release.yml wiring (build from binaries/pkg/, push alongside the source gem). Until then CI builds the gems as artifacts only; the optional gem "microsandbox-rb-binaries" simply isn't resolvable yet.
  • The ~/.microsandbox first-use download still has no digest check (upstream #1300 pending; fix(deps): adopt v0.6.8 digest-verification backport for runtime downloads #54 tracked a backport). The binaries-gem path is verified at build time.

Verification

  • Local: 438 unit specs green (incl. subprocess specs against the real native set-once slot and a fail-closed matrix for the vendoring tool), standardrb / rustfmt / clippy -D warnings clean, cargo check without the SDK-level prebuilt.
  • Local E2E: rake -C binaries vendor:all built all three platform gems (23–27 MB each, correct per-platform payload); gem installed the arm64-darwin one into an isolated GEM_HOME, ruby -I libMicrosandbox.runtime_path resolves into the installed gem, msb --versionmsb 0.6.9, ensure_runtime! skips the installer; same under bundle exec with RUBYLIB (the integration bundled-lane mechanism).
  • Adversarial review (critic) findings fixed in-branch: bundled integration lane silently skipping everything; Gem::Specification.load path cache breaking vendor:all; foreign-gem activation via bare require; mutable upstream checksums (now pinned in-repo); symlinks passing manifest verification; silent runtime_path= no-op.
  • CI: integration runs only on push-to-main / workflow_dispatch, so the two lanes are dispatched manually on this branch — merge gate is the bundled lane going green (real VMs booted from the gem's binaries).

🤖 Generated with Claude Code

https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH

ya-luotao and others added 4 commits August 22, 2026 03:01
…le time

Pull the core crate with default-features = false, features = ["keyring",
"net", "ssh"]: the SDK crate's `prebuilt` feature made its build.rs fetch msb +
libkrunfw into ~/.microsandbox while compiling the extension. The host runtime
now comes from the companion microsandbox-rb-binaries gem or, as the lowest
tier, the first-use download (setup::install is not gated on prebuilt).

The guest agentd must still be embedded: microsandbox-filesystem's build.rs
demands a locally built build/agentd without ITS prebuilt feature (the two
downloads shared one umbrella feature upstream), so enable just that
sub-feature through a direct microsandbox-runtime dependency. Cargo.lock loses
only the SDK-level prebuilt's transitive deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH
One gem per platform (arm64-darwin, x86_64-linux-gnu, aarch64-linux-gnu)
shipping vendor/bin/msb + vendor/lib/libkrunfw.* + vendor/manifest.json from
the upstream release named by Microsandbox::Binaries::RUNTIME_VERSION.

`rake -C binaries vendor[<platform>]` downloads the release bundle and verifies
it fail-closed against the release's checksums.sha256 as committed in
binaries/checksums/<tag>.sha256 (the live file must agree — a GitHub release is
mutable); unknown bundle entries, missing digests, symlinks and partial trees
all abort. `build[<platform>]` re-verifies the staged tree against the manifest
and sets the payload explicitly (Gem::Specification.load caches by path, so the
gemspec's vendor/ glob cannot be trusted across a vendor:all run). `verify`
runs the vendored msb on the host. No executables, no dependency edge to the
SDK gem in either direction.

Gemfile: `gemspec glob: "{,*}.gemspec"` — Bundler's default glob evaluated the
nested gemspec in every `bundle exec` process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH
`require "microsandbox"` now activates the companion gem once, eagerly (like
the Node SDK's platform package at module load, so every msb-spawning entry
point sees it, not just Sandbox.create/start): lockstep `gem "…", "= VERSION"`
pin → `require "microsandbox/binaries"` (absent → silent) → the provider must
be the companion gem, this gem's own tree or a bare load path → RUNTIME_VERSION
must match (else warn + skip — a stale msb passes an exists-check and then
fails every create on a wire-protocol mismatch) → msb AND libkrunfw present →
Native.set_runtime_msb_path. Never raises. Effective ladder: MSB_PATH →
binaries gem → config → ~/.microsandbox → PATH.

ensure_runtime! skips the ~/.microsandbox installer when the resolver returns
the bundled msb; runtime_path= warns that the set-once slot is already claimed.
Integration specs now gate on a resolvable runtime path rather than
`installed?` (home-dir only), so a bundled runtime runs them instead of
skipping all of them.

Specs: in-process activation matrix, lockstep guard for the companion gem's
VERSION/RUNTIME_VERSION, and subprocess specs against the real set-once slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH
…ment the split

CI: a `binaries` job vendors + builds all three platform gems and smoke-runs the
host one from the installed gem; `package` installs source gem + binaries gem
with plain `gem install` and asserts the resolver lands in the gem, msb runs,
and the installer is skipped; `integration` gains a `bundled` lane booting real
microVMs from the gem's vendored runtime (RUBYLIB=binaries/lib, auto-install
off) next to the `provisioned` fallback lane, and both assert that examples
actually ran (rspec exits 0 when everything is skipped).

Docs: README installation/runtime-configuration/versioning/releasing,
DESIGN runtime section, CHANGELOG Unreleased, CLAUDE.md third lock-step axis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f7baa259

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/microsandbox.rb Outdated
end
Native.set_runtime_msb_path(msb)
@bundled_msb_path = msb
rescue => e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Catch syntax failures from the optional companion

When the installed companion's microsandbox/binaries.rb is corrupt or uses syntax unsupported by the running Ruby, require raises SyntaxError, which inherits from ScriptError rather than StandardError; neither the inner rescue LoadError nor this rescue => e catches it. Consequently, the optional companion can make require "microsandbox" fail—even for cloud-only users—instead of following the documented warn-and-fallback behavior. Catch SyntaxError at the companion load boundary as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed: the activation boundary now rescues StandardError, ScriptError (with a comment on why), and a spec raises SyntaxError out of the stubbed require to pin it (spec/unit/runtime_spec.rb).

…ample

Both lanes of the first dispatch booted real microVMs (49 examples, 0
failures) and were then failed by the new `pending_count == 0` assertion over
attach_spec's legitimate "requires a real TTY" skip. Keep the guard against a
lane that silently skips because its runtime never resolved — that is what
the bundled lane exists to catch — by matching spec_helper's runtime-gate
skip messages, and list any other pending examples for the log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00522849a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/microsandbox.rb Outdated
"(only microsandbox-rb-binaries is trusted for the bundled runtime)"
return nil
end
unless Binaries::RUNTIME_VERSION == RUNTIME_VERSION

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject companion gems with a mismatched gem version

When Bundler has selected a different microsandbox-rb-binaries version, the exact-version gem call above raises Gem::LoadError, which is deliberately swallowed, and require then loads the mismatched bundled version. This condition only checks RUNTIME_VERSION, so gem-only SDK releases that share the same upstream runtime tag are accepted despite the documented lockstep requirement; an older companion API or packaging revision can therefore claim the resolver slot. Include Binaries::VERSION == VERSION in the activation gate rather than relying on the rescued activation attempt.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed — the gem "…", "= VERSION" pin is best-effort under Bundler, so it cannot be the enforcement. The gate now requires Binaries::VERSION == VERSION and Binaries::RUNTIME_VERSION == RUNTIME_VERSION; either mismatch warns and falls back to ~/.microsandbox. Spec added for the same-runtime/different-gem-version case; README/DESIGN/CHANGELOG/binaries README updated to say "same version and same runtime".

@ya-luotao

Copy link
Copy Markdown
Owner Author

Merge-gate evidence — workflow_dispatch run on this branch (integration doesn't run on PR events): https://github.com/ya-luotao/microsandbox-rb/actions/runs/32519427196all green, including both integration lanes: provisioned (fallback download) and bundled (real microVMs booted from the gem's vendored msb + libkrunfw, resolver asserted at binaries/vendor/bin/msb, auto-install disabled) — 49 examples / 0 failures each; the single pending example is attach_spec's TTY skip.

…ive SyntaxError

Review (Codex) on #58:
- The activation gate only compared RUNTIME_VERSION, so a companion gem at a
  different gem version but the same runtime tag was accepted although the
  documented contract is "install both at the same version" — and the
  `gem "…", "= VERSION"` pin cannot enforce it (under Bundler it raises for
  any other bundled version and is swallowed). Require Binaries::VERSION ==
  VERSION as well; either mismatch warns and falls back.
- A corrupt / newer-syntax binaries.rb raises SyntaxError out of `require`,
  which is a ScriptError, not a StandardError: neither rescue caught it, so an
  optional gem could take `require "microsandbox"` down — even for cloud-only
  users. Rescue ScriptError at the activation boundary too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9upBMYy2uZqz5gp21qgTH
@ya-luotao
ya-luotao merged commit 20d1342 into main Aug 23, 2026
25 checks passed
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