open_sandbox 0.1.1 → 0.2.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 +4 -4
- data/lib/open_sandbox/execd/code_interpreter.rb +140 -0
- data/lib/open_sandbox/execd/commands.rb +60 -0
- data/lib/open_sandbox/execd/files.rb +119 -0
- data/lib/open_sandbox/execd.rb +63 -0
- data/lib/open_sandbox/http_client.rb +56 -4
- data/lib/open_sandbox/sandboxes.rb +11 -5
- data/lib/open_sandbox/version.rb +1 -1
- data/lib/open_sandbox.rb +1 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad3786f0020200378180e62e770b0ca8463f666d9910e3d70257c3892734400a
|
|
4
|
+
data.tar.gz: cc0b2c9240f79f07e37a69b5e74c9ab400eebb1f969833f4d4f8fa679daa189e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a244435b09be28039d5709d687510172618173a689bc4bfcfa83cf79840b9f05b32d97ccd3a0933e96488e3d77cf324181114a6562f7f01d97371a78b238a6de
|
|
7
|
+
data.tar.gz: 72d656009b9fc0e81faa683ffc67b44ced01731a8fc9ad41cfc0e03118084dfa7ceeec3bec2241fb15eb8a0b2f52e67467a9a360004a6365c13c183e4ba757fa
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenSandbox
|
|
6
|
+
class Execd
|
|
7
|
+
class CodeInterpreter
|
|
8
|
+
CodeResult = Data.define(:stdout, :stderr, :result, :error, :execution_count, :execution_time_ms)
|
|
9
|
+
Context = Data.define(:id, :language)
|
|
10
|
+
|
|
11
|
+
def initialize(execd)
|
|
12
|
+
@execd = execd
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Create a new code execution context.
|
|
16
|
+
def create_context(language: "python")
|
|
17
|
+
response = @execd.proxy(method: :post, path: "/code/context", body: { language: language })
|
|
18
|
+
data = JSON.parse(response.body)
|
|
19
|
+
Context.new(id: data["contextId"] || data["id"], language: language)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# List active contexts.
|
|
23
|
+
def list_contexts(language: nil)
|
|
24
|
+
query_path = "/code/contexts"
|
|
25
|
+
query_path += "?language=#{language}" if language
|
|
26
|
+
response = @execd.proxy(method: :get, path: query_path)
|
|
27
|
+
data = JSON.parse(response.body)
|
|
28
|
+
contexts = data.is_a?(Array) ? data : (data["contexts"] || [])
|
|
29
|
+
contexts.map { |c| Context.new(id: c["id"], language: c["language"]) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Delete a context.
|
|
33
|
+
def delete_context(context_id)
|
|
34
|
+
@execd.proxy(method: :delete, path: "/code/contexts/#{context_id}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Execute code in a context. Returns parsed result from streaming JSON/SSE response.
|
|
38
|
+
# If a block is given, yields each parsed event as it arrives.
|
|
39
|
+
def run(code, context_id: nil, language: "python", &block)
|
|
40
|
+
context = { language: language }
|
|
41
|
+
context[:id] = context_id if context_id
|
|
42
|
+
body = { code: code, context: context }
|
|
43
|
+
|
|
44
|
+
if block_given?
|
|
45
|
+
parse_streaming_response(block) do |chunk_handler|
|
|
46
|
+
@execd.proxy_stream(method: :post, path: "/code", body: body, &chunk_handler)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
response = @execd.proxy(method: :post, path: "/code", body: body)
|
|
50
|
+
parse_sse_code_response(response.body)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def parse_streaming_response(event_callback)
|
|
57
|
+
buffer = +""
|
|
58
|
+
events = []
|
|
59
|
+
|
|
60
|
+
handler = lambda do |chunk|
|
|
61
|
+
buffer << chunk
|
|
62
|
+
while (newline_index = buffer.index("\n"))
|
|
63
|
+
line = buffer.slice!(0..newline_index).strip
|
|
64
|
+
next if line.empty?
|
|
65
|
+
|
|
66
|
+
event = parse_event_line(line)
|
|
67
|
+
next unless event
|
|
68
|
+
|
|
69
|
+
events << event
|
|
70
|
+
event_callback.call(event)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
yield handler
|
|
75
|
+
unless buffer.strip.empty?
|
|
76
|
+
event = parse_event_line(buffer.strip)
|
|
77
|
+
if event
|
|
78
|
+
events << event
|
|
79
|
+
event_callback.call(event)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
build_result_from_events(events)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def parse_sse_code_response(body)
|
|
88
|
+
events = body.to_s.each_line.filter_map { |line| parse_event_line(line.strip) }
|
|
89
|
+
build_result_from_events(events)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_event_line(line)
|
|
93
|
+
return nil if line.empty?
|
|
94
|
+
|
|
95
|
+
json_str = line.start_with?("data:") ? line.sub(/\Adata:\s*/, "") : line
|
|
96
|
+
return nil if json_str.empty?
|
|
97
|
+
|
|
98
|
+
JSON.parse(json_str)
|
|
99
|
+
rescue JSON::ParserError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_result_from_events(events)
|
|
104
|
+
stdout_lines = []
|
|
105
|
+
stderr_lines = []
|
|
106
|
+
result_text = nil
|
|
107
|
+
error_text = nil
|
|
108
|
+
exec_count = nil
|
|
109
|
+
exec_time = nil
|
|
110
|
+
|
|
111
|
+
events.each do |data|
|
|
112
|
+
case data["type"]
|
|
113
|
+
when "stdout"
|
|
114
|
+
stdout_lines << data["text"]
|
|
115
|
+
when "stderr"
|
|
116
|
+
stderr_lines << data["text"]
|
|
117
|
+
when "result"
|
|
118
|
+
result_text = data["text"]
|
|
119
|
+
when "error"
|
|
120
|
+
error = data["error"]
|
|
121
|
+
error_text = data["text"] || data["message"] || error&.dig("evalue") || error&.dig("ename")
|
|
122
|
+
when "execution_complete"
|
|
123
|
+
exec_time = data["executionTimeInMillis"] || data["execution_time"]
|
|
124
|
+
when "execution_count"
|
|
125
|
+
exec_count = data["count"]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
CodeResult.new(
|
|
130
|
+
stdout: stdout_lines.join,
|
|
131
|
+
stderr: stderr_lines.join,
|
|
132
|
+
result: result_text,
|
|
133
|
+
error: error_text,
|
|
134
|
+
execution_count: exec_count,
|
|
135
|
+
execution_time_ms: exec_time
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenSandbox
|
|
4
|
+
class Execd
|
|
5
|
+
class Commands
|
|
6
|
+
CommandResult = Data.define(:stdout, :stderr, :exit_code, :execution_time_ms)
|
|
7
|
+
|
|
8
|
+
def initialize(execd)
|
|
9
|
+
@execd = execd
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Run a shell command and wait for completion.
|
|
13
|
+
# The execd API returns SSE-formatted response.
|
|
14
|
+
def run(command, timeout: 60)
|
|
15
|
+
response = @execd.proxy(
|
|
16
|
+
method: :post,
|
|
17
|
+
path: "/command",
|
|
18
|
+
body: { command: command, timeout: timeout, background: false }
|
|
19
|
+
)
|
|
20
|
+
parse_sse_response(response.body)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse_sse_response(body)
|
|
26
|
+
stdout_lines = []
|
|
27
|
+
stderr_lines = []
|
|
28
|
+
exit_code = nil
|
|
29
|
+
exec_time = nil
|
|
30
|
+
|
|
31
|
+
body.to_s.each_line do |line|
|
|
32
|
+
line = line.strip
|
|
33
|
+
next if line.empty?
|
|
34
|
+
json_str = line.start_with?("data:") ? line.sub(/\Adata:\s*/, "") : line
|
|
35
|
+
next if json_str.empty?
|
|
36
|
+
|
|
37
|
+
data = JSON.parse(json_str)
|
|
38
|
+
case data["type"]
|
|
39
|
+
when "stdout"
|
|
40
|
+
stdout_lines << data["text"]
|
|
41
|
+
when "stderr"
|
|
42
|
+
stderr_lines << data["text"]
|
|
43
|
+
when "execution_complete"
|
|
44
|
+
exit_code = data["exitCode"] || data["exit_code"] || 0
|
|
45
|
+
exec_time = data["executionTimeInMillis"] || data["execution_time"]
|
|
46
|
+
end
|
|
47
|
+
rescue JSON::ParserError
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
CommandResult.new(
|
|
52
|
+
stdout: stdout_lines.join,
|
|
53
|
+
stderr: stderr_lines.join,
|
|
54
|
+
exit_code: exit_code || 0,
|
|
55
|
+
execution_time_ms: exec_time
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module OpenSandbox
|
|
9
|
+
class Execd
|
|
10
|
+
class Files
|
|
11
|
+
FileInfo = Data.define(:path, :size, :mode, :is_dir, :mod_time)
|
|
12
|
+
WriteEntry = Data.define(:path, :data, :mode)
|
|
13
|
+
|
|
14
|
+
def initialize(execd)
|
|
15
|
+
@execd = execd
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Write one or more files into the sandbox.
|
|
19
|
+
def write_files(entries)
|
|
20
|
+
boundary = "----OpenSandboxRuby#{SecureRandom.hex(12)}"
|
|
21
|
+
body = build_multipart_body(entries, boundary)
|
|
22
|
+
response = @execd.proxy(
|
|
23
|
+
method: :post,
|
|
24
|
+
path: "/files/upload",
|
|
25
|
+
raw_body: body,
|
|
26
|
+
headers: { "Content-Type" => "multipart/form-data; boundary=#{boundary}" }
|
|
27
|
+
)
|
|
28
|
+
response.code >= 200 && response.code < 300
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Read a file from the sandbox.
|
|
32
|
+
def read_file(path)
|
|
33
|
+
response = @execd.proxy(
|
|
34
|
+
method: :get,
|
|
35
|
+
path: "/files/download?path=#{escape(path)}"
|
|
36
|
+
)
|
|
37
|
+
response.body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Search for files matching a pattern.
|
|
41
|
+
def search(path:, pattern: "*")
|
|
42
|
+
response = @execd.proxy(
|
|
43
|
+
method: :get,
|
|
44
|
+
path: "/files/search?path=#{escape(path)}&pattern=#{escape(pattern)}"
|
|
45
|
+
)
|
|
46
|
+
data = JSON.parse(response.body)
|
|
47
|
+
files = data.is_a?(Array) ? data : (data["files"] || [])
|
|
48
|
+
files.map do |f|
|
|
49
|
+
FileInfo.new(
|
|
50
|
+
path: f["path"],
|
|
51
|
+
size: f["size"],
|
|
52
|
+
mode: f["mode"],
|
|
53
|
+
is_dir: f["isDir"],
|
|
54
|
+
mod_time: f["modTime"]
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get file metadata.
|
|
60
|
+
def info(path)
|
|
61
|
+
response = @execd.proxy(
|
|
62
|
+
method: :get,
|
|
63
|
+
path: "/files/info?path=#{escape(path)}"
|
|
64
|
+
)
|
|
65
|
+
data = JSON.parse(response.body)
|
|
66
|
+
FileInfo.new(
|
|
67
|
+
path: data["path"],
|
|
68
|
+
size: data["size"],
|
|
69
|
+
mode: data["mode"],
|
|
70
|
+
is_dir: data["isDir"],
|
|
71
|
+
mod_time: data["modTime"]
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Delete files.
|
|
76
|
+
def delete(paths)
|
|
77
|
+
@execd.proxy(method: :delete, path: "/files", body: { paths: paths })
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Create directories (mkdir -p semantics).
|
|
81
|
+
def mkdir(paths, mode: 755)
|
|
82
|
+
body = paths.to_h { |path| [ path, { mode: mode } ] }
|
|
83
|
+
@execd.proxy(method: :post, path: "/directories", body: body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def build_multipart_body(entries, boundary)
|
|
89
|
+
body = +"".b
|
|
90
|
+
entries.each do |entry|
|
|
91
|
+
metadata = { path: entry.path, mode: entry.mode || 644 }.to_json
|
|
92
|
+
data = entry.data.to_s.b
|
|
93
|
+
|
|
94
|
+
append_binary(body, "--#{boundary}\r\n")
|
|
95
|
+
append_binary(body, "Content-Disposition: form-data; name=\"metadata\"; filename=\"metadata\"\r\n")
|
|
96
|
+
append_binary(body, "Content-Type: application/json\r\n\r\n")
|
|
97
|
+
append_binary(body, metadata)
|
|
98
|
+
append_binary(body, "\r\n")
|
|
99
|
+
|
|
100
|
+
append_binary(body, "--#{boundary}\r\n")
|
|
101
|
+
append_binary(body, "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(entry.path)}\"\r\n")
|
|
102
|
+
append_binary(body, "Content-Type: application/octet-stream\r\n\r\n")
|
|
103
|
+
body << data
|
|
104
|
+
append_binary(body, "\r\n")
|
|
105
|
+
end
|
|
106
|
+
append_binary(body, "--#{boundary}--\r\n")
|
|
107
|
+
body
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def append_binary(body, value)
|
|
111
|
+
body << value.to_s.b
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def escape(value)
|
|
115
|
+
CGI.escape(value.to_s)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "execd/commands"
|
|
4
|
+
require_relative "execd/files"
|
|
5
|
+
require_relative "execd/code_interpreter"
|
|
6
|
+
|
|
7
|
+
module OpenSandbox
|
|
8
|
+
class Execd
|
|
9
|
+
EXECD_PORT = 44772
|
|
10
|
+
|
|
11
|
+
attr_reader :sandbox_id
|
|
12
|
+
|
|
13
|
+
def initialize(sandbox_id:, client: OpenSandbox.client)
|
|
14
|
+
@sandbox_id = sandbox_id
|
|
15
|
+
@client = client
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def commands
|
|
19
|
+
@commands ||= Execd::Commands.new(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def files
|
|
23
|
+
@files ||= Execd::Files.new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def code_interpreter
|
|
27
|
+
@code_interpreter ||= Execd::CodeInterpreter.new(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Low-level proxy helper — routes HTTP requests through the OpenSandbox server proxy
|
|
31
|
+
def proxy(method:, path:, body: nil, headers: {}, raw_body: nil)
|
|
32
|
+
@client.sandboxes.proxy(
|
|
33
|
+
@sandbox_id,
|
|
34
|
+
port: EXECD_PORT,
|
|
35
|
+
method: method,
|
|
36
|
+
path: path,
|
|
37
|
+
body: body,
|
|
38
|
+
headers: headers,
|
|
39
|
+
raw_body: raw_body
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def proxy_stream(method:, path:, body: nil, headers: {}, raw_body: nil, &block)
|
|
44
|
+
@client.sandboxes.proxy_stream(
|
|
45
|
+
@sandbox_id,
|
|
46
|
+
port: EXECD_PORT,
|
|
47
|
+
method: method,
|
|
48
|
+
path: path,
|
|
49
|
+
body: body,
|
|
50
|
+
headers: headers,
|
|
51
|
+
raw_body: raw_body,
|
|
52
|
+
&block
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ping
|
|
57
|
+
response = proxy(method: :get, path: "/ping")
|
|
58
|
+
response.code == 200
|
|
59
|
+
rescue OpenSandbox::Error
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "httparty"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
5
7
|
|
|
6
8
|
module OpenSandbox
|
|
7
9
|
# Low-level HTTP client for the OpenSandbox API.
|
|
@@ -38,13 +40,18 @@ module OpenSandbox
|
|
|
38
40
|
|
|
39
41
|
# Proxy raw HTTP request to sandbox internal service
|
|
40
42
|
# Returns the raw HTTParty response
|
|
41
|
-
def proxy(method, path, body: nil, headers: {})
|
|
42
|
-
request(method, path, body: body, headers: headers, raw: true)
|
|
43
|
+
def proxy(method, path, body: nil, headers: {}, raw_body: nil)
|
|
44
|
+
request(method, path, body: body, headers: headers, raw: true, raw_body: raw_body)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Stream raw HTTP response body chunks. Intended for SSE/JSONL execd proxy endpoints.
|
|
48
|
+
def proxy_stream(method, path, body: nil, headers: {}, raw_body: nil, &block)
|
|
49
|
+
stream_request(method, path, body: body, headers: headers, raw_body: raw_body, &block)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
private
|
|
46
53
|
|
|
47
|
-
def request(method, path, query: {}, body: nil, headers: {}, raw: false)
|
|
54
|
+
def request(method, path, query: {}, body: nil, headers: {}, raw: false, raw_body: nil)
|
|
48
55
|
url = "#{base_url}#{path}"
|
|
49
56
|
options = {
|
|
50
57
|
timeout: timeout,
|
|
@@ -53,7 +60,9 @@ module OpenSandbox
|
|
|
53
60
|
}
|
|
54
61
|
options[:query] = query if query && !query.empty?
|
|
55
62
|
|
|
56
|
-
if
|
|
63
|
+
if raw_body
|
|
64
|
+
options[:body] = raw_body
|
|
65
|
+
elsif body
|
|
57
66
|
options[:body] = body.to_json
|
|
58
67
|
options[:headers] = (options[:headers] || {}).merge("Content-Type" => "application/json")
|
|
59
68
|
end
|
|
@@ -74,6 +83,49 @@ module OpenSandbox
|
|
|
74
83
|
headers.merge(extra)
|
|
75
84
|
end
|
|
76
85
|
|
|
86
|
+
def stream_request(method, path, body: nil, headers: {}, raw_body: nil)
|
|
87
|
+
uri = URI("#{base_url}#{path}")
|
|
88
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
89
|
+
http.use_ssl = uri.scheme == "https"
|
|
90
|
+
http.open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
91
|
+
http.read_timeout = timeout
|
|
92
|
+
|
|
93
|
+
request_class = case method.to_sym
|
|
94
|
+
when :get then Net::HTTP::Get
|
|
95
|
+
when :post then Net::HTTP::Post
|
|
96
|
+
when :put then Net::HTTP::Put
|
|
97
|
+
when :delete then Net::HTTP::Delete
|
|
98
|
+
else
|
|
99
|
+
raise InvalidRequestError, "Unsupported streaming method: #{method}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
req = request_class.new(uri)
|
|
103
|
+
build_headers(headers).each { |key, value| req[key] = value }
|
|
104
|
+
|
|
105
|
+
if raw_body
|
|
106
|
+
req.body = raw_body
|
|
107
|
+
elsif body
|
|
108
|
+
req.body = body.to_json
|
|
109
|
+
req["Content-Type"] ||= "application/json"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
http.request(req) do |response|
|
|
113
|
+
unless response.code.to_i.between?(200, 299)
|
|
114
|
+
error_body = +""
|
|
115
|
+
response.read_body { |chunk| error_body << chunk }
|
|
116
|
+
raise Error, "Unexpected status #{response.code}: #{error_body}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
response.read_body do |chunk|
|
|
120
|
+
yield chunk.force_encoding("UTF-8")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
rescue ::Net::OpenTimeout, ::Net::ReadTimeout => e
|
|
124
|
+
raise ConnectionError, "Request timed out: #{e.message}"
|
|
125
|
+
rescue ::SocketError, ::Errno::ECONNREFUSED => e
|
|
126
|
+
raise ConnectionError, "Cannot connect to sandbox server at #{base_url}: #{e.message}"
|
|
127
|
+
end
|
|
128
|
+
|
|
77
129
|
def handle_response(response)
|
|
78
130
|
case response.code
|
|
79
131
|
when 200, 201, 202
|
|
@@ -137,12 +137,12 @@ module OpenSandbox
|
|
|
137
137
|
# @param body [Hash, nil] request body
|
|
138
138
|
# @param headers [Hash] additional headers
|
|
139
139
|
# @return [HTTParty::Response] raw response
|
|
140
|
-
def proxy(sandbox_id, port:, method: :get, path: "/", body: nil, headers: {})
|
|
141
|
-
proxy_path
|
|
142
|
-
|
|
143
|
-
: "/v1/sandboxes/#{sandbox_id}/proxy/#{port}/#{path.delete_prefix('/')}"
|
|
140
|
+
def proxy(sandbox_id, port:, method: :get, path: "/", body: nil, headers: {}, raw_body: nil)
|
|
141
|
+
@http.proxy(method, proxy_path(sandbox_id, port, path), body: body, headers: headers, raw_body: raw_body)
|
|
142
|
+
end
|
|
144
143
|
|
|
145
|
-
|
|
144
|
+
def proxy_stream(sandbox_id, port:, method: :get, path: "/", body: nil, headers: {}, raw_body: nil, &block)
|
|
145
|
+
@http.proxy_stream(method, proxy_path(sandbox_id, port, path), body: body, headers: headers, raw_body: raw_body, &block)
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# ── Diagnostics ─────────────────────────────────────────────────────────
|
|
@@ -220,6 +220,12 @@ module OpenSandbox
|
|
|
220
220
|
|
|
221
221
|
private
|
|
222
222
|
|
|
223
|
+
def proxy_path(sandbox_id, port, path)
|
|
224
|
+
path == "/" || path.empty? \
|
|
225
|
+
? "/v1/sandboxes/#{sandbox_id}/proxy/#{port}" \
|
|
226
|
+
: "/v1/sandboxes/#{sandbox_id}/proxy/#{port}/#{path.delete_prefix('/')}"
|
|
227
|
+
end
|
|
228
|
+
|
|
223
229
|
def build_image_spec(image, auth)
|
|
224
230
|
spec = { "uri" => image }
|
|
225
231
|
if auth
|
data/lib/open_sandbox/version.rb
CHANGED
data/lib/open_sandbox.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: open_sandbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Grayson Chen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httparty
|
|
@@ -48,6 +48,10 @@ files:
|
|
|
48
48
|
- lib/open_sandbox.rb
|
|
49
49
|
- lib/open_sandbox/client.rb
|
|
50
50
|
- lib/open_sandbox/errors.rb
|
|
51
|
+
- lib/open_sandbox/execd.rb
|
|
52
|
+
- lib/open_sandbox/execd/code_interpreter.rb
|
|
53
|
+
- lib/open_sandbox/execd/commands.rb
|
|
54
|
+
- lib/open_sandbox/execd/files.rb
|
|
51
55
|
- lib/open_sandbox/http_client.rb
|
|
52
56
|
- lib/open_sandbox/models.rb
|
|
53
57
|
- lib/open_sandbox/pools.rb
|
|
@@ -71,7 +75,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
71
75
|
requirements:
|
|
72
76
|
- - ">="
|
|
73
77
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 3.
|
|
78
|
+
version: 3.2.0
|
|
75
79
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
80
|
requirements:
|
|
77
81
|
- - ">="
|