Skip to content

sdk/ruby: add Ruby SDK for on-device semantic search - #463

Open
pulipakaa24 wants to merge 2 commits into
usemoss:mainfrom
pulipakaa24:sdk/ruby-client
Open

sdk/ruby: add Ruby SDK for on-device semantic search#463
pulipakaa24 wants to merge 2 commits into
usemoss:mainfrom
pulipakaa24:sdk/ruby-client

Conversation

@pulipakaa24

@pulipakaa24 pulipakaa24 commented Jul 23, 2026

Copy link
Copy Markdown

Pull Request Checklist

Please ensure that your PR meets the following requirements:

  • I have read the CONTRIBUTING guide.
  • I have updated the documentation (if applicable).
  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

Description

Adds a Ruby SDK over the Moss runtime so Ruby and Rails developers get
on-device semantic search without leaving their stack. It has parity with the
existing SDKs on index management, search, and metadata filtering, and
follows the same two-layer structure.

Two gems under sdks/ruby/:

  • moss-core (sdks/ruby/bindings/) — FFI bindings over the prebuilt
    libmoss C SDK, exposing ManageClient, IndexManager, and Session.
    libmoss is attached lazily; when it is absent the SDK degrades to a
    cloud-query fallback / BindingsUnavailableError (mirrors the Go SDK).
  • moss (sdks/ruby/sdk/) — the high-level Moss::Client: async job
    polling, local sub-10ms queries with cloud fallback, metadata filtering,
    custom embeddings, and sessions.

Also includes unit tests, an env-gated live integration test, runnable samples,
a live validation harness, shared RuboCop config, CHANGELOGs, and READMEs;
AGENTS.md is updated to document the SDK.

require "moss"

client = Moss::Client.new # creds from MOSS_PROJECT_ID / MOSS_PROJECT_KEY
client.create_index("support-docs", documents)
client.load_index("support-docs")
client.query("support-docs", "how long do refunds take?", top_k: 3)

Testing: bundle exec rake test green for both gems and bundle exec rubocop clean (0 offenses). Validated live against a real Moss project (native
libmoss runtime): create + async job polling, get/list/delete index,
load/unload, semantic query, $eq and $in metadata filters, and
custom-embedding indexes. Sessions are implemented and unit-tested; the live
session round-trip requires an enterprise plan and is skipped gracefully
otherwise. Native/E2E tests auto-skip when libmoss or credentials are absent,
matching the other SDKs.

Note for reviewers: local search needs the libmoss C SDK at runtime
(download from the c-sdk-v0.9.0 release and point MOSS_LIB_DIR at it); cloud
queries work without it. Targets Ruby 3.0+.

Fixes #430

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Adds a Ruby SDK over the Moss runtime with parity on index management,
search, and metadata filtering, following the two-layer structure of the
existing language SDKs.

Two gems under sdks/ruby/:

- moss-core (sdks/ruby/bindings): FFI bindings over the prebuilt libmoss
  C SDK, exposing ManageClient, IndexManager, and Session. Attaches
  libmoss lazily and degrades to a cloud-query fallback /
  BindingsUnavailableError when it is absent (mirrors the Go SDK).
- moss (sdks/ruby/sdk): the high-level Moss::Client with async job
  polling, local sub-10ms queries with cloud fallback, metadata
  filtering, custom embeddings, and sessions.

Includes unit tests, an env-gated live integration test, runnable
samples, a live validation harness, shared RuboCop config, CHANGELOGs,
and READMEs. Documents the SDK in AGENTS.md.

Closes usemoss#430.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fm5bzWU8BRe6WtmFMRKify
Copilot AI review requested due to automatic review settings July 23, 2026 01:57
@CLAassistant

CLAassistant commented Jul 23, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Ruby SDK to Moss under sdks/ruby/, following the repository’s two-layer pattern: a high-level moss gem and a low-level moss-core FFI bindings gem over libmoss, plus tests, samples, and a live validation harness.

