Class: Insika::Sandbox::Boundary
- Inherits:
-
Object
- Object
- Insika::Sandbox::Boundary
- Defined in:
- lib/insika/sandbox/boundary.rb
Overview
HARD filesystem boundary: confines every path a tool resolves to a single root directory. Each model/user supplied path is resolved and verified to live INSIDE the root BEFORE any IO happens. This is the FS half of the sandbox primitive — always on, independent of the exec provider (local or docker) and of the engine's approval layer.
Extracted VERBATIM from the insika-code prototype's Workspace (item 5)
and promoted to a core primitive (item 35). The two layers of defense:
1. `File.expand_path` normalizes `..` traversal; the expanded path must be
the root itself or a descendant of it (string containment on a
separator boundary, so `/ws-evil` does not pass for root `/ws`).
2. symlink guard: the final path component may never be a symlink (an
lstat check that also catches a BROKEN symlink whose target does not
yet exist), and for an existing target (or, on writes, its parent dir)
the REAL path (`File.realpath`, which follows symlinks) must also be
contained — a symlink inside the sandbox pointing outside is rejected.
A value object: immutable root, no IO of its own beyond the realpath checks.
Constant Summary collapse
- Escape =
Raised on any attempt to touch a path outside the root. Tools rescue it and return a structured error to the model (never crash a turn).
Class.new(StandardError)
Instance Attribute Summary collapse
-
#root ⇒ Object
readonly
Returns the value of attribute root.
Instance Method Summary collapse
-
#initialize(root) ⇒ Boundary
constructor
root: the directory that bounds all operations.
-
#inside?(abs) ⇒ Boolean
Boolean containment check for an ALREADY-absolute path (used by grep to skip glob results that resolve outside the root via a symlink).
-
#relative(abs) ⇒ Object
Path relative to the root, for display — never leaks absolute host paths back to the model.
-
#resolve(path, for_write: false) ⇒ Object
Resolve a relative/absolute path to an absolute path GUARANTEED inside the root.
Constructor Details
#initialize(root) ⇒ Boundary
root: the directory that bounds all operations. Must exist (a boundary rooted at a missing dir is a misconfiguration -> fail fast at boot).
34 35 36 37 38 |
# File 'lib/insika/sandbox/boundary.rb', line 34 def initialize(root) @root = File.realpath(File.(root.to_s)) rescue Errno::ENOENT raise Escape, "sandbox root does not exist: #{root}" end |
Instance Attribute Details
#root ⇒ Object (readonly)
Returns the value of attribute root.
30 31 32 |
# File 'lib/insika/sandbox/boundary.rb', line 30 def root @root end |
Instance Method Details
#inside?(abs) ⇒ Boolean
Boolean containment check for an ALREADY-absolute path (used by grep to skip glob results that resolve outside the root via a symlink). Never raises.
67 68 69 70 71 72 |
# File 'lib/insika/sandbox/boundary.rb', line 67 def inside?(abs) real = File.exist?(abs) ? File.realpath(abs) : File.(abs) real == @root || real.start_with?(@root + File::SEPARATOR) rescue StandardError false end |
#relative(abs) ⇒ Object
Path relative to the root, for display — never leaks absolute host paths back to the model. The root itself renders as ".".
76 77 78 79 80 |
# File 'lib/insika/sandbox/boundary.rb', line 76 def relative(abs) return "." if abs == @root abs.delete_prefix(@root + File::SEPARATOR) end |
#resolve(path, for_write: false) ⇒ Object
Resolve a relative/absolute path to an absolute path GUARANTEED inside the root. Raises Escape on traversal/symlink escape or an empty path. for_write: the target file may not exist yet, so the symlink guard is applied to its PARENT directory instead of the file itself.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/insika/sandbox/boundary.rb', line 44 def resolve(path, for_write: false) raise Escape, "empty path" if path.to_s.strip.empty? abs = File.(path.to_s, @root) contain!(abs) # The final component is never allowed to be a symlink. On writes this is # the crux of the boundary: `contain!` only checks the STRING, and the # parent-dir realpath probe below only vets the parent — so without this a # symlink under the root pointing outside would let `File.write` follow it # and clobber a file outside the sandbox. `File.symlink?` is an lstat, so # it also catches a BROKEN symlink (dangling target) that `File.exist?` # would report as absent. raise Escape, "path is a symlink" if File.symlink?(abs) probe = for_write ? File.dirname(abs) : abs contain!(File.realpath(probe)) if File.exist?(probe) abs end |