hermetic 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.
@@ -0,0 +1,64 @@
1
+ require "open3"
2
+
3
+ module Hermetic
4
+ module Transport
5
+ # Host-side process runner: spawns an argv, pipes stdin, and enforces the
6
+ # wall clock with a reaper (never trusts the guest to exit — the pattern
7
+ # ported from Silas's Docker adapter). Returns
8
+ # [stdout, stderr, exit_status, timed_out]; a timeout maps to exit 124
9
+ # (coreutils convention, matches Silas TIMEOUT_EXIT). The low-level
10
+ # capture is injectable so the tuple/timeout contract is unit-testable
11
+ # with no real processes.
12
+ class Subprocess
13
+ TIMEOUT_EXIT = 124
14
+
15
+ def initialize(capture: nil)
16
+ @capture = capture || method(:popen_capture)
17
+ end
18
+
19
+ def call(argv, stdin: nil, timeout: 30)
20
+ unless argv.is_a?(Array) && !argv.empty? && argv.all?(String)
21
+ raise ConfigError, "argv must be a non-empty Array of Strings, got #{argv.inspect}"
22
+ end
23
+
24
+ out, err, status, timed_out = @capture.call(argv, stdin: stdin, timeout: timeout)
25
+ return [ out, err, status, false ] unless timed_out
26
+
27
+ [ "", "hermetic: command timed out after #{timeout}s", TIMEOUT_EXIT, true ]
28
+ end
29
+
30
+ private
31
+
32
+ def popen_capture(argv, stdin:, timeout:)
33
+ Open3.popen3(*argv) do |in_io, out_io, err_io, wait|
34
+ timed_out = false
35
+ reaper = Thread.new do
36
+ unless wait.join(timeout)
37
+ timed_out = true
38
+ begin
39
+ Process.kill("KILL", wait.pid)
40
+ rescue Errno::ESRCH
41
+ nil # exited on the deadline itself; wall clock was still blown
42
+ end
43
+ end
44
+ end
45
+
46
+ out_reader = Thread.new { out_io.read }
47
+ err_reader = Thread.new { err_io.read }
48
+ begin
49
+ in_io.write(stdin) if stdin
50
+ rescue Errno::EPIPE
51
+ nil # guest exited without reading stdin; its exit status tells the story
52
+ end
53
+ in_io.close
54
+
55
+ status = wait.value
56
+ reaper.join
57
+ [ out_reader.value, err_reader.value, status.exitstatus || (128 + status.termsig.to_i), timed_out ]
58
+ end
59
+ rescue Errno::ENOENT => e
60
+ raise TransportError, "#{argv.first.inspect} not found on PATH (#{e.message})"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Hermetic
2
+ VERSION = "0.1.0"
3
+ end
data/lib/hermetic.rb ADDED
@@ -0,0 +1,67 @@
1
+ require "hermetic/version"
2
+ require "hermetic/errors"
3
+ require "hermetic/result"
4
+ require "hermetic/limits"
5
+ require "hermetic/network_policy"
6
+ require "hermetic/credentials"
7
+ require "hermetic/request"
8
+ require "hermetic/transport/subprocess"
9
+ require "hermetic/transport/http"
10
+ require "hermetic/backends/base"
11
+ require "hermetic/backends/null"
12
+
13
+ # hermetic — the missing Ruby sandbox. One `run` interface, swappable
14
+ # isolation backends, designed to run off the app host so a guest escape
15
+ # can't reach your secrets. Every constructor returns a plain object
16
+ # implementing run/enabled?, so `config.sandbox = Hermetic.gvisor(...)`
17
+ # drops into Silas unchanged.
18
+ #
19
+ # Backends beyond Null are required lazily — constructing one you haven't
20
+ # shipped/installed fails at the constructor, not at boot.
21
+ module Hermetic
22
+ # Options that shape the client-side Request (or the wire itself) and so
23
+ # stay on the Remote client when `executor:` reroutes a local constructor;
24
+ # everything else is the executor's to construct with (spec §3).
25
+ CLIENT_OPTS = %i[limits credentials credential_mode env_allowlist transport].freeze
26
+
27
+ class << self
28
+ def null = Backends::Null.new
29
+
30
+ def docker(**opts) = backend(:docker, :Docker, "hermetic/backends/docker", **opts)
31
+
32
+ def gvisor(**opts) = backend(:gvisor, :Gvisor, "hermetic/backends/gvisor", **opts)
33
+
34
+ def firecracker(**opts) = backend(:firecracker, :Firecracker, "hermetic/backends/firecracker", **opts)
35
+
36
+ def hosted(provider, **opts)
37
+ require "hermetic/backends/hosted"
38
+ Backends::Hosted.build(provider, **opts)
39
+ end
40
+
41
+ # Off-host executor client (trust :remote): the isolation runs on a
42
+ # dedicated sandbox host; this process holds only the URL + token.
43
+ def wrap(executor:, **opts)
44
+ require "hermetic/remote"
45
+ Remote.new(executor: executor, **opts)
46
+ end
47
+
48
+ private
49
+
50
+ # Any local constructor takes `executor:` (+ `token:`) and becomes a
51
+ # Remote client for that isolation kind — same backend, off the app host:
52
+ # Hermetic.gvisor(image: "ruby:3.3-slim", executor: "https://sbx:8443", token: ...)
53
+ # Request-shaping options stay client-side; the rest ship as
54
+ # backend_options for the executor to construct with.
55
+ def backend(kind, const, path, **opts)
56
+ if opts.key?(:executor)
57
+ return wrap(backend: kind,
58
+ executor: opts[:executor], token: opts[:token],
59
+ backend_options: opts.except(:executor, :token, *CLIENT_OPTS),
60
+ **opts.slice(*CLIENT_OPTS))
61
+ end
62
+
63
+ require path
64
+ Backends.const_get(const).new(**opts)
65
+ end
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hermetic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel St Paul
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.13'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.13'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: Pick your isolation strength (gVisor, a Firecracker microVM, a hosted
41
+ sandbox, or hardened Docker) behind one `run` call, and run it off your app host
42
+ so an escape can't reach your database or secrets. Zero runtime dependencies — stdlib
43
+ only.
44
+ email:
45
+ - daniel@zerogravity.co.uk
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - CHANGELOG.md
51
+ - EXECUTOR.md
52
+ - LICENSE
53
+ - README.md
54
+ - lib/hermetic.rb
55
+ - lib/hermetic/backends/base.rb
56
+ - lib/hermetic/backends/docker.rb
57
+ - lib/hermetic/backends/firecracker.rb
58
+ - lib/hermetic/backends/gvisor.rb
59
+ - lib/hermetic/backends/hosted.rb
60
+ - lib/hermetic/backends/hosted/e2b.rb
61
+ - lib/hermetic/backends/null.rb
62
+ - lib/hermetic/credentials.rb
63
+ - lib/hermetic/errors.rb
64
+ - lib/hermetic/limits.rb
65
+ - lib/hermetic/network_policy.rb
66
+ - lib/hermetic/remote.rb
67
+ - lib/hermetic/request.rb
68
+ - lib/hermetic/result.rb
69
+ - lib/hermetic/silas.rb
70
+ - lib/hermetic/transport/http.rb
71
+ - lib/hermetic/transport/subprocess.rb
72
+ - lib/hermetic/version.rb
73
+ homepage: https://github.com/danielstpaul/hermetic
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/danielstpaul/hermetic
78
+ source_code_uri: https://github.com/danielstpaul/hermetic
79
+ changelog_uri: https://github.com/danielstpaul/hermetic/blob/main/CHANGELOG.md
80
+ rubygems_mfa_required: 'true'
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '3.2'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.6.9
96
+ specification_version: 4
97
+ summary: The missing Ruby sandbox — real isolation for untrusted / model-generated
98
+ code.
99
+ test_files: []