Changes:

  • Introduces moss-core Ruby FFI bindings exposing ManageClient, IndexManager, and Session over the libmoss C SDK.
  • Introduces the high-level moss gem (Moss::Client) with cloud-backed mutations, local-query routing with cloud fallback, metadata filtering, custom embeddings, and sessions.
  • Adds unit tests, env-gated live integration tests, runnable samples, and a credential-scrubbed validation script; updates repo agent docs.

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sdks/ruby/sdk/test/test_helper.rb Test scaffolding (fake clients/managers + simple stubbing helper).
sdks/ruby/sdk/test/models_test.rb Unit tests for Ruby SDK models and CloudQuery parsing/errors.
sdks/ruby/sdk/test/integration_test.rb Env/libmoss-gated end-to-end integration test against live Moss.
sdks/ruby/sdk/test/client_test.rb Unit tests for Moss::Client behavior (validation, polling, routing, mapping).
sdks/ruby/sdk/scripts/validate.rb Live validation harness (loads creds, provisions libmoss, runs full workflow).
sdks/ruby/sdk/samples/session_usage_sample.rb Runnable sessions sample (enterprise-plan feature).
sdks/ruby/sdk/samples/metadata_filtering_sample.rb Runnable metadata filtering sample demonstrating operators.
sdks/ruby/sdk/samples/custom_embeddings_sample.rb Runnable custom-embedding index + embedding query sample.
sdks/ruby/sdk/samples/comprehensive_sample.rb Runnable end-to-end “create/load/query/read/cleanup” sample.
sdks/ruby/sdk/samples/.env.template Sample env template for credentials + libmoss location.
sdks/ruby/sdk/README.md End-user documentation for installation, usage, and features.
sdks/ruby/sdk/Rakefile Test + RuboCop rake tasks for the moss gem.
sdks/ruby/sdk/moss.gemspec Gem metadata and dependencies for the high-level Ruby SDK.
sdks/ruby/sdk/LICENSE License file for the moss gem.
sdks/ruby/sdk/lib/moss/version.rb Ruby SDK version constant.
sdks/ruby/sdk/lib/moss/session.rb High-level session wrapper around Moss::Core::Session.
sdks/ruby/sdk/lib/moss/models.rb High-level Ruby SDK value objects and constants.
sdks/ruby/sdk/lib/moss/errors.rb Ruby SDK error hierarchy (configuration, args, job, HTTP, unsupported query).
sdks/ruby/sdk/lib/moss/cloud_query.rb Cloud query fallback implementation (Net::HTTP POST + response mapping).
sdks/ruby/sdk/lib/moss/client.rb Main Moss::Client implementation (polling, routing, conversions, lifecycle).
sdks/ruby/sdk/lib/moss.rb Gem entrypoint and helpers (native_runtime_available?, libmoss version).
sdks/ruby/sdk/Gemfile Development Gemfile pointing to sibling bindings during in-repo development.
sdks/ruby/sdk/CHANGELOG.md Changelog for the Ruby SDK gem.
sdks/ruby/sdk/.rubocop.yml RuboCop config inheriting from shared sdks/ruby/.rubocop.yml.
sdks/ruby/README.md Top-level Ruby SDK overview describing the two-gem structure.
sdks/ruby/bindings/test/test_helper.rb Test helper for bindings gem.
sdks/ruby/bindings/test/library_test.rb Bindings tests for library resolution and (optional) attach/version smoke test.
sdks/ruby/bindings/README.md Documentation for moss-core bindings usage and libmoss provisioning.
sdks/ruby/bindings/Rakefile Test + RuboCop rake tasks for the moss-core gem.
sdks/ruby/bindings/moss-core.gemspec Gem metadata and dependencies for the bindings gem (ffi).
sdks/ruby/bindings/LICENSE License file for the bindings gem.
sdks/ruby/bindings/lib/moss/core/version.rb Bindings gem version + pinned libmoss ABI version.
sdks/ruby/bindings/lib/moss/core/session.rb FFI-backed Session implementation wrapping MossSession*.
sdks/ruby/bindings/lib/moss/core/models.rb Core-layer value objects and option structs for binding surfaces.
sdks/ruby/bindings/lib/moss/core/marshalling.rb Ruby<->C struct marshalling and result-code checking utilities.
sdks/ruby/bindings/lib/moss/core/manage_client.rb FFI-backed manage client for cloud mutations/reads + session creation.
sdks/ruby/bindings/lib/moss/core/library.rb Lazy libmoss discovery/attach logic with env overrides and graceful fallback.
sdks/ruby/bindings/lib/moss/core/index_manager.rb FFI-backed local runtime (load/unload/query/refresh + loaded-index bookkeeping).
sdks/ruby/bindings/lib/moss/core/ffi.rb Raw FFI ABI mapping to libmoss symbols and C structs.
sdks/ruby/bindings/lib/moss/core/errors.rb Bindings-layer error types (unavailable, closed, native error codes).
sdks/ruby/bindings/lib/moss/core/client_handle.rb Thread-safe owner of native MossClient* handle with finalizer safety net.
sdks/ruby/bindings/lib/moss/core.rb Bindings gem entrypoint exposing availability + libmoss version helpers.
sdks/ruby/bindings/Gemfile Development Gemfile for bindings (includes RuboCop).
sdks/ruby/bindings/CHANGELOG.md Changelog for moss-core.
sdks/ruby/bindings/.rubocop.yml RuboCop config inheriting from shared sdks/ruby/.rubocop.yml.
sdks/ruby/.rubocop.yml Shared RuboCop configuration for both Ruby gems.
sdks/ruby/.gitignore Gitignore for vendored libmoss and Ruby build artifacts.
AGENTS.md Updates agent guidance/docs to include Ruby SDK and related commands.
Comments suppressed due to low confidence (1)

