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.
Virtualruns in memory with zero host access.#shellraisesNotImplementedErroron purpose, that’s the safety property, not a bug in waiting.Localgives you the host filesystem and shell for trusted dev and CI work. Every path is guarded to stay insidecwd, and the shell only seesPATH,HOME, andLANG. It never sees your full environment.Containerruns tools inside a throwaway Docker or Applecontainersandbox, 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.Remoteis 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.