MEP 76. Mochi and Ruby gem bridge
| Field | Value |
|---|---|
| MEP | 76 |
| Title | Mochi and Ruby gem bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 22:15 (GMT+7) |
| Depends | MEP-1 (Grammar, for the import ruby extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for optional / union translation), MEP-56 (Ruby transpiler, for the emit pipeline and the mochi-runtime gem), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, the sparse-index protocol, the content-addressed object store, the capability declaration model, and OIDC trusted publishing) |
| Research | /docs/research/0076/ |
| Tracking | /docs/implementation/0076/ |
Abstract
Mochi today (May 2026, after MEP-56's Ruby transpiler landed and MEP-57's source-level package system reached Draft) has ten code-emitting targets and a developing package management story. MEP-56 gave Mochi the ability to transpile to idiomatic Ruby source, and MEP-56 phase 22 shipped TargetRubyGem together with the mochi-runtime gem on RubyGems.org, proving that Direction 2 (Mochi-as-gem-producer) is already structurally complete. What neither MEP provides is Direction 1: a Mochi program cannot yet pull in an arbitrary gem from RubyGems.org, and there is no end-to-end mochi pkg publish --to=rubygems.org flow backed by OIDC trusted publishing. RubyGems.org hosts 170,000+ gems (May 2026); the top 100 most-downloaded gems account for hundreds of millions of downloads per month.
MEP-76 specifies the bidirectional Ruby gem bridge: Mochi packages can consume any gem via import ruby "<gem>@<semver>" as <alias> with no user-written boilerplate, and Mochi packages can publish to RubyGems.org as gems via mochi pkg publish --to=rubygems.org. The bridge is architecturally simpler than MEP-73 (the Rust bridge) in one critical dimension: because MEP-56 already transpiles Mochi to Ruby, there is no C FFI wrapper layer. The generated Ruby code simply calls require '<gem>' and invokes gem methods directly. The synthesised shim is a thin Ruby wrapper plus a Mochi extern fn corpus, not a foreign-language ABI bridge.
The proposal builds on MEP-57's manifest / lockfile / capability infrastructure and MEP-56's emit pipeline, and adds a new self-contained component under package3/ruby/. The Mochi grammar gains the single keyword ruby as a valid <lang> token in the existing FFI-import production. The lockfile gains one new repeated table ([[ruby-package]]). No existing transpiler MEP needs to change beyond what MEP-56 phase 22 already provided.
The system is anchored on seven load-bearing decisions, each justified in §Rationale: RBS as canonical API surface, direct require/call without a C FFI layer, gem_rbs_collection as fallback for gems without bundled RBS, a closed type-mapping table, TargetRubyGem reuse for Direction 2, RubyGems.org OIDC trusted publishing as the sole publish path, and Bundler as the build orchestrator.
Motivation
Mochi today integrates with foreign ecosystems through the Go, Python, TypeScript, and Rust FFI surfaces. Ruby is the fifth. The Ruby gap is distinct in character from the Rust gap (MEP-73) because Mochi already transpiles to Ruby: the bridge's consumer direction does not need to solve a cross-language ABI problem at all. The motivation has six dimensions:
-
RubyGems.org is the canonical distribution channel for web application libraries, DevOps tooling, test frameworks, data-serialisation helpers, and cloud SDK adapters. Gems such as
activesupport,faraday,redis,sidekiq,aws-sdk-s3, andsinatrahave no equivalent in Mochi's standard library and no easy substitute in the other bridged ecosystems. A Mochi program targeting a Ruby web host (e.g., a Heroku dyno or a Fly.io app built with MEP-56's emit path) needs access to those gems without forfeiting the Mochi type system. The 170,000+ gems on RubyGems.org represent a decade of production-tested Ruby libraries that Mochi users should reach without forfeiting static typing. -
MEP-56's Ruby emit target makes the consumer direction uniquely cheap. Every competing FFI bridge (MEP-73 for Rust, the Go bridge, the Python bridge) requires either a C ABI layer or a subprocess protocol because the generated code runs in a different runtime from the foreign library. Mochi-to-Ruby output runs inside the Ruby runtime by definition:
require 'gem_name'is the native Ruby mechanism, and the gem's Ruby code executes in the same MRI/CRuby or JRuby interpreter as Mochi's transpiled output. No shared-library linking, no subprocess IPC, no foreign-function ABI. MEP-76 exploits this structural advantage: the bridge's consumer shim is a file that callsrequireand a handful of delegation methods, not a synthesisedextern "C"wrapper crate. -
RBS (Ruby 3.0+) has matured into a stable, machine-readable, schema-versioned type description for Ruby code. The Ruby core team shipped RBS as part of Ruby 3.0 (December 2020) and the format has been stable since. Gems that bundle their own RBS sigs store them under
sig/and declare them in the gemspecfileslist. Thegem_rbs_collectionrepository (github.com/ruby/gem_rbs_collection) provides community-maintained RBS for the most popular gems that don't yet bundle their own. RBS gives the bridge a path to typed bindings that does not require parsing Ruby source, does not require running the gem, and does not require hand-written annotations on the gem author's side. -
The
mochi-runtimegem on RubyGems.org is proof that Direction 2 already works. MEP-56 phase 22 shippedTargetRubyGemand pushedmochi-runtime(Apache-2.0, ~123 LOC) to RubyGems.org. The gem is live, versioned, and installable viagem install mochi-runtime. MEP-76 extends this proven infrastructure to user-written Mochi packages: any Mochi library can become a gem by adding a[ruby.publish]section tomochi.tomland runningmochi pkg publish --to=rubygems.org. No new emit-pipeline work is needed on the MEP-56 side. -
RubyGems.org OIDC trusted publishing (GA 2023) is the current supply-chain best practice for the Ruby ecosystem. RubyGems.org launched OIDC trusted publishing in 2023 (stable GA, no opt-in flag required), using GitHub Actions
id-token: writeto obtain a short-lived OIDC token that the RubyGems.org server exchanges for a publishing certificate via Sigstore. This is the same pattern MEP-57 mandates for all publish flows. Long-livedGEM_HOST_API_KEYtokens are not accepted by MEP-76 for the same reasons MEP-73 rejectsCARGO_REGISTRY_TOKEN. -
Bundler is the de-facto Ruby build orchestrator and the bridge should not reinvent it. Bundler has been the authoritative Ruby dependency resolver since 2010 and is bundled with Ruby itself since 2.6. Every production Ruby project already uses a
Gemfileand aGemfile.lock. MEP-76's build orchestration emits aGemfilefrom the[ruby-dependencies]manifest table and delegates resolution and installation tobundle install. This means Mochi users get all of Bundler's network caching, platform-specific precompiled gem support, and lockfile semantics without the bridge having to re-implement a Ruby resolver. It also means a Mochi project with a pre-existingGemfilecan merge its Ruby dep graph with the Mochi-managed one via abundle mergesub-step.
Specification
This section is normative. Sub-notes under /docs/research/0076/ are informative.
1. Pipeline overview
MEP-76 introduces a per-import Ruby dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-56 build driver:
mochi.toml [ruby-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved Ruby dep tree (gem name + version + source URL)
| package3/ruby/sparseindex.Fetch (RubyGems sparse-index protocol)
v
.gem tarballs in ~/.cache/mochi/ruby-deps/<sha256-hex>/
| package3/ruby/rbs.IngestBundled (walk sig/ directory in gem)
| package3/ruby/rbs.IngestCollection (gem_rbs_collection fallback)
| package3/ruby/yard.IngestDoc (YARD doc fallback when no RBS)
v
RBS sig set (or YARD-derived pseudo-RBS) per gem
| package3/ruby/typemap.Translate (closed RBS-to-Mochi table)
v
TranslatedSurface + SkipReport per gem
| package3/ruby/shimemit.Emit (.rb shim: require + method wrappers)
v
ruby_shims/<gem>/shim.rb (emitted into MEP-56 output directory)
| package3/ruby/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per gem, imported by the user's source
| MEP-56 Driver.Build (TargetRuby / TargetRubyGem)
v
Ruby source tree or publishable gem
The bridge does not run the Ruby interpreter at ingest time. RBS file parsing is done entirely in Go (via the package3/ruby/rbs/ parser). The .rb shim is a thin delegation layer emitted as source by the Go side; the user's bundle exec ruby (or bundle exec rake build, for gem publish) runs it inside the Ruby runtime alongside the user's transpiled program.
2. Manifest extension: [ruby-dependencies], [ruby], and [ruby.publish]
The MEP-57 mochi.toml gains three new optional top-level tables:
[ruby-dependencies]
faraday = "~> 2.9"
redis = "~> 5.0"
sidekiq = ">= 7.2, < 8.0"
dry-types = "~> 1.7"
activesupport = { version = "~> 7.1", require = "active_support/all" }
my-local-gem = { path = "../my-gem" }
my-git-gem = { git = "https://github.com/example/my-gem", ref = "main" }
[ruby]
ruby-version = "3.3"
bundler-version = "~> 2.5"
platform = "ruby"
[ruby.publish]
gem-name = "my-mochi-package"
summary = "A Mochi package published as a Ruby gem."
description = "Longer description here."
homepage = "https://github.com/example/my-mochi-package"
license = "Apache-2.0"
authors = ["Mochi core"]
files = ["lib/**/*.rb", "sig/**/*.rbs"]
require-path = "lib/my_mochi_package"
[ruby-dependencies] follows RubyGems/Bundler's gem directive grammar: simple string for a version constraint, table for inline constraint plus require alias, path, or git/ref. This is intentional: Mochi users with Ruby background can copy entries from an existing Gemfile without translation; the bridge passes the table through to the synthesised Gemfile with a verbatim gem '<name>', '<constraint>' line per entry.
The [ruby] table holds Mochi-specific knobs:
ruby-version: the minimum Ruby version required. Written toGemfileasruby '~> <version>'. Default"3.0"(the Ruby 3.0 baseline where RBS is standard).bundler-version: the Bundler version constraint. Written toGemfileasgem 'bundler', '<constraint>'. Default"~> 2.5".platform: the RubyGems platform string. Default"ruby"(pure-Ruby). Set to"x86_64-linux"etc. for platform-specific precompiled gems.
The [ruby.publish] table mirrors a gemspec. When present, mochi pkg publish --to=rubygems.org reads these fields to generate the .gemspec file that TargetRubyGem emits. All fields map one-to-one to Gem::Specification attributes. If the package already ships a hand-written .gemspec, the generated one is skipped and the hand-written one is used instead.
3. Lockfile extension: [[ruby-package]]
The MEP-57 mochi.lock gains one new repeated table:
[[ruby-package]]
name = "faraday"
version = "2.9.0"
source = { kind = "rubygems", registry = "https://rubygems.org" }
gem-sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
rbs-source = "bundled"
rbs-sig-sha256 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
shim-sha256 = "..."
capabilities-declared = ["net"]
dependencies = ["faraday-net_http@~> 3.0", "rack@>= 1.4", "logger"]
gem-sha256 is the SHA-256 of the .gem tarball as downloaded from https://rubygems.org/gems/<name>-<version>.gem. This hash is also the one RubyGems.org publishes in its index, enabling cross-verification.
rbs-source is one of "bundled" (RBS sigs shipped in the gem under sig/), "collection" (fetched from gem_rbs_collection), "yard" (derived from YARD doc), or "none" (no sig; the gem's entire surface is skipped with a SkipReport). A drift in rbs-source at mochi pkg lock --check time (e.g., the gem author added bundled RBS after a previous lock used "collection") is a warning, not an error, but the rbs-sig-sha256 must still match.
rbs-sig-sha256 is the SHA-256 of the concatenated RBS source files the bridge ingested (sorted by path, then concatenated). A drift here at --check time is a hard error: the shim must be regenerated.
shim-sha256 is the SHA-256 of the synthesised .rb shim file. A drift here at --check time is a hard error.
capabilities-declared mirrors MEP-57's capability model. The RubyGems bridge recognises net (gem opens network sockets), fs (gem reads/writes files), proc (gem spawns subprocesses), and native (gem includes a native C extension). The native capability gates phase 12 behaviour.
4. RBS-to-Mochi type mapping
The bridge uses a closed translation table. Items not in the table are skipped with a SkipReport entry naming the item and the reason.
| RBS type | Mochi type | Notes |
|---|---|---|
Integer | int | |
Float | float | |
String | string | |
Symbol | string | Symbols become strings; SkipReport note if ambiguity with String |
bool / boolish / true | false | bool | RBS boolish is a common alias |
nil | nil literal | |
Array[T] where T in table | list<T> | |
Hash[String, V] / Hash[Symbol, V] where V in table | map<string, V> | |
T? (optional) | T? | Mochi optional |
T | nil | T? | Normalised to optional |
struct / Data class | Mochi record | Fields must all be in-table |
^(A) -> B (proc type, simple arity) | fun(A): B | Only for fixed-arity, in-table A and B |
Proc (untyped proc) | refused | SkipReport: untyped proc |
untyped | refused | SkipReport: untyped |
self | refused | SkipReport: self type |
instance | refused | SkipReport: instance type |
class | refused | SkipReport: class type |
top | refused | SkipReport: top type |
bot | refused | SkipReport: bottom type |
void (non-return position) | refused | SkipReport: void in non-return |
complex union A | B (neither is nil) | refused | SkipReport: non-optional union |
The closed-table philosophy is identical to MEP-73's: the bridge promises sound bindings for in-table items, not full generality. A user who needs an out-of-table item can write a hand-authored extern fn declaration in Mochi that takes responsibility for the type.
5. Surface syntax: import ruby "..."
The Mochi grammar's existing FFI-import production:
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust" | "ruby"
gains ruby as a Lang alternative. The string literal is one of:
<gem-name>: bare name, resolves through[ruby-dependencies]constraint plusmochi.lock.<gem-name>@<semver-constraint>: explicit version constraint, must match[ruby-dependencies].<gem-name>@git+<url>#<ref>: git source.<gem-name>@path+<relative>: path source.
Example surface programs:
import ruby "faraday@~> 2.9" as faraday
import ruby "redis" as redis
import ruby "activesupport" as active_support
fn fetch_json(url: string): string {
let conn = faraday.new(url)
let resp = conn.get("/api/data")
return resp.body()
}
fn cache_set(key: string, value: string): bool {
let r = redis.new
r.set(key, value)
return true
}
The <alias> introduces a Mochi namespace. Symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <gem-name>#<method>. The auto keyword is admitted for import ruby ... auto to opt into flat top-level binding (rather than namespaced under the alias), matching the semantics already defined for import go ... auto.
6. Ruby shim emit
For each gem, the bridge emits two files:
ruby_shims/<gem>/shim.rb (injected into the MEP-56 emit directory):
# Auto-generated by mochi pkg lock. DO NOT EDIT.
# gem: faraday 2.9.0 (rbs: bundled)
require 'faraday'
module MochiShim__Faraday
def self.new(url)
::Faraday.new(url)
end
def self.get(conn, path)
conn.get(path)
end
# ... one method per translated RBS method signature
end
The shim module exposes every translated method as a module-level (self.) method, turning instance methods into two-argument calls (receiver + args). This is necessary because Mochi's extern fn model is procedural, not object-oriented: the Mochi caller passes the receiver object as the first argument.
ruby_shims/<gem>/shim.mochi (the extern fn corpus):
extern type Faraday__Connection
extern fn faraday__new(url: string): Faraday__Connection
extern fn faraday__get(conn: Faraday__Connection, path: string): Faraday__Response
extern fn faraday__response__body(resp: Faraday__Response): string
extern fn faraday__response__status(resp: Faraday__Response): int
The extern type declarations introduce opaque Mochi types for each gem class that appears as a receiver or return value in the translated surface. The external name mangling is <gem>__<class> for types and <gem>__<class>__<method> (or <gem>__<module_method> for module-level methods) for functions.
7. CLI surface
The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:
mochi pkg add ruby <gem>[@<constraint>]: adds an entry to[ruby-dependencies]and runsmochi pkg lock.mochi pkg lock: as today (MEP-57), now extended to walk[ruby-dependencies], query the RubyGems sparse index for resolution, fetch each.gemtarball into the content-addressed cache, run RBS/YARD ingest, synthesise the shims, and write[[ruby-package]]entries.mochi pkg lock --check: as today, now extended to verifygem-sha256,rbs-sig-sha256, andshim-sha256for every[[ruby-package]]entry.mochi pkg publish --to=rubygems.org [--dry-run]: builds the package as a Ruby gem viaTargetRubyGem, packages it withgem build, obtains an OIDC token from the CI environment (GitHub Actionsid-token: write), presents it to the RubyGems.org OIDC trusted-publishing endpoint, uploads the signed gem.--dry-runskips upload but still exercises the signing flow against a mock-rubygems harness.mochi pkg sync ruby: regenerates the shim files from scratch (without changing the lockfile). Used after manual edits to the synthesised shim.
8. Build orchestration
When a Mochi program contains one or more import ruby "..." declarations, the MEP-56 build driver gains the following extensions:
-
Before invoking
bundle exec ruby(orrake build), the driver invokespackage3/ruby/Bridge.PrepareWorkspace(workdir, mochiLock)which:- For each
[[ruby-package]]inmochi.lock, materialises the gem source from the content-addressed cache. - Materialises the synthesised shim files into
<workdir>/ruby_shims/<gem>/. - Writes a
<workdir>/Gemfilethat includes onegem '<name>', '<version>'line per[[ruby-package]]entry (exact version pinned from lockfile) plusgem 'bundler'. - Runs
bundle install --deployment --frozento install gems into<workdir>/vendor/bundle/(the--frozenflag ensures theGemfile.lockBundler generates matches themochi.lockpinned versions).
- For each
-
The MEP-56 emit pass treats each
import ruby "<gem>" as <alias>as a Mochiimport "./ruby_shims/<gem>/shim.mochi" as <alias>shim, where the shim file is theextern fncorpus the bridge emitted. -
The emitted Ruby entry-point file (
main.rborlib/<name>.rb) gains arequire_relative 'ruby_shims/<gem>/shim'line per imported gem. -
The driver invalidates the shim cache when any
[[ruby-package]]shim-sha256changes.
9. Publish flow (Direction 2)
mochi pkg publish --to=rubygems.org runs the following steps:
- Invoke
TargetRubyGem(MEP-56 phase 22) to emit the gem source tree and.gemspec. - Run
gem build <name>.gemspecto produce<name>-<version>.gem. - Obtain an OIDC token from the CI environment. On GitHub Actions this requires
id-token: writein the workflowpermissionsblock. The bridge checks forACTIONS_ID_TOKEN_REQUEST_URLandACTIONS_ID_TOKEN_REQUEST_TOKENenvironment variables and uses the GitHub Actions OIDC provider endpoint to obtain the token. On other CI providers (GitLab CI, Buildkite, CircleCI) the bridge uses the provider-specific OIDC endpoint. - Present the OIDC token to
https://rubygems.org/api/v1/webauthn_verification(the RubyGems.org trusted-publishing endpoint, GA 2023). - RubyGems.org exchanges the token for a short-lived signing certificate via Sigstore/Fulcio and records a Rekor log entry.
- Upload
<name>-<version>.gemtohttps://rubygems.org/api/v1/gemswith the signing certificate. - Record the Rekor log entry URL in
mochi.lockunder a new[publish]sub-table of the relevant[[ruby-package]]entry.
--dry-run executes steps 1-3, skips steps 4-7, and prints the token + payload that would be sent. A mock-rubygems harness (parallel to the sigstore-mock-fulcio harness in MEP-57) is available for integration test use.
Rationale
1. RBS as canonical API surface (vs YARD vs runtime introspection)
RBS is the official Ruby type system shipped with Ruby 3.0 (December 2020) and maintained by the Ruby core team. RBS files are machine-readable, schema-stable, and do not require running the gem. The alternative of parsing YARD documentation comments is fragile: YARD types are human-readable strings embedded in @param [Type] comments, not a formal grammar, and are routinely misspelled, omitted, or written as English prose. The alternative of runtime introspection (gem.instance_methods.map { |m| [m, gem.method(m).arity] }) provides method names and arity counts but no type information at all, which would make the closed-table philosophy impossible. A fourth alternative, parsing Ruby source ASTs via RuboCop-AST, would require a full Ruby parser in Go and would not handle method aliases, mixins, or modules forwarded via extend. RBS gives us formal types, class hierarchies, method signatures, and generic parameterisation in a stable, versioned format. The bridge treats RBS as authoritative and only falls back to YARD (phase 4) when no RBS is available at all.
2. Direct Ruby require/call, no C FFI layer
This decision is the fundamental architectural difference between MEP-76 and MEP-73. In MEP-73, the generated code runs inside the Mochi runtime (compiled to native via Rust), which is a different process-level runtime from the Rust crates the user imports. A C FFI wrapper crate is therefore necessary to cross the ABI boundary. In MEP-76, the generated code runs inside the Ruby interpreter (MRI or JRuby), which is the same runtime as every gem on RubyGems.org. require 'faraday' is the exact same mechanism that every Ruby script uses. There is no ABI boundary to cross, no extern "C" surface to synthesise, no box / unbox marshalling to write. The synthesised .rb shim is not an ABI bridge; it is a thin Ruby module that reorganises the gem's method signatures into the procedural shape that Mochi's extern fn model expects (receiver as first argument). The alternative of synthesising a C extension wrapper anyway (for "uniformity" with MEP-73) would add ~300 LOC of unnecessary complexity per imported gem and would break on JRuby, TruffleRuby, and mruby targets where C extensions do not run. Directness wins.
3. gem_rbs_collection as fallback for gems without bundled RBS
Many popular gems predate RBS (or have not yet added sig/ to their release process) but have community-maintained RBS sigs in the gem_rbs_collection repository (github.com/ruby/gem_rbs_collection), which is the official RBS community collection maintained by the ruby/rbs team. As of May 2026, gem_rbs_collection covers roughly 150 popular gems including activesupport, redis, sinatra, rack, faraday, rake, zeitwerk, and others from the fixture corpus. The bridge fetches the collection at mochi pkg lock time (pinning a specific commit SHA and recording it in mochi.lock), caches it in the content-addressed store, and queries it by gem name and version range before falling back to YARD. The alternative of requiring gem authors to add RBS to their gems before the bridge can import them would block 80%+ of the top 200 gems indefinitely. The alternative of skipping untyped gems entirely would make the bridge too narrow to be useful. The gem_rbs_collection fallback is the pragmatic middle path.
4. Closed type-mapping table (same philosophy as MEP-73)
The bridge promises typed Mochi bindings only for the subset of RBS types it can translate soundly. Items outside the table (complex union types, untyped procs, self, instance, class, top, bot, void in non-return position) are skipped with a structured SkipReport. This is the same closed-table philosophy as MEP-73's Rust bridge. The alternative of "best-effort" translation (e.g., mapping every complex union to any / object) would produce bindings that compile but silently violate type safety at runtime. The alternative of refusing to import any gem that has out-of-table items in its surface would block nearly every real gem (since virtually all Ruby libraries use untyped or self somewhere in their RBS). The closed table plus explicit skip-report gives the user a clear, auditable list of what the bridge translated and what it did not, without making unsound promises.
5. TargetRubyGem reuse for Direction 2
MEP-56 phase 22 already shipped TargetRubyGem: a build target that lowers a Mochi package to a publishable RubyGems gem, generates a .gemspec, and packages the mochi-runtime gem dependency. The mochi-runtime gem is already live on RubyGems.org (Apache-2.0, ~123 LOC), which means the gem push path (gem credentials, gemspec format, RubyGems.org API) has already been proven end-to-end. MEP-76 does not redesign TargetRubyGem; it extends it with OIDC trusted publishing metadata, reads publish metadata from mochi.toml's [ruby.publish] section, and wires the whole flow into mochi pkg publish. The alternative of designing a new TargetRubyGemBridge target for MEP-76 would duplicate code, fragment the publish path, and risk diverging the mochi-runtime gem from the user gem emit pipeline. Reuse wins.
6. RubyGems.org OIDC trusted publishing (GA 2023), no long-lived API key path
RubyGems.org launched OIDC trusted publishing in 2023 (stable GA, no opt-in flag), using GitHub Actions id-token: write to obtain a short-lived OIDC token. The RubyGems.org team published the trusted publishing spec and confirmed GA status in the RubyGems.org changelog for 2023. This matches the broader industry trend: npm (April 2024 GA), Maven Central (Sigstore GA October 2024), PyPI (PEP 740 GA late 2025), Cargo (RFC #3724 accepted Q4 2025) all converged on the same keyless OIDC pattern. MEP-76 ships exclusively on this path. The GEM_HOST_API_KEY environment variable path is not supported. The rationale is identical to MEP-73's rejection of CARGO_REGISTRY_TOKEN: long-lived API tokens are the leading cause of gem account compromise and supply-chain injection. The bridge was designed after those lessons; it does not repeat them.
7. Bundler as build orchestrator, not a custom resolver
Bundler has been the canonical Ruby dependency resolver since 2010, is bundled with Ruby 2.6+, handles platform-specific precompiled gem selection, understands platform gems (x86_64-linux, aarch64-linux, etc.), respects Gemfile.lock for reproducibility, and integrates with the full RubyGems.org API including the compact index. Writing a custom Ruby resolver in Go would be a major undertaking (estimated 8,000+ LOC to match Bundler's resolver fidelity on gems with complex gemspec constraint expressions) and would diverge from ecosystem tooling, breaking projects that mix Mochi-managed and hand-authored Gemfile entries. The bridge takes the same approach as MEP-57 takes with mochi.toml for Go: own the manifest layer, delegate resolution to the ecosystem's canonical tool. MEP-76 generates a Gemfile from [ruby-dependencies] and calls bundle install --frozen; the resulting Gemfile.lock is cross-checked against mochi.lock's [[ruby-package]] entries as a consistency assertion.
Phases
See /docs/implementation/0076/ for the per-phase tracking matrix. Fourteen phases cover skeleton (0), RubyGems sparse-index client (1), RBS bundled-sig ingest (2), gem_rbs_collection fallback ingest (3), YARD doc fallback ingest (4), RBS-to-Mochi type mapping (5), Ruby shim emit (6), import ruby grammar extension (7), Bundler build orchestration (8), mochi.lock integration (9), TargetRubyGem publish metadata (10), OIDC trusted publishing (11), native C extension gem handling (12), and Ractor/Fiber async bridge (13).
A phase is LANDED only when its gate is green against the curated 20-gem fixture corpus.
Target matrix
| Phase | MRI Ruby 3.3 (darwin-arm64) | MRI Ruby 3.3 (linux-x64) | JRuby 9.4 | TruffleRuby 24 |
|---|---|---|---|---|
| 0. skeleton | NOT STARTED | n/a | n/a | n/a |
| 1. sparse-index client | NOT STARTED | n/a | n/a | n/a |
| 2. RBS bundled-sig ingest | NOT STARTED | n/a | n/a | n/a |
| 3. gem_rbs_collection fallback | NOT STARTED | n/a | n/a | n/a |
| 4. YARD doc fallback | NOT STARTED | n/a | n/a | n/a |
| 5. type-mapping table | NOT STARTED | n/a | n/a | n/a |
| 6. Ruby shim emit | NOT STARTED | required | required | required |
| 7. import ruby grammar | NOT STARTED | required | required | required |
| 8. build orchestration | NOT STARTED | required | required | required |
| 9. mochi.lock integration | NOT STARTED | required | required | required |
| 10. TargetRubyGem publish metadata | NOT STARTED | required | n/a (publish is MRI only) | n/a |
| 11. OIDC trusted publishing | NOT STARTED | required | n/a | n/a |
| 12. native C extension gems | NOT STARTED | required | n/a (no C ext on JRuby) | n/a |
| 13. Ractor/Fiber async bridge | NOT STARTED | required | n/a (Ractor is MRI only) | n/a |
A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.
Non-goals
-
Full Ruby source parsing. The bridge does not parse
.rbfiles. It reads RBS files only. Ruby source parsing would require a full Ruby AST parser in Go and would not handle metaprogramming (method_missing,define_method,extend,includewith dynamic module composition). -
Complete RBS coverage. Items outside the closed type-mapping table are skipped. The bridge does not attempt to translate arbitrary Ruby types to Mochi.
-
Bidirectional method override / monkey-patching. The bridge generates read-only bindings: the Mochi caller calls gem methods, not the other way around. A gem cannot call back into Mochi code via the shim.
-
Gem feature flags. RubyGems does not have a concept analogous to Cargo features. The
requirefield in[ruby-dependencies]covers the common case of a gem with multiple require paths, but there is no gem-feature activation mechanism. -
mruby / mruby-on-WASM targets. MEP-56 targets MRI Ruby for the primary emit path. mruby is a distinct runtime with a different C API; gems that require MRI's C extension ABI do not run on mruby.
-
Automatic RBS generation for the user's own Mochi package. When publishing as a gem (Direction 2), the bridge does not generate RBS sigs for the emitted gem's public surface. This is a future sub-phase (MEP-76 N.1) that requires a Mochi-type-to-RBS emitter.
Appendix: Fixture corpus
The 20-gem fixture corpus covers the top-downloaded RubyGems.org gems plus representative C-extension and async use cases.
| Gem | Version (May 2026) | RBS source | Phase gate | Notes |
|---|---|---|---|---|
| json | 2.7.x | bundled | 2 | Ruby stdlib-adjacent; always available |
| rake | 13.x | collection | 3 | Build tool; extensive module surface |
| rack | 3.x | collection | 3 | Web server interface |
| faraday | 2.9.x | bundled | 2 | HTTP client |
| httparty | 0.22.x | yard | 4 | No RBS; YARD fallback |
| redis | 5.x | collection | 3 | Key-value client |
| sinatra | 4.x | collection | 3 | Web DSL |
| rspec-core | 3.13.x | yard | 4 | Testing; complex DSL surface |
| rubocop | 1.65.x | bundled | 2 | Linter; large method surface |
| dry-types | 1.7.x | bundled | 2 | Type system; record/sum types |
| zeitwerk | 2.7.x | bundled | 2 | Code loader |
| dotenv | 3.x | yard | 4 | Env file loader |
| activesupport | 7.1.x | collection | 3 | Rails utilities; large surface |
| nokogiri | 1.16.x | collection | 3 | C ext (libxml2); phase 12 |
| pg | 1.5.x | yard | 4 | C ext (libpq); phase 12 |
| aws-sdk-s3 | 1.x | bundled | 2 | Cloud SDK; async IO |
| openai | 2.x | bundled | 2 | LLM API client |
| sidekiq | 7.x | collection | 3 | Background job; Ractor interest |
| puma | 6.x | yard | 4 | Web server; C ext; phase 12 |
| mini_magick | 5.x | yard | 4 | Image processing via ImageMagick subprocess |
Gems marked "C ext" (nokogiri, pg, puma) exercise phase 12 (native C extension gem detection and skip/warn/docker-prebuilt strategy). Gems marked "Ractor interest" (sidekiq) exercise phase 13 (Ractor/Fiber async bridge). Every gem in the corpus must produce a clean SkipReport (listing out-of-table items with reasons) and a compilable extern fn corpus before its phase gate is marked LANDED.
Acknowledgements
This MEP builds on MEP-56 (Ruby transpiler) for the emit pipeline, the TargetRubyGem build target, and the mochi-runtime gem on RubyGems.org; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the SHA-256 content-addressed object store, the OIDC trusted-publishing infrastructure, and the capability declaration scheme; on the Ruby core team for the RBS type system (Ruby 3.0+, stable since); on the ruby/gem_rbs_collection community for maintaining RBS signatures for popular gems; on Bundler for the dependency resolution model and the Gemfile / Gemfile.lock semantics; on the YARD documentation framework for the fallback type-extraction path; on RubyGems.org for the OIDC trusted publishing API (GA 2023); on Sigstore / Fulcio / Rekor for the keyless certificate and transparency log infrastructure; on the RubyGems sparse-index protocol for the dependency fetch path; on the Ruby on Rails core team for activesupport as a representative large-surface gem in the fixture corpus; and on MEP-73 (Rust bridge) for the architectural patterns (closed type-mapping table, capability declarations, lockfile dual-hashing, fixture corpus gate philosophy) that MEP-76 adapts to the Ruby context.