agent_jail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cbe7c9edb7dc3c7d7300b5617162dd80bc63d278d83a42f8afbede50bf2e45f1
4
+ data.tar.gz: f50d55b8ff825febc26a1c74592f91ca70ca0fcd0ee90753cf44d8aac4461b7d
5
+ SHA512:
6
+ metadata.gz: 6a2f0343e61d99643735a35d6cba6b871fe9f176bf2168375c88e4d0bc7150da8b07717bafd4873055a2ec60d40520c9072f678c3184ecd71fbdd8a89df76a45
7
+ data.tar.gz: a9964bff1b0ffc418cda699527b2aa0e7fd7c1e7830314c849a08a9e1dfe355860635aa099ba4e62bba4ac35f3fe2c9df8e27c0ada7909ab33a9a58b8495e7bf
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,107 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+ TargetRubyVersion: 3.2
7
+ Exclude:
8
+ - "bin/**/*"
9
+ - "vendor/**/*"
10
+ - "pkg/**/*"
11
+
12
+ Style/StringLiterals:
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/FrozenStringLiteralComment:
16
+ Enabled: true
17
+
18
+ Metrics/MethodLength:
19
+ Max: 25
20
+
21
+ Metrics/BlockLength:
22
+ Max: 50
23
+ Exclude:
24
+ - "spec/**/*"
25
+
26
+ Metrics/ClassLength:
27
+ Max: 150
28
+
29
+ Metrics/ModuleLength:
30
+ Max: 150
31
+
32
+ Style/Documentation:
33
+ Enabled: false
34
+
35
+ Naming/MethodParameterName:
36
+ MinNameLength: 1
37
+
38
+ Naming/PredicateMethod:
39
+ Enabled: false
40
+
41
+ RSpec/VerifiedDoubles:
42
+ Enabled: false
43
+
44
+ RSpec/MessageSpies:
45
+ Enabled: false
46
+
47
+ RSpec/IdenticalEqualityAssertion:
48
+ Enabled: false
49
+
50
+ RSpec/StubbedMock:
51
+ Enabled: false
52
+
53
+ Metrics/AbcSize:
54
+ Max: 40
55
+
56
+ Metrics/CyclomaticComplexity:
57
+ Max: 10
58
+
59
+ Metrics/PerceivedComplexity:
60
+ Max: 12
61
+
62
+ Metrics/ParameterLists:
63
+ Max: 8
64
+
65
+ RSpec/SpecFilePathFormat:
66
+ Enabled: false
67
+
68
+ RSpec/MultipleExpectations:
69
+ Max: 8
70
+
71
+ RSpec/ExampleLength:
72
+ Max: 30
73
+
74
+ RSpec/MultipleMemoizedHelpers:
75
+ Max: 10
76
+
77
+ Lint/EmptyBlock:
78
+ Exclude:
79
+ - "spec/**/*"
80
+
81
+ # Security/MarshalLoad is acknowledged — we only load data we wrote ourselves
82
+ # (child → parent pipe), never from external input.
83
+ Security/MarshalLoad:
84
+ Enabled: false
85
+
86
+ # Lint/RescueException is needed in Child#run to catch all exceptions from the block
87
+ # and forward them over the pipe rather than crashing the child process.
88
+ Lint/RescueException:
89
+ Exclude:
90
+ - "lib/agent_jail/child.rb"
91
+
92
+ # set_memory / set_cpu are not property setters; they apply OS-level resource limits.
93
+ Naming/AccessorMethodName:
94
+ Enabled: false
95
+
96
+ # Spec files legitimately test multiple related classes in one file.
97
+ RSpec/MultipleDescribes:
98
+ Enabled: false
99
+
100
+ # Common RSpec pattern: multiline expect block followed by assertion on the error.
101
+ Style/MultilineBlockChain:
102
+ Enabled: false
103
+
104
+ # $test_agent_jail_var is used to assert fork isolation (child doesn't mutate parent globals).
105
+ Style/GlobalVars:
106
+ Exclude:
107
+ - "spec/**/*"
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] — 2026-06-09
4
+
5
+ ### Added
6
+ - `AgentJail.run` — fork + pipe + monitor pattern for sandboxed block execution
7
+ - Linux Landlock LSM filesystem restrictions (kernel >= 5.13) via FFI syscalls (444/445/446)
8
+ - macOS Seatbelt (`sandbox_init`) filesystem restrictions via FFI
9
+ - POSIX resource limits: address space (`RLIMIT_AS`) and CPU time (`RLIMIT_CPU`) via `setrlimit`
10
+ - Wall-clock timeout enforced by a monitor thread in the parent process
11
+ - Typed exception hierarchy: `TimeoutError`, `MemoryError`, `FilesystemError`, `SandboxError`, `UnsupportedPlatformError`
12
+ - `SandboxError` wraps block exceptions with `original_class`, `original_message`, `original_backtrace`
13
+ - `AgentJail.configure` block with `default_timeout`, `default_memory_mb`, `default_fs_allow`, `on_unsupported`
14
+ - Graceful degradation on unsupported platforms (`:warn`, `:raise`, `:ignore`)
15
+ - `fs_allow` (read-write) and `fs_read_allow` (read-only) per-run path lists
16
+ - Implicit system read-only paths so Ruby functions correctly inside the sandbox
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jibran Usman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # agent_jail
2
+
3
+ **Run LLM-generated tool calls in a sandboxed child process — timeout, memory limit, filesystem restrictions.**
4
+
5
+ [![CI](https://github.com/jibranusman95/agent_jail/actions/workflows/ci.yml/badge.svg)](https://github.com/jibranusman95/agent_jail/actions)
6
+ [![Gem Version](https://badge.fury.io/rb/agent_jail.svg)](https://badge.fury.io/rb/agent_jail)
7
+ [![Downloads](https://img.shields.io/gem/dt/agent_jail)](https://rubygems.org/gems/agent_jail)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
+
10
+ ---
11
+
12
+ You're building a Rails agent. The LLM generates a tool call. The tool call loops forever, allocates 10GB, or tries to read `/etc/passwd`.
13
+
14
+ You have no clean way to stop it.
15
+
16
+ ```ruby
17
+ # No protection — this blocks your process indefinitely
18
+ result = tool.call(llm_generated_args)
19
+
20
+ # Or kills your server
21
+ result = eval(llm_generated_code)
22
+ ```
23
+
24
+ `agent_jail` runs the block in a forked child process with real OS-level restrictions:
25
+
26
+ ```ruby
27
+ result = AgentJail.run(timeout: 5, memory_mb: 256, fs_allow: ["/app/tmp"]) do
28
+ tool.call(llm_generated_args)
29
+ end
30
+ ```
31
+
32
+ The child is isolated. If it misbehaves, it's killed. The parent gets a typed exception — no crash, no data loss, no runaway process.
33
+
34
+ ---
35
+
36
+ ## What you get
37
+
38
+ **Timeout enforcement** — wall-clock timeout via a monitor thread in the parent plus CPU time limit via `RLIMIT_CPU`. A sleeping infinite loop is killed just like a CPU-burning one.
39
+
40
+ **Memory limit** — address space limit via `RLIMIT_AS` (`setrlimit`). The child is killed before it can OOM your server.
41
+
42
+ **Filesystem restrictions (Linux 5.13+)** — Linux Landlock LSM via direct syscalls (no root, no capabilities needed). Paths outside `fs_allow` are inaccessible at the kernel level, not just at the Ruby level.
43
+
44
+ **macOS sandbox** — Seatbelt (`sandbox_init`) profile generated from `fs_allow` and applied in the child.
45
+
46
+ **Typed exceptions** — `TimeoutError`, `MemoryError`, `FilesystemError`, `SandboxError` (wraps block exceptions), `UnsupportedPlatformError`. Catch exactly what you need.
47
+
48
+ **Block isolation** — uses `fork`, so the block has full access to in-memory state (open connections, loaded gems, closures) without serialization. The child's mutations never affect the parent.
49
+
50
+ **Graceful degradation** — on Windows or Linux < 5.13, runs the block with resource limits only (or raises, or silently runs unsandboxed — your choice).
51
+
52
+ ---
53
+
54
+ ## Platform support
55
+
56
+ | Platform | Filesystem isolation | Resource limits | Notes |
57
+ |----------|---------------------|-----------------|-------|
58
+ | Linux >= 5.13 | Landlock LSM | `setrlimit` + wall-clock | Full support |
59
+ | Linux < 5.13 | None (warning logged) | `setrlimit` + wall-clock | Resource limits only |
60
+ | macOS | Seatbelt (`sandbox_init`) | `setrlimit` + wall-clock | Partial — `sandbox_init` is deprecated but functional |
61
+ | Windows | None | None | No-op with warning; set `on_unsupported: :raise` to hard-fail |
62
+ | JRuby / TruffleRuby | None | None | `fork` not available; behaves like Windows |
63
+
64
+ ---
65
+
66
+ ## Install
67
+
68
+ ```ruby
69
+ # Gemfile
70
+ gem "agent_jail"
71
+ ```
72
+
73
+ ```
74
+ bundle install
75
+ ```
76
+
77
+ Requires Ruby >= 3.2.
78
+
79
+ ---
80
+
81
+ ## Usage
82
+
83
+ ### Basic — timeout and memory
84
+
85
+ ```ruby
86
+ require "agent_jail"
87
+
88
+ result = AgentJail.run(timeout: 5, memory_mb: 256) do
89
+ SomeToolCall.execute(args)
90
+ end
91
+ ```
92
+
93
+ ### With filesystem restrictions
94
+
95
+ ```ruby
96
+ result = AgentJail.run(
97
+ timeout: 10,
98
+ memory_mb: 512,
99
+ fs_allow: ["/app/tmp", "/app/uploads"], # read-write
100
+ fs_read_allow: ["/app/config"] # read-only
101
+ ) do
102
+ tool.call(args)
103
+ end
104
+ ```
105
+
106
+ ### All options
107
+
108
+ ```ruby
109
+ result = AgentJail.run(
110
+ timeout: 10, # wall-clock seconds before child is killed (default: 30)
111
+ cpu_timeout: 5, # CPU seconds before child is killed (default: same as timeout)
112
+ memory_mb: 256, # address space limit in MB (default: 512)
113
+ fs_allow: ["/app/tmp"], # read-write paths (default: [])
114
+ fs_read_allow: ["/app/config"] # read-only paths (default: [])
115
+ ) do
116
+ tool.call(args)
117
+ end
118
+ ```
119
+
120
+ ### Configuration
121
+
122
+ ```ruby
123
+ AgentJail.configure do |c|
124
+ c.default_timeout = 30 # seconds
125
+ c.default_memory_mb = 512 # MB
126
+ c.default_fs_allow = []
127
+ c.on_unsupported = :warn # :warn (default), :raise, or :ignore
128
+ end
129
+ ```
130
+
131
+ `on_unsupported` controls behaviour on platforms where sandboxing is unavailable:
132
+
133
+ | Value | Behaviour |
134
+ |-------|-----------|
135
+ | `:warn` | Logs a warning to stderr, runs block unsandboxed |
136
+ | `:raise` | Raises `AgentJail::UnsupportedPlatformError` |
137
+ | `:ignore` | Runs block unsandboxed silently |
138
+
139
+ ---
140
+
141
+ ## Exceptions
142
+
143
+ | Exception | Raised when |
144
+ |-----------|-------------|
145
+ | `AgentJail::TimeoutError` | Block exceeded wall-clock or CPU time limit |
146
+ | `AgentJail::MemoryError` | Block exceeded memory (address space) limit |
147
+ | `AgentJail::FilesystemError` | Block accessed a path outside `fs_allow` (Linux Landlock) |
148
+ | `AgentJail::SandboxError` | Block raised an exception — wraps original with `original_class`, `original_message`, `original_backtrace` |
149
+ | `AgentJail::UnsupportedPlatformError` | Platform can't sandbox and `on_unsupported: :raise` is set |
150
+
151
+ All inherit from `AgentJail::Error < StandardError`.
152
+
153
+ ```ruby
154
+ begin
155
+ AgentJail.run(timeout: 5) { tool.call(args) }
156
+ rescue AgentJail::TimeoutError
157
+ # handle timeout
158
+ rescue AgentJail::MemoryError
159
+ # handle OOM
160
+ rescue AgentJail::FilesystemError
161
+ # handle filesystem violation
162
+ rescue AgentJail::SandboxError => e
163
+ logger.error("Tool call raised #{e.original_class}: #{e.original_message}")
164
+ logger.error(e.original_backtrace.join("\n"))
165
+ end
166
+ ```
167
+
168
+ ---
169
+
170
+ ## How it works
171
+
172
+ ```
173
+ AgentJail.run { block }
174
+
175
+ ├── opens result_pipe (r, w)
176
+ ├── fork → Child process
177
+ │ │
178
+ │ ├── closes r
179
+ │ ├── setrlimit (RLIMIT_AS + RLIMIT_CPU)
180
+ │ ├── Landlock / sandbox_init (if supported)
181
+ │ ├── runs block
182
+ │ ├── marshals result → writes to w
183
+ │ └── exit!(0)
184
+
185
+ ├── closes w
186
+ ├── starts monitor thread (wall-clock timeout → SIGKILL)
187
+ ├── reads result_pipe until EOF
188
+ ├── Process.waitpid2(child_pid)
189
+ ├── kills monitor thread
190
+ └── unmarshals result OR raises TimeoutError/MemoryError
191
+ ```
192
+
193
+ **Why fork?** `fork` copies the full Ruby process including loaded gems, open connections, and closures. The block can reference any in-scope object without serialization. Threads share memory (no isolation); `spawn` requires serializing the block (loses closures, complex). `fork` is the right tool.
194
+
195
+ **Why both wall-clock and CPU timeout?** `RLIMIT_CPU` limits CPU time — a sleeping process uses no CPU and would never be killed. The monitor thread kills by wall-clock regardless of CPU usage.
196
+
197
+ **Landlock design:** Landlock ABI v1 syscalls (444/445/446) via FFI. No root or capabilities required — just `prctl(PR_SET_NO_NEW_PRIVS)` first. The child creates a ruleset denying all filesystem access, adds explicit allow rules for system paths (read-only) and user-specified paths, then locks itself in with `landlock_restrict_self`.
198
+
199
+ ---
200
+
201
+ ## Known sharp edges
202
+
203
+ **Threads + fork:** If your parent process has background threads when `AgentJail.run` is called, the child inherits them in a broken state. Call `AgentJail.run` from a single-threaded context where possible. Background threads in the child are not safe to use.
204
+
205
+ **Database connections:** `fork` copies open file descriptors including database connections. Do not use ActiveRecord or any connection pool in the block — the child's connections are copies of the parent's and using them will corrupt state. Read-only closures over already-fetched data are fine.
206
+
207
+ **`RLIMIT_AS` vs RSS:** `RLIMIT_AS` limits virtual address space, not physical RAM. Ruby's VM allocates large virtual ranges upfront. Set `memory_mb` generously (>= 256) to avoid killing the child on startup. The default of 512MB is conservative.
208
+
209
+ **Return value size:** The result is marshalled over a pipe. Very large return values (> 10MB) will work but are slow. If you need to return large data, write it to an allowed path and return the path.
210
+
211
+ **MRI only:** `fork` is not available on JRuby or TruffleRuby. `Platform.fork_supported?` returns false on these runtimes and `on_unsupported` kicks in.
212
+
213
+ ---
214
+
215
+ ## Requirements
216
+
217
+ - Ruby >= 3.2.0
218
+ - Linux kernel >= 5.13 for Landlock filesystem restrictions (kernel detection is automatic)
219
+ - `ffi` gem (the only runtime dependency)
220
+
221
+ ---
222
+
223
+ ## Contributing
224
+
225
+ Bug reports and pull requests are welcome at https://github.com/jibranusman95/agent_jail.
226
+
227
+ ---
228
+
229
+ ## From the same author
230
+
231
+ | Gem | What it does |
232
+ |-----|-------------|
233
+ | [http_decoy](https://github.com/jibranusman95/http_decoy) | A real Rack server that runs inside your RSpec tests — test HTTP contracts without WebMock stubs |
234
+ | [llm_cassette](https://github.com/jibranusman95/llm_cassette) | Streaming-aware cassette recorder for LLM calls — record once, replay fast |
235
+ | [webhook_inbox](https://github.com/jibranusman95/webhook_inbox) | Transactional webhook inbox for Rails — deduplicate, replay, inspect |
236
+ | [promptscrub](https://github.com/jibranusman95/promptscrub) | Bidirectional PII redaction middleware for LLM calls |
237
+ | [turbo_presence](https://github.com/jibranusman95/turbo_presence) | Figma-style live cursors and presence in Rails with one line |
238
+
239
+ ---
240
+
241
+ ## License
242
+
243
+ MIT — see [LICENSE](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ # Runs inside the forked child process.
5
+ # Applies restrictions then executes the block, writing the result to the pipe.
6
+ class Child
7
+ def initialize(write:, cpu_timeout:, memory_mb:, fs_allow:, fs_read_allow:, block:)
8
+ @write = write
9
+ @cpu_timeout = cpu_timeout
10
+ @memory_mb = memory_mb
11
+ @fs_allow = fs_allow
12
+ @fs_read_allow = fs_read_allow
13
+ @block = block
14
+ end
15
+
16
+ def run
17
+ apply_restrictions
18
+
19
+ begin
20
+ result = @block.call
21
+ Pipe.write_result(@write, result)
22
+ rescue Exception => e
23
+ Pipe.write_error(@write, e)
24
+ ensure
25
+ @write.close
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def apply_restrictions
32
+ # Apply CPU limit first to prevent runaway setup code.
33
+ # Apply filesystem restrictions before the memory (RLIMIT_AS) limit,
34
+ # because Landlock setup allocates memory for FFI structs and file descriptors.
35
+ # Setting RLIMIT_AS too early can cause the setup itself to OOM.
36
+ AgentJail::FFI::Setrlimit.set_cpu(@cpu_timeout)
37
+ apply_filesystem_restrictions
38
+ AgentJail::FFI::Setrlimit.set_memory(@memory_mb * 1024 * 1024)
39
+ end
40
+
41
+ def apply_filesystem_restrictions
42
+ if Platform.landlock_supported?
43
+ Restrictions::Landlock.new(
44
+ fs_allow: @fs_allow,
45
+ fs_read_allow: @fs_read_allow
46
+ ).apply
47
+ elsif Platform.macos?
48
+ Restrictions::Seatbelt.new(
49
+ fs_allow: @fs_allow,
50
+ fs_read_allow: @fs_read_allow
51
+ ).apply
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ class Configuration
5
+ VALID_ON_UNSUPPORTED = %i[warn raise ignore].freeze
6
+
7
+ attr_accessor :default_timeout, :default_memory_mb, :default_fs_allow, :on_unsupported
8
+
9
+ def initialize
10
+ @default_timeout = 30
11
+ @default_memory_mb = 512
12
+ @default_fs_allow = []
13
+ @on_unsupported = :warn
14
+ end
15
+
16
+ def validate!
17
+ return if VALID_ON_UNSUPPORTED.include?(@on_unsupported)
18
+
19
+ raise ArgumentError,
20
+ "on_unsupported must be one of #{VALID_ON_UNSUPPORTED.inspect}, got #{@on_unsupported.inspect}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ # Base error for all agent_jail exceptions.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the sandboxed block exceeds the wall-clock or CPU time limit.
8
+ class TimeoutError < Error; end
9
+
10
+ # Raised when the sandboxed block exceeds the memory (address space) limit.
11
+ class MemoryError < Error; end
12
+
13
+ # Raised when the sandboxed block attempts to access a path not in fs_allow.
14
+ class FilesystemError < Error; end
15
+
16
+ # Raised when on_unsupported: :raise and the current platform cannot sandbox.
17
+ class UnsupportedPlatformError < Error; end
18
+
19
+ # Raised when the sandboxed block raises an exception.
20
+ # Wraps the original exception without requiring the original class to be present.
21
+ class SandboxError < Error
22
+ attr_reader :original_class, :original_message, :original_backtrace
23
+
24
+ def initialize(original_class:, original_message:, original_backtrace:)
25
+ @original_class = original_class
26
+ @original_message = original_message
27
+ @original_backtrace = original_backtrace
28
+ super("#{original_class}: #{original_message}")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module AgentJail
6
+ module FFI
7
+ # FFI bindings for the Linux Landlock LSM (kernel 5.13+).
8
+ # Uses raw syscalls via libc's syscall(2) since Landlock has no libc wrappers.
9
+ module Landlock
10
+ extend ::FFI::Library
11
+
12
+ ffi_lib ::FFI::Library::LIBC
13
+
14
+ # Landlock ABI v1 filesystem access rights
15
+ ACCESS_FS_EXECUTE = 1 << 0
16
+ ACCESS_FS_WRITE_FILE = 1 << 1
17
+ ACCESS_FS_READ_FILE = 1 << 2
18
+ ACCESS_FS_READ_DIR = 1 << 3
19
+ ACCESS_FS_REMOVE_DIR = 1 << 4
20
+ ACCESS_FS_REMOVE_FILE = 1 << 5
21
+ ACCESS_FS_MAKE_CHAR = 1 << 6
22
+ ACCESS_FS_MAKE_DIR = 1 << 7
23
+ ACCESS_FS_MAKE_REG = 1 << 8
24
+ ACCESS_FS_MAKE_SOCK = 1 << 9
25
+ ACCESS_FS_MAKE_FIFO = 1 << 10
26
+ ACCESS_FS_MAKE_BLOCK = 1 << 11
27
+ ACCESS_FS_MAKE_SYM = 1 << 12
28
+
29
+ # Composite access groups
30
+ ACCESS_FS_READ_WRITE = ACCESS_FS_READ_FILE | ACCESS_FS_READ_DIR |
31
+ ACCESS_FS_WRITE_FILE | ACCESS_FS_REMOVE_FILE |
32
+ ACCESS_FS_MAKE_REG | ACCESS_FS_MAKE_DIR
33
+ ACCESS_FS_READ_ONLY = ACCESS_FS_READ_FILE | ACCESS_FS_READ_DIR
34
+
35
+ # All ABI v1 access bits — used in ruleset's handled_access_fs
36
+ ALL_ACCESS_FS = ACCESS_FS_EXECUTE | ACCESS_FS_WRITE_FILE | ACCESS_FS_READ_FILE |
37
+ ACCESS_FS_READ_DIR | ACCESS_FS_REMOVE_DIR | ACCESS_FS_REMOVE_FILE |
38
+ ACCESS_FS_MAKE_CHAR | ACCESS_FS_MAKE_DIR | ACCESS_FS_MAKE_REG |
39
+ ACCESS_FS_MAKE_SOCK | ACCESS_FS_MAKE_FIFO | ACCESS_FS_MAKE_BLOCK |
40
+ ACCESS_FS_MAKE_SYM
41
+
42
+ RULE_PATH_BENEATH = 1
43
+
44
+ # Syscall numbers (x86_64 Linux)
45
+ SYS_LANDLOCK_CREATE_RULESET = 444
46
+ SYS_LANDLOCK_ADD_RULE = 445
47
+ SYS_LANDLOCK_RESTRICT_SELF = 446
48
+
49
+ # prctl option
50
+ PR_SET_NO_NEW_PRIVS = 38
51
+
52
+ # struct landlock_ruleset_attr { __u64 handled_access_fs; }
53
+ class RulesetAttr < ::FFI::Struct
54
+ layout :handled_access_fs, :uint64
55
+ end
56
+
57
+ # struct landlock_path_beneath_attr — packed (no padding between fields)
58
+ # __u64 allowed_access; __s32 parent_fd;
59
+ class PathBeneathAttr < ::FFI::Struct
60
+ pack 1
61
+ layout :allowed_access, :uint64,
62
+ :parent_fd, :int32
63
+ end
64
+
65
+ # syscall(number, ...) — varargs, each extra arg is a (type, value) pair
66
+ attach_function :syscall, %i[long varargs], :long
67
+ attach_function :prctl, %i[int ulong ulong ulong ulong], :int
68
+ attach_function :close, [:int], :int
69
+
70
+ def self.create_ruleset(handled_access_fs)
71
+ attr = RulesetAttr.new
72
+ attr[:handled_access_fs] = handled_access_fs
73
+ syscall(SYS_LANDLOCK_CREATE_RULESET, :pointer, attr.to_ptr, :size_t, attr.size, :uint32, 0)
74
+ end
75
+
76
+ def self.add_path_rule(ruleset_fd, path_fd, allowed_access)
77
+ attr = PathBeneathAttr.new
78
+ attr[:allowed_access] = allowed_access
79
+ attr[:parent_fd] = path_fd
80
+ syscall(SYS_LANDLOCK_ADD_RULE,
81
+ :int, ruleset_fd,
82
+ :uint32, RULE_PATH_BENEATH,
83
+ :pointer, attr.to_ptr,
84
+ :uint32, 0)
85
+ end
86
+
87
+ def self.restrict_self(ruleset_fd)
88
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
89
+ result = syscall(SYS_LANDLOCK_RESTRICT_SELF, :int, ruleset_fd, :uint32, 0)
90
+ close(ruleset_fd)
91
+ result
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module AgentJail
6
+ module FFI
7
+ # FFI bindings for macOS Seatbelt (sandbox_init).
8
+ # sandbox_init(3) is deprecated since macOS 10.8 but remains functional.
9
+ # No-op stubs are defined when libsandbox is unavailable so the module
10
+ # always responds to sandbox_init / sandbox_free_error regardless of platform.
11
+ module Seatbelt
12
+ extend ::FFI::Library
13
+
14
+ AVAILABLE = begin
15
+ ffi_lib "libsandbox.1.dylib"
16
+
17
+ # int sandbox_init(const char *profile, uint64_t flags, char **errorbuf)
18
+ attach_function :sandbox_init, %i[string uint64 pointer], :int
19
+ # void sandbox_free_error(char *errorbuf)
20
+ attach_function :sandbox_free_error, [:pointer], :void
21
+
22
+ true
23
+ rescue LoadError
24
+ # No-op stubs — callers check AVAILABLE before using; stubs keep the
25
+ # interface consistent and allow mocking in tests on all platforms.
26
+ def self.sandbox_init(_profile, _flags, _errbuf)
27
+ 0
28
+ end
29
+
30
+ def self.sandbox_free_error(_errbuf)
31
+ nil
32
+ end
33
+
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module AgentJail
6
+ module FFI
7
+ module Setrlimit
8
+ extend ::FFI::Library
9
+
10
+ ffi_lib ::FFI::Library::LIBC
11
+
12
+ # rlim_t is uint64 on 64-bit platforms
13
+ class RLimit < ::FFI::Struct
14
+ layout :rlim_cur, :uint64,
15
+ :rlim_max, :uint64
16
+ end
17
+
18
+ # int setrlimit(int resource, const struct rlimit *rlim)
19
+ attach_function :setrlimit, %i[int pointer], :int
20
+
21
+ # POSIX resource limit constants
22
+ RLIMIT_CPU = 0 # CPU time in seconds
23
+ # RLIMIT_AS is virtual address space (address space = memory limit)
24
+ # Linux: 9, macOS: 5
25
+ RLIMIT_AS_LINUX = 9
26
+ RLIMIT_AS_MACOS = 5
27
+
28
+ def self.rlimit_as
29
+ RUBY_PLATFORM.include?("darwin") ? RLIMIT_AS_MACOS : RLIMIT_AS_LINUX
30
+ end
31
+
32
+ def self.set_memory(bytes)
33
+ rlimit = RLimit.new
34
+ rlimit[:rlim_cur] = bytes
35
+ rlimit[:rlim_max] = bytes
36
+ setrlimit(rlimit_as, rlimit)
37
+ end
38
+
39
+ def self.set_cpu(seconds)
40
+ rlimit = RLimit.new
41
+ rlimit[:rlim_cur] = seconds
42
+ rlimit[:rlim_max] = seconds
43
+ setrlimit(RLIMIT_CPU, rlimit)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ # Handles the result pipe protocol between parent and child processes.
5
+ # The child writes exactly one Marshal payload (success or error).
6
+ # The parent reads until EOF and parses it.
7
+ module Pipe
8
+ def self.write_result(io, value)
9
+ io.write(Marshal.dump({ status: :ok, value: value }))
10
+ end
11
+
12
+ def self.write_error(io, exception)
13
+ io.write(Marshal.dump({
14
+ status: :error,
15
+ class: exception.class.name,
16
+ message: exception.message,
17
+ backtrace: exception.backtrace
18
+ }))
19
+ end
20
+
21
+ def self.read_result(raw)
22
+ return nil if raw.nil? || raw.empty?
23
+
24
+ Marshal.load(raw)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ module Platform
5
+ def self.linux?
6
+ RUBY_PLATFORM.include?("linux")
7
+ end
8
+
9
+ def self.macos?
10
+ RUBY_PLATFORM.include?("darwin")
11
+ end
12
+
13
+ def self.windows?
14
+ RUBY_PLATFORM.match?(/mingw|mswin|cygwin/)
15
+ end
16
+
17
+ def self.fork_supported?
18
+ linux? || macos?
19
+ end
20
+
21
+ def self.landlock_supported?
22
+ return false unless linux?
23
+
24
+ kernel_version >= Gem::Version.new("5.13")
25
+ end
26
+
27
+ def self.kernel_version
28
+ return Gem::Version.new("0.0") unless linux?
29
+
30
+ read_kernel_version
31
+ end
32
+
33
+ def self.read_kernel_version
34
+ content = File.read("/proc/version")
35
+ match = content.match(/Linux version (\d+\.\d+)/)
36
+ match ? Gem::Version.new(match[1]) : Gem::Version.new("0.0")
37
+ rescue StandardError
38
+ Gem::Version.new("0.0")
39
+ end
40
+ private_class_method :read_kernel_version
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ module Restrictions
5
+ # Base interface for all restriction strategies.
6
+ # Each subclass implements #apply which is called in the child process
7
+ # before the sandboxed block runs.
8
+ class Base
9
+ def apply
10
+ raise NotImplementedError, "#{self.class}#apply not implemented"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ module Restrictions
5
+ # Applies Linux Landlock filesystem restrictions in the child process.
6
+ # Requires kernel 5.13+ and is a no-op on older kernels.
7
+ class Landlock < Base
8
+ # Paths that are implicitly allowed read-only for Ruby to function.
9
+ # These are added automatically regardless of user-specified fs_allow.
10
+ SYSTEM_READ_PATHS = %w[
11
+ /usr
12
+ /lib
13
+ /lib64
14
+ /proc
15
+ /dev
16
+ /etc
17
+ /run/systemd
18
+ ].freeze
19
+
20
+ def initialize(fs_allow:, fs_read_allow:)
21
+ super()
22
+ @fs_allow = Array(fs_allow)
23
+ @fs_read_allow = Array(fs_read_allow)
24
+ end
25
+
26
+ def apply
27
+ ruleset_fd = FFI::Landlock.create_ruleset(FFI::Landlock::ALL_ACCESS_FS)
28
+ raise "landlock_create_ruleset failed: #{ruleset_fd}" if ruleset_fd.negative?
29
+
30
+ add_read_only_paths(ruleset_fd)
31
+ add_read_write_paths(ruleset_fd)
32
+
33
+ FFI::Landlock.restrict_self(ruleset_fd)
34
+ end
35
+
36
+ private
37
+
38
+ def add_read_only_paths(ruleset_fd)
39
+ (SYSTEM_READ_PATHS + @fs_read_allow).each do |path|
40
+ add_rule(ruleset_fd, path, FFI::Landlock::ACCESS_FS_READ_ONLY)
41
+ end
42
+ end
43
+
44
+ def add_read_write_paths(ruleset_fd)
45
+ @fs_allow.each do |path|
46
+ add_rule(ruleset_fd, path, FFI::Landlock::ACCESS_FS_READ_WRITE)
47
+ end
48
+ end
49
+
50
+ def add_rule(ruleset_fd, path, access_mask)
51
+ return unless File.exist?(path)
52
+
53
+ File.open(path) do |f|
54
+ result = FFI::Landlock.add_path_rule(ruleset_fd, f.fileno, access_mask)
55
+ # Negative return means the syscall failed; we ignore ENOENT/ENOTDIR silently
56
+ result
57
+ end
58
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
59
+ # Path doesn't exist or can't be opened — skip it
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ module Restrictions
5
+ # Applies POSIX resource limits (setrlimit) in the child process.
6
+ # Sets both address space (RLIMIT_AS) and CPU time (RLIMIT_CPU).
7
+ class ResourceLimits < Base
8
+ def initialize(memory_mb:, cpu_timeout:)
9
+ super()
10
+ @memory_bytes = memory_mb * 1024 * 1024
11
+ @cpu_timeout = cpu_timeout
12
+ end
13
+
14
+ def apply
15
+ AgentJail::FFI::Setrlimit.set_memory(@memory_bytes)
16
+ AgentJail::FFI::Setrlimit.set_cpu(@cpu_timeout)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ module Restrictions
5
+ # Applies macOS Seatbelt (sandbox_init) filesystem restrictions in the child process.
6
+ class Seatbelt < Base
7
+ SYSTEM_READ_PATHS = %w[
8
+ /usr
9
+ /Library
10
+ /System
11
+ /private/var/db/dyld
12
+ /private/etc
13
+ /dev
14
+ /proc
15
+ ].freeze
16
+
17
+ def initialize(fs_allow:, fs_read_allow:)
18
+ super()
19
+ @fs_allow = Array(fs_allow)
20
+ @fs_read_allow = Array(fs_read_allow)
21
+ end
22
+
23
+ def apply
24
+ return unless FFI::Seatbelt::AVAILABLE
25
+
26
+ profile = build_profile
27
+ errorbuf_ptr = ::FFI::MemoryPointer.new(:pointer)
28
+ result = FFI::Seatbelt.sandbox_init(profile, 0, errorbuf_ptr)
29
+
30
+ return unless result != 0
31
+
32
+ err_ptr = errorbuf_ptr.read_pointer
33
+ message = err_ptr.null? ? "sandbox_init failed" : err_ptr.read_string
34
+ FFI::Seatbelt.sandbox_free_error(err_ptr) unless err_ptr.null?
35
+ raise "sandbox_init failed: #{message}"
36
+ end
37
+
38
+ private
39
+
40
+ def build_profile
41
+ lines = ["(version 1)", "(deny default)"]
42
+
43
+ SYSTEM_READ_PATHS.each do |path|
44
+ lines << "(allow file-read* (subpath \"#{path}\"))"
45
+ end
46
+
47
+ @fs_read_allow.each do |path|
48
+ lines << "(allow file-read* (subpath \"#{path}\"))"
49
+ end
50
+
51
+ @fs_allow.each do |path|
52
+ lines << "(allow file-read-write* (subpath \"#{path}\"))"
53
+ end
54
+
55
+ lines << "(allow process-exec*)"
56
+ lines << "(allow mach*)"
57
+ lines << "(allow sysctl*)"
58
+ lines << "(allow signal)"
59
+ lines << "(allow network* (local))"
60
+
61
+ lines.join("\n")
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ # Orchestrates the fork + pipe + monitor pattern.
5
+ # Forks a child, applies a wall-clock timeout monitor thread, reads the result.
6
+ class Runner
7
+ def initialize(options, &block)
8
+ cfg = AgentJail.configuration
9
+ @timeout = options.fetch(:timeout, cfg.default_timeout)
10
+ @cpu_timeout = options.fetch(:cpu_timeout, @timeout)
11
+ @memory_mb = options.fetch(:memory_mb, cfg.default_memory_mb)
12
+ @fs_allow = options.fetch(:fs_allow, cfg.default_fs_allow)
13
+ @fs_read_allow = options.fetch(:fs_read_allow, [])
14
+ @block = block
15
+ end
16
+
17
+ def call
18
+ read, write = IO.pipe
19
+
20
+ child_pid = fork do
21
+ read.close
22
+ Child.new(
23
+ write: write,
24
+ cpu_timeout: @cpu_timeout,
25
+ memory_mb: @memory_mb,
26
+ fs_allow: @fs_allow,
27
+ fs_read_allow: @fs_read_allow,
28
+ block: @block
29
+ ).run
30
+ exit!(0)
31
+ end
32
+
33
+ write.close
34
+ raw, status = wait_for_child(read, child_pid)
35
+ read.close
36
+
37
+ parse_result(raw, status)
38
+ end
39
+
40
+ private
41
+
42
+ def wait_for_child(read_io, child_pid)
43
+ monitor = Thread.new do
44
+ sleep(@timeout)
45
+ Process.kill(:KILL, child_pid)
46
+ rescue Errno::ESRCH
47
+ # Child already exited before wall-clock timeout — no-op
48
+ end
49
+
50
+ raw = read_io.read
51
+ _pid, status = Process.waitpid2(child_pid)
52
+ monitor.kill
53
+ monitor.join
54
+
55
+ [raw, status]
56
+ end
57
+
58
+ def parse_result(raw, status)
59
+ result = Pipe.read_result(raw)
60
+ return raise_from_result(result) if result
61
+
62
+ # Pipe was empty — child was killed before writing
63
+ raise_from_signal(status)
64
+ end
65
+
66
+ def raise_from_result(result)
67
+ case result[:status]
68
+ when :ok
69
+ result[:value]
70
+ when :error
71
+ klass = result[:class]
72
+ if filesystem_error?(klass)
73
+ raise FilesystemError, result[:message]
74
+ elsif memory_error?(klass)
75
+ raise MemoryError, result[:message]
76
+ else
77
+ raise SandboxError.new(
78
+ original_class: klass,
79
+ original_message: result[:message],
80
+ original_backtrace: result[:backtrace]
81
+ )
82
+ end
83
+ end
84
+ end
85
+
86
+ def raise_from_signal(status)
87
+ raise MemoryError, "Process exceeded #{@memory_mb}MB memory limit" unless status.signaled?
88
+
89
+ sig = status.termsig
90
+ case sig
91
+ when Signal.list["XCPU"]
92
+ raise TimeoutError, "Process exceeded #{@cpu_timeout}s CPU time limit"
93
+ when Signal.list["SEGV"]
94
+ raise MemoryError, "Process exceeded #{@memory_mb}MB memory limit (SIGSEGV)"
95
+ else
96
+ # SIGKILL (9) — sent by our monitor thread (wall-clock) or OS OOM killer
97
+ raise TimeoutError, "Process exceeded #{@timeout}s wall-clock timeout"
98
+ end
99
+ end
100
+
101
+ def filesystem_error?(klass)
102
+ %w[Errno::EACCES Errno::EPERM AgentJail::FilesystemError].include?(klass)
103
+ end
104
+
105
+ def memory_error?(klass)
106
+ %w[NoMemoryError AgentJail::MemoryError].include?(klass)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentJail
4
+ VERSION = "0.1.0"
5
+ end
data/lib/agent_jail.rb ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "agent_jail/version"
4
+ require "agent_jail/configuration"
5
+ require "agent_jail/errors"
6
+ require "agent_jail/platform"
7
+ require "agent_jail/pipe"
8
+ require "agent_jail/ffi/setrlimit"
9
+ require "agent_jail/restrictions/base"
10
+ require "agent_jail/restrictions/resource_limits"
11
+ require "agent_jail/child"
12
+ require "agent_jail/runner"
13
+
14
+ if AgentJail::Platform.linux?
15
+ require "agent_jail/ffi/landlock"
16
+ require "agent_jail/restrictions/landlock"
17
+ end
18
+
19
+ # Seatbelt files load on all platforms — ffi/seatbelt.rb sets AVAILABLE = false
20
+ # when libsandbox is not present, and Restrictions::Seatbelt#apply is a no-op then.
21
+ require "agent_jail/ffi/seatbelt"
22
+ require "agent_jail/restrictions/seatbelt"
23
+
24
+ module AgentJail
25
+ class << self
26
+ def configure
27
+ yield configuration
28
+ configuration.validate!
29
+ end
30
+
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def reset!
36
+ @configuration = nil
37
+ end
38
+
39
+ # Run a block inside a sandboxed child process with resource limits and
40
+ # optional filesystem restrictions.
41
+ #
42
+ # @param timeout [Integer] wall-clock timeout in seconds (default: 30)
43
+ # @param cpu_timeout [Integer] CPU time limit in seconds (default: same as timeout)
44
+ # @param memory_mb [Integer] address space limit in MB (default: 512)
45
+ # @param fs_allow [Array<String>] read-write paths the child may access
46
+ # @param fs_read_allow [Array<String>] read-only paths the child may access
47
+ # @return the block's return value
48
+ def run(**options, &block)
49
+ raise ArgumentError, "block required" unless block
50
+
51
+ unless Platform.fork_supported?
52
+ handle_unsupported("Sandboxing is not supported on #{RUBY_PLATFORM}")
53
+ return block.call
54
+ end
55
+
56
+ Runner.new(options, &block).call
57
+ end
58
+
59
+ private
60
+
61
+ def handle_unsupported(reason)
62
+ case configuration.on_unsupported
63
+ when :raise
64
+ raise UnsupportedPlatformError, reason
65
+ when :warn
66
+ warn "[AgentJail] WARNING: #{reason}. Running block unsandboxed."
67
+ when :ignore
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ module AgentJail
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class TimeoutError < Error
8
+ end
9
+
10
+ class MemoryError < Error
11
+ end
12
+
13
+ class FilesystemError < Error
14
+ end
15
+
16
+ class UnsupportedPlatformError < Error
17
+ end
18
+
19
+ class SandboxError < Error
20
+ attr_reader original_class: String
21
+ attr_reader original_message: String
22
+ attr_reader original_backtrace: Array[String]?
23
+
24
+ def initialize: (original_class: String, original_message: String, original_backtrace: Array[String]?) -> void
25
+ end
26
+
27
+ class Configuration
28
+ attr_accessor default_timeout: Integer
29
+ attr_accessor default_memory_mb: Integer
30
+ attr_accessor default_fs_allow: Array[String]
31
+ attr_accessor on_unsupported: Symbol
32
+
33
+ def initialize: () -> void
34
+ def validate!: () -> void
35
+ end
36
+
37
+ def self.configure: () { (Configuration) -> void } -> void
38
+ def self.configuration: () -> Configuration
39
+ def self.reset!: () -> void
40
+ def self.run: (?timeout: Integer, ?cpu_timeout: Integer, ?memory_mb: Integer, ?fs_allow: Array[String], ?fs_read_allow: Array[String]) { () -> untyped } -> untyped
41
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agent_jail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jibran Usman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description: |-
28
+ Forks a child process, applies Linux Landlock filesystem restrictions and POSIX resource limits
29
+ (setrlimit), then runs your block. If the block times out, exceeds memory, or touches a disallowed
30
+ path, the child is killed and the parent gets a typed exception. macOS uses Seatbelt (sandbox_init).
31
+ Degrades gracefully on unsupported platforms.
32
+ email:
33
+ - jibran.usman@eunasolutions.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".rspec"
39
+ - ".rubocop.yml"
40
+ - CHANGELOG.md
41
+ - LICENSE
42
+ - README.md
43
+ - Rakefile
44
+ - lib/agent_jail.rb
45
+ - lib/agent_jail/child.rb
46
+ - lib/agent_jail/configuration.rb
47
+ - lib/agent_jail/errors.rb
48
+ - lib/agent_jail/ffi/landlock.rb
49
+ - lib/agent_jail/ffi/seatbelt.rb
50
+ - lib/agent_jail/ffi/setrlimit.rb
51
+ - lib/agent_jail/pipe.rb
52
+ - lib/agent_jail/platform.rb
53
+ - lib/agent_jail/restrictions/base.rb
54
+ - lib/agent_jail/restrictions/landlock.rb
55
+ - lib/agent_jail/restrictions/resource_limits.rb
56
+ - lib/agent_jail/restrictions/seatbelt.rb
57
+ - lib/agent_jail/runner.rb
58
+ - lib/agent_jail/version.rb
59
+ - sig/agent_jail.rbs
60
+ homepage: https://github.com/jibranusman95/agent_jail
61
+ licenses: []
62
+ metadata:
63
+ homepage_uri: https://github.com/jibranusman95/agent_jail
64
+ source_code_uri: https://github.com/jibranusman95/agent_jail
65
+ changelog_uri: https://github.com/jibranusman95/agent_jail/blob/main/CHANGELOG.md
66
+ rubygems_mfa_required: 'true'
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.2.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.5.22
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: 'Sandbox LLM tool calls in a child process: timeout, memory, and filesystem
86
+ limits.'
87
+ test_files: []