Class: Nexo::Sandbox
- Inherits:
-
Object
- Object
- Nexo::Sandbox
- Defined in:
- lib/nexo/sandbox.rb
Overview
The execution-environment seam. A sandbox is where an agent's tools actually touch files and run commands; swapping the sandbox swaps the whole execution context (in-memory, host, or — later — remote) by constructor injection.
Concrete sandboxes implement the four-method contract below. The base class
raises NotImplementedError for each so an incomplete subclass fails loudly.
See Sandboxes::Virtual (default, zero host access) and Sandboxes::Local (host filesystem + shell, guarded).
Direct Known Subclasses
Nexo::Sandboxes::Container, Nexo::Sandboxes::Local, Nexo::Sandboxes::Remote, Nexo::Sandboxes::Virtual
Constant Summary collapse
- PROBE_COMMANDS =
Commands the default #environment probe looks for. Deliberately short — each entry costs a
command -vplus one--versionwhen found — and extensible per call for anything else a skill's script might need. %w[ruby python3 node sh].freeze
- PROBE_SCRIPT =
One POSIX
shscript, no interpreter required on the far side, so the probe works on a busybox image.command -vlocates each command and a single--versionreports it; the format placeholder is filled by #environment. <<~SH printf 'locale=%%s\\n' "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" for c in %<commands>s; do p=$(command -v "$c" 2>/dev/null) || continue v=$("$c" --version 2>/dev/null | head -1) printf 'cmd=%%s\\t%%s\\t%%s\\n' "$c" "$p" "$v" done SH
Class Method Summary collapse
-
.empty_environment ⇒ Object
The shape #environment always answers with, built fresh on every call.
Instance Method Summary collapse
-
#close ⇒ Object
Releases any resources the sandbox holds.
-
#environment(commands: PROBE_COMMANDS) ⇒ Object
What this execution environment actually provides, as data:.
-
#glob(pattern) ⇒ Object
Returns the paths matching the glob
pattern. -
#instructions ⇒ Object
A short, plain-text description of the execution environment (cwd, host access, network) for the agent to inject into the system prompt.
-
#mtime(path) ⇒ Object
The last-modified time of
path, used by the read-before-write + stale guard for real-filesystem sandboxes. -
#read(path) ⇒ Object
Returns the contents of
pathas a String. -
#shell(command, timeout: 30) ⇒ Object
Runs
commandand returns{ stdout:, stderr:, status: }(status is the integer exit code). -
#supports?(capability) ⇒ Boolean
Whether the sandbox supports
capability(one of:read,:write,:glob,:shell). -
#write(path, content) ⇒ Object
Writes
contenttopath.
Class Method Details
.empty_environment ⇒ Object
The shape #environment always answers with, built fresh on every call.
Deliberately a method and not a frozen constant: the :commands Hash is
mutated while parsing, and CONST.dup is shallow — sharing one inner Hash
let every probe accumulate into the constant, so a sandbox with no shell
answered with the previous sandbox's findings. :error is nil on a
successful probe and carries the reason when one could not be run.
75 76 77 |
# File 'lib/nexo/sandbox.rb', line 75 def self.empty_environment {commands: {}, locale: nil, error: nil} end |
Instance Method Details
#close ⇒ Object
Releases any resources the sandbox holds. No-op by default.
36 37 38 |
# File 'lib/nexo/sandbox.rb', line 36 def close nil end |
#environment(commands: PROBE_COMMANDS) ⇒ Object
What this execution environment actually provides, as data:
sandbox.environment
# => { commands: { "ruby" => { path: "/usr/local/bin/ruby", version: "4.0.0" } },
# locale: "C.UTF-8" }
#instructions describes the environment for the model; this is the same question answered for code, so a caller can check before staging a skill's script rather than discovering the answer as a stack trace several turns in. A container typically has no locale at all, under which Ruby's default external encoding is US-ASCII and a bare File.read on a UTF-8 file raises — and an image can carry a full Ruby toolchain and still report no locale, so the two are reported as independent axes.
Deliberately coarse: commands on PATH and the locale, never packages. Gems,
wheels and npm modules belong to whoever builds the image, and modelling them
here would be a cross-language dependency resolver competing with the manifest
every ecosystem already has.
Costs one #shell round trip and is memoized for the sandbox's lifetime (0.14s
on :local, 0.25–0.48s on a container, measured). A sandbox with no shell
A sandbox with no shell (Virtual) reports empty. A probe that fails for any
other reason — the container would not start, the client is unreachable —
also reports empty, but carries the reason under :error: this is
diagnostics and must never be the reason a run dies, yet "I probed and found
nothing" and "I could not probe" are different answers and a caller building
an error message needs to tell them apart.
120 121 122 123 |
# File 'lib/nexo/sandbox.rb', line 120 def environment(commands: PROBE_COMMANDS) @environment ||= {} @environment[commands] ||= probe_environment(commands) end |
#glob(pattern) ⇒ Object
Returns the paths matching the glob pattern.
31 32 33 |
# File 'lib/nexo/sandbox.rb', line 31 def glob(pattern) raise NotImplementedError end |
#instructions ⇒ Object
A short, plain-text description of the execution environment (cwd, host
access, network) for the agent to inject into the system prompt. Base
returns nil — inject nothing. Real-filesystem sandboxes (Local,
Container) override this so a weak tool-caller knows where it runs.
44 45 46 |
# File 'lib/nexo/sandbox.rb', line 44 def instructions nil end |
#mtime(path) ⇒ Object
The last-modified time of path, used by the read-before-write + stale
guard for real-filesystem sandboxes. Base returns nil (no external
mutation to guard against, e.g. Virtual), which disables the guard.
60 61 62 |
# File 'lib/nexo/sandbox.rb', line 60 def mtime(path) nil end |
#read(path) ⇒ Object
Returns the contents of path as a String.
15 16 17 |
# File 'lib/nexo/sandbox.rb', line 15 def read(path) raise NotImplementedError end |
#shell(command, timeout: 30) ⇒ Object
Runs command and returns { stdout:, stderr:, status: } (status is the
integer exit code). timeout is in seconds.
26 27 28 |
# File 'lib/nexo/sandbox.rb', line 26 def shell(command, timeout: 30) raise NotImplementedError end |
#supports?(capability) ⇒ Boolean
Whether the sandbox supports capability (one of :read, :write,
:glob, :shell). The base supports everything but :shell (an
in-memory sandbox has no process to run a command in), so an agent only
attaches the Shell tool when the sandbox reports it. Real-process
sandboxes (Local, Container) override to add :shell.
53 54 55 |
# File 'lib/nexo/sandbox.rb', line 53 def supports?(capability) capability != :shell end |
#write(path, content) ⇒ Object
Writes content to path.
20 21 22 |
# File 'lib/nexo/sandbox.rb', line 20 def write(path, content) raise NotImplementedError end |