sdks/ruby/bindings/lib/moss/core/manage_client.rb:145

  • SessionOptions uses an instance variable to retain the model_id pointer; in concurrent calls this can be overwritten before the native call executes, leading to a dangling pointer/use-after-free. Build the options struct (and the model_id MemoryPointer) as locals so they stay alive for the duration of the FFI call and remain thread-safe.
      def session(name, options = nil)
        opts_ptr = session_options_pointer(options)
        out = ::FFI::MemoryPointer.new(:pointer)

        @handle.with_handle do |client|
          Marshalling.check!(FFIBindings.moss_client_session(client, name.to_s, opts_ptr, out))
        end

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +40 to +55
def add_docs(name, docs, options = nil)
input = Marshalling.build_documents(docs)
opts_ptr = mutation_options_pointer(options)
out = ::FFI::MemoryPointer.new(:pointer)

@handle.with_handle do |client|
Marshalling.check!(
FFIBindings.moss_client_add_docs(
client, name.to_s, input.pointer, docs.length, opts_ptr, out
)
)
end
_retain(input)

read_and_free_mutation_result(out)
end
Comment on lines +30 to +38
def load_index(index_name, options = nil)
opts_ptr = load_index_options_pointer(options)
out = ::FFI::MemoryPointer.new(:pointer)

@handle.with_handle do |client|
Marshalling.check!(
FFIBindings.moss_client_load_index(client, index_name.to_s, opts_ptr, out)
)
end
Comment on lines +34 to +50
def add_docs(docs, options = nil)
input = Marshalling.build_documents(docs)
opts_ptr = add_docs_options_pointer(options)
added = ::FFI::MemoryPointer.new(:size_t)
updated = ::FFI::MemoryPointer.new(:size_t)

with_handle do |session|
Marshalling.check!(
FFIBindings.moss_session_add_docs(
session, input.pointer, docs.length, opts_ptr, added, updated
)
)
end
_retain(input)

Core::SessionAddResult.new(added: added.read(:size_t), updated: updated.read(:size_t))
end
Comment on lines +380 to +386
def resolve_query_url(explicit)
value = string_or_nil(explicit) || string_or_nil(ENV.fetch("MOSS_CLOUD_QUERY_URL", nil))
return value if value
return nil if @manage_url.nil? || @manage_url.empty?

@manage_url.sub("/v1/manage", "/query")
end
…y-URL guard

Follow-up to review feedback on usemoss#463:

- Route the mutation / add-docs / load-index / session option structs through
  Marshalling.build_*_options, which keep the FFI::Struct (and any backing
  strings) referenced for the duration of the native call via the same
  Allocation retain pattern already used for query options. FFI::Struct#to_ptr
  returns the owning MemoryPointer, so this was already memory-safe on CRuby;
  the change makes the lifetime explicit and consistent and removes an
  instance-variable hack in ManageClient#session.

- resolve_query_url returns nil when the manage->query substitution does not
  change the URL, so the cloud fallback fails fast with a ConfigurationError
  instead of silently POSTing to the manage endpoint. Adds unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pulipakaa24

Copy link
Copy Markdown
Author

Thanks for the review! Addressed the feedback in c8f4297.

FFI option-struct lifetimes (manage_client.rb, index_manager.rb, session.rb) — I looked into the use-after-free concern: FFI::Struct#to_ptr returns the owning MemoryPointer, and the caller holds that pointer for the whole native call, so the backing memory stays alive even after the Struct wrapper is collected (I confirmed it survives a forced full GC). So there wasn't an actual UAF on CRuby — but the retention was inconsistent: query options were retained explicitly while the small option structs relied on to_ptr ownership. I've routed all of them (mutation / add-docs / load-index / session) through a shared Marshalling.build_*_options retain pattern, so the struct lifetime is now explicit and uniform. This also removed an instance-variable hack in ManageClient#session.

resolve_query_url (client.rb) — good catch. It now returns nil when the /v1/manage/query substitution doesn't actually change the URL, so a custom manage_url with no explicit query URL makes the cloud fallback fail fast with a ConfigurationError instead of silently POSTing to the manage endpoint. Added unit tests covering both the failure and the explicit-query_url cases.

RuboCop is clean and the unit suite + full live end-to-end run (create → load → query → $eq/$in filters → custom embeddings) pass with no regression.

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.

SDK: Ruby client for Moss

4 participants