Skip to main content

July 20, 2026

Introducing Nexo: the harness for Ruby agents

Mario Alberto Chávez Cárdenas
Nexo — the harness for Ruby AI agents

Ruby has most of the pieces you need to build an AI agent already, and they’re good. ruby_llm gives you a provider-neutral chat loop and tool calling that’s cleaner than most language SDKs out there, one API across a dozen providers instead of a new SDK per vendor. ruby_llm-skills gives you SKILL.md loading. ruby_llm-mcp gives you MCP servers. ruby_llm-schema gives you structured output. What none of them give you is a front door, one place where those pieces snap together with defaults you don’t have to think about twice.

Building an agent on top of these gems means wiring the same sandbox and permissions boilerplate into every new project, by hand, every time. Nexo exists to stop that. It doesn’t rebuild the tool-call loop, skill loading, MCP, or structured output; those already exist, they’re well built, and duplicating them would just make Nexo worse at the things ruby_llm and its ecosystem already do well. Nexo sits on top of them instead. Two things were missing from the ecosystem, and those are the two things Nexo adds: a sandbox and permissions seam, and a real job primitive called WorkflowRun.

Agent = Model + Harness

A model on its own forgets everything the moment it finishes a response. Turning “a model that replies” into “an agent that does work” takes tools, a place for those tools to act, a policy for what they’re allowed to do, and a way to track a job from start to finish. That collection of things is the harness. Ruby has all of it, but scattered across gems with different DSLs, and wiring them by hand is the first thing every new agent project reinvents.

Nexo is the nexus: the connective tissue between a model and everything else an agent needs, and between the fragmented RubyLLM-ecosystem gems and one coherent whole.

require "nexo"

class CodeReviewer < Nexo::Agent
  model       ENV.fetch("NEXO_MODEL")   # any ruby_llm model, no vendor default
  sandbox     :local
  permissions :read_only

  instructions "You are a careful code reviewer. Read files and report issues. Do not write files."
end

CodeReviewer.new(cwd: "/path/to/repo").prompt("Review the auth module")

What Nexo adds: a sandbox and permissions seam

An agent’s tools have to act somewhere, and something has to decide what they’re allowed to do there. Nexo splits that into two questions, where (the sandbox) and what (the permission mode), and answers both safely by default: :virtual sandbox, :read_only permissions. An untrusted model gets zero host access until you opt into more, explicitly, in your own code.

  • Virtual runs in memory with zero host access. #shell raises NotImplementedError on purpose, that’s the safety property, not a bug in waiting.
  • Local gives you the host filesystem and shell for trusted dev and CI work. Every path is guarded to stay inside cwd, and the shell only sees PATH, HOME, and LANG. It never sees your full environment.
  • Container runs tools inside a throwaway Docker or Apple container sandbox, hardened by default (no network, dropped capabilities, read-only rootfs), with every hardening an explicit opt-out. Full flag reference is in the sandboxes doc.
  • Remote is a four-method contract, read, write, exec, close, that you adapt to whatever provider you’re already using. Nexo ships zero vendor code here; swap the injected object and you’ve swapped providers.

A write under :read_only, a shell call on Virtual, any denied action comes back as { error: ... } and the loop keeps running. The model can read that and adjust; a permission denial isn’t a reason to crash the process. A path that tries to escape its sandbox is a different kind of failure, and it raises SecurityError instead.

What Nexo adds: WorkflowRun

An agent accumulates context, it’s a conversation that keeps going. A lot of real work isn’t that. It’s a finite task that starts, does something, and finishes with a result you can check on later, sometimes from a different process entirely. Nothing in the ecosystem covered that cleanly, so Nexo adds Workflow.

A workflow can drive an agent too, so the two primitives Nexo owns fold into one recipe: stage inputs into the run’s sandbox, run the agent against them, capture the output. The agent class macro names the agent, and run_agent runs it bound to the run’s own sandbox:

class Summarizer < Nexo::Agent
  model ENV.fetch("NEXO_MODEL")   # any ruby_llm model, no vendor default

  instructions "Summarize the given text in two plain sentences. No commentary."
end

class SummarizeDocument < Nexo::Workflow
  agent Summarizer

  def call(payload)
    emit(:started, doc_id: payload[:doc_id])
    response = run_agent("Summarize this document:\n\n#{payload[:text]}")
    emit(:summarized, length: response.content.length)
    { summary: response.content }
  end
end

run = SummarizeDocument.run(doc_id: 123, text: "Long text...")
run.id      # => "0191d6b2-..."  (UUID v7, time-ordered)
run.status  # => "done"
run.result  # => { "summary" => "The document covers ..." }

Every run gets a stable id, a status, a payload, a result, and an ordered event log you can replay with Nexo::Workflow.logs(run.id) or rake nexo:logs[id]. The agent’s tool calls and its final response land in that same log alongside the workflow’s own :started/:summarized events, so a driven run reads as one story instead of two logs stitched together by hand. Outside Rails it all records to memory and just works offline, no database required; inside Rails, install the migration and the same code persists to a nexo_workflow_runs table instead, with no other change.

A workflow that raises is recorded as failed and the exception still propagates to your caller, the opposite of a tool failure, which returns { error: ... } and never interrupts the agent loop. Jobs that need to pause and pick back up later, waiting on a human approval or a slow external process, get the same lifecycle extended with checkpoints, suspend!, and resume, covered in the durable workflows doc.

Provider-neutral, on purpose

The only hard dependency is ruby_llm. No hardcoded default model, no vendor SDK called directly anywhere in the core loop; every example in the docs pulls the model from ENV.fetch("NEXO_MODEL"), just as happy pointing at a local Ollama model as a hosted one. ruby_llm-skills, ruby_llm-mcp, and an optional Anthropic-oriented autonomous loop are all soft dependencies, required lazily, and they raise a clear Nexo::MissingDependencyError with install instructions if you haven’t added them. You only pay for what you use.

Where it stands today

Nexo is early and the API isn’t stable yet. The Apple container runtime parity table in the docs says so plainly: Docker is the verified path today, and Apple’s CLI flags are encoded from the reference mapping but not yet confirmed against a live daemon. The caveat is published rather than papered over.

What’s shipped: the sandbox and permissions seam across Virtual, Local, Container, and Remote; the Workflow/WorkflowRun lifecycle with staging, artifacts, and durable checkpoints; skills, MCP behind a fail-closed gate, a web fetch tool with an SSRF guard, sessions, and a Rails engine with generators for the conventional app/agents / app/workflows / app/skills layout.

gem "nexo_ai"

The gem publishes as nexo_ai; everything in the code lives under Nexo::.

None of this exists without the RubyLLM ecosystem underneath it. Nexo was worth building because that foundation, ruby_llm and everything built around it, is solid.

The Nexo documentation has the full index, sandboxes, permissions, tools, loop backends, workflows, durable workflows, skills, MCP, web, sessions, Rails, and concurrency. If you’re already building on ruby_llm and you’ve written this same sandbox-and-permissions setup more than once, this is for you.

Repo: github.com/maquina-app/nexo


Need help with your Rails project?

I'm Mario Alberto Chávez—Rails architect available for consulting, AI integration, and code review.