
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.

```ruby
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](/documentation/nexo/sandboxes/#container-sandbox--docker--apple-container).
- **`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:

```ruby
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](/documentation/nexo/durable-workflows/).

## 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](/documentation/nexo/sandboxes/#container-sandbox--docker--apple-container)
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`](https://github.com/crmne/ruby_llm) and
everything built around it, is solid.

The [Nexo documentation](/documentation/nexo/) 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](https://github.com/maquina-app/nexo)
