Class: Nexo::Sandboxes::Local
- Inherits:
-
Nexo::Sandbox
- Object
- Nexo::Sandbox
- Nexo::Sandboxes::Local
- Defined in:
- lib/nexo/sandboxes/local.rb
Overview
Host filesystem + shell sandbox, for trusted dev/CI use. The developer opts into this explicitly (the default is Virtual); two guards keep a model-driven agent contained:
- Path-escape guard — every +read+/+write+ path is expanded against
cwdand must stay inside it, otherwiseSecurityErroris raised. - Narrowed ENV — the shell sees only PATH, HOME, LANG (plus explicit
env:additions), never the full process environment.
Under a fiber reactor (Spec 5) these blocking file/shell syscalls would
stall every other concurrent fiber. When Nexo.config.concurrency == :async
each operation is offloaded to a worker thread (see #offload); otherwise
it runs inline, byte-for-byte the Spec 1 behavior with zero overhead. The
offload never changes return values or the security properties below.
Instance Attribute Summary collapse
-
#cwd ⇒ Object
readonly
The expanded workspace root; every path is guarded to stay inside it.
Instance Method Summary collapse
- #glob(pattern) ⇒ Object
-
#initialize(cwd: Dir.pwd, env: {}) ⇒ Local
constructor
Roots the sandbox at
cwd(expanded, defaultDir.pwd) and narrows the shell environment to +PATH+/+HOME+/+LANG+ plus any explicitenv:entries. -
#instructions ⇒ Object
A plain-text description of this host environment for the agent's system prompt: the host cwd, that the real host filesystem and shell are reachable, and that access is guarded to
cwd. -
#mtime(path) ⇒ Object
The last-modified time of
path(guarded against escapingcwd), ornilwhen the file is absent — so a new-file write skips the read-before-write guard. - #read(path) ⇒ Object
- #shell(command, timeout: 30) ⇒ Object
-
#supports?(cap) ⇒ Boolean
Supports all four capabilities — unlike Virtual, a real process runs the shell command here.
- #write(path, content) ⇒ Object
Methods inherited from Nexo::Sandbox
Constructor Details
#initialize(cwd: Dir.pwd, env: {}) ⇒ Local
Roots the sandbox at cwd (expanded, default Dir.pwd) and narrows the
shell environment to +PATH+/+HOME+/+LANG+ plus any explicit env: entries.
29 30 31 32 33 34 35 36 |
# File 'lib/nexo/sandboxes/local.rb', line 29 def initialize(cwd: Dir.pwd, env: {}) @cwd = File.(cwd) # The physical root (symlinks resolved), used by the escape guards so a # symlink inside cwd can't leak a target outside it. @real_cwd = File.exist?(@cwd) ? File.realpath(@cwd) : @cwd # Deliberately narrow env — never hand a model-driven shell the whole ENV. @env = ENV.to_h.slice("PATH", "HOME", "LANG").merge(env) end |
Instance Attribute Details
#cwd ⇒ Object (readonly)
The expanded workspace root; every path is guarded to stay inside it.
25 26 27 |
# File 'lib/nexo/sandboxes/local.rb', line 25 def cwd @cwd end |
Instance Method Details
#glob(pattern) ⇒ Object
42 43 44 45 46 47 48 49 50 51 |
# File 'lib/nexo/sandboxes/local.rb', line 42 def glob(pattern) # Guard the pattern itself so it can't escape via "../.." (metacharacters # like * are preserved by expand_path), then drop any match whose real # path (symlinks resolved) lands outside the sandbox. full = File.(pattern, @cwd) unless within?(full, @cwd) raise SecurityError, "glob pattern escapes sandbox: #{pattern}" end offload { Dir.glob(full) }.select { |match| real_within?(match) } end |
#instructions ⇒ Object
A plain-text description of this host environment for the agent's system
prompt: the host cwd, that the real host filesystem and shell are
reachable, and that access is guarded to cwd.
64 65 66 67 |
# File 'lib/nexo/sandboxes/local.rb', line 64 def instructions "You run on the host machine, cwd #{@cwd}. The real host filesystem " \ "and shell are reachable; file access is guarded to #{@cwd}." end |
#mtime(path) ⇒ Object
The last-modified time of path (guarded against escaping cwd), or
nil when the file is absent — so a new-file write skips the
read-before-write guard.
78 79 80 81 |
# File 'lib/nexo/sandboxes/local.rb', line 78 def mtime(path) full = absolute(path) File.exist?(full) ? File.mtime(full) : nil end |
#read(path) ⇒ Object
38 39 40 |
# File 'lib/nexo/sandboxes/local.rb', line 38 def read(path) offload { File.read(absolute(path)) } end |
#shell(command, timeout: 30) ⇒ Object
83 84 85 86 87 88 89 90 91 92 |
# File 'lib/nexo/sandboxes/local.rb', line 83 def shell(command, timeout: 30) offload do # ruby_llm's target Ruby has no `timeout:` kwarg on Open3.capture3 # (verified), so the wall-clock bound lives in Timeout.timeout. out, err, status = Timeout.timeout(timeout) do Open3.capture3(@env, command, chdir: @cwd) end {stdout: out, stderr: err, status: status.exitstatus} end end |
#supports?(cap) ⇒ Boolean
Supports all four capabilities — unlike Virtual, a real process runs the shell command here.
71 72 73 |
# File 'lib/nexo/sandboxes/local.rb', line 71 def supports?(cap) %i[read write shell glob].include?(cap) end |
#write(path, content) ⇒ Object
53 54 55 56 57 58 59 |
# File 'lib/nexo/sandboxes/local.rb', line 53 def write(path, content) offload do full = absolute(path) FileUtils.mkdir_p(File.dirname(full)) File.write(full, content) end end |