pinesandbox 0.3.18 → 0.3.20
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/LICENSE +201 -0
- data/README.md +222 -161
- data/lib/pinesandbox/adapters/coordinator_adapter.rb +90 -114
- data/lib/pinesandbox/agent_events.rb +2 -2
- data/lib/pinesandbox/agent_turn.rb +25 -2
- data/lib/pinesandbox/artifacts.rb +776 -0
- data/lib/pinesandbox/attach_credentials_source.rb +58 -16
- data/lib/pinesandbox/bind_hpke.rb +4 -4
- data/lib/pinesandbox/client.rb +75 -37
- data/lib/pinesandbox/computer.rb +146 -116
- data/lib/pinesandbox/computer_binding.rb +14 -10
- data/lib/pinesandbox/control_plane.rb +10 -10
- data/lib/pinesandbox/error_taxonomy.rb +3 -0
- data/lib/pinesandbox/errors.rb +83 -76
- data/lib/pinesandbox/models/artifact.rb +1 -1
- data/lib/pinesandbox/models/artifact_file.rb +40 -0
- data/lib/pinesandbox/models/computer_session.rb +7 -3
- data/lib/pinesandbox/operations.rb +2 -3
- data/lib/pinesandbox/{runtime_details.rb → sandbox_details.rb} +4 -4
- data/lib/pinesandbox/{runtime_handle.rb → sandbox_handle.rb} +16 -20
- data/lib/pinesandbox/session.rb +39 -4
- data/lib/pinesandbox/spec_version_middleware.rb +1 -1
- data/lib/pinesandbox/streamed_read.rb +80 -0
- data/lib/pinesandbox/version.rb +3 -1
- data/lib/pinesandbox/zone.rb +8 -11
- data/lib/pinesandbox.rb +4 -2
- data/pinesandbox.gemspec +5 -3
- metadata +11 -6
data/lib/pinesandbox/session.rb
CHANGED
|
@@ -19,11 +19,17 @@ module PineSandbox
|
|
|
19
19
|
# tabs, focus, epoch, drive actions, passkey intent and decisions) need the
|
|
20
20
|
# session token.
|
|
21
21
|
class Session
|
|
22
|
+
# What a session's agent may do with the Computer's artifacts
|
|
23
|
+
# (Computer#create_session `artifact_access:`).
|
|
24
|
+
ARTIFACT_ACCESS = %w[none read_only read_write].freeze
|
|
25
|
+
|
|
22
26
|
attr_reader :info, :parent
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
# `artifact_access_known` is false when only the Computer knows the
|
|
29
|
+
# session's artifact access (Computer#adopt_session).
|
|
30
|
+
def initialize(parent:, payload:, artifact_access_known: true)
|
|
25
31
|
@parent = parent
|
|
26
|
-
@info = Models::Session.from_payload(payload)
|
|
32
|
+
@info = Models::Session.from_payload(payload, artifact_access_known: artifact_access_known)
|
|
27
33
|
raise PineSandbox::ApiError.new(status: 0, error_message: "empty session payload") if @info.nil?
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -37,6 +43,9 @@ module PineSandbox
|
|
|
37
43
|
info.label
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# The session token (ps_). A session read with Computer#session or
|
|
47
|
+
# Computer#sessions has none: only Computer#create_session returns it, so
|
|
48
|
+
# save it then.
|
|
40
49
|
def token
|
|
41
50
|
info.token
|
|
42
51
|
end
|
|
@@ -49,6 +58,15 @@ module PineSandbox
|
|
|
49
58
|
info.session_dir
|
|
50
59
|
end
|
|
51
60
|
|
|
61
|
+
# What this session's agent may do with the Computer's artifacts, as set
|
|
62
|
+
# at create: "none", "read_only" or "read_write". "none" when the Computer
|
|
63
|
+
# doesn't say (an older one). nil for a session from
|
|
64
|
+
# Computer#adopt_session, whose access isn't known: read it with
|
|
65
|
+
# Computer#session(name).
|
|
66
|
+
def artifact_access
|
|
67
|
+
info.artifact_access
|
|
68
|
+
end
|
|
69
|
+
|
|
52
70
|
# session.agent.* — hand-off-the-loop driving (run/steer/answer/cancel/reset/
|
|
53
71
|
# status/result/events). The agent drives; you delegate goals. See {AgentMode}.
|
|
54
72
|
def agent
|
|
@@ -286,6 +304,23 @@ module PineSandbox
|
|
|
286
304
|
read_call { |read| coordinator.read_file(token: read, name: name, path: path, &block) }
|
|
287
305
|
end
|
|
288
306
|
|
|
307
|
+
# A workdir file's bytes as a stream, for copying a large file onward
|
|
308
|
+
# without buffering it, such as a file a turn's result names: yields binary
|
|
309
|
+
# chunks as they arrive. Without a block, returns an Enumerator of the
|
|
310
|
+
# chunks that sends the request only when iterated
|
|
311
|
+
# (`session.open_file(path).each { |chunk| io.write(chunk) }`).
|
|
312
|
+
# An error answer raises before any chunk.
|
|
313
|
+
#
|
|
314
|
+
# @param path [String] workdir-relative file path.
|
|
315
|
+
# @yieldparam chunk [String] the next bytes, binary encoding.
|
|
316
|
+
# @return [nil, Enumerator]
|
|
317
|
+
def open_file(path, &block)
|
|
318
|
+
raise ArgumentError, "file path is required" if path.to_s.empty?
|
|
319
|
+
return enum_for(:open_file, path) unless block
|
|
320
|
+
|
|
321
|
+
read_call { |read| coordinator.read_file(token: read, name: name, path: path, &block) }
|
|
322
|
+
end
|
|
323
|
+
|
|
289
324
|
# Deposits a material (an agent INPUT file) into the session's attachments/
|
|
290
325
|
# root. `filename` is a single name — path separators ("/", "..") are
|
|
291
326
|
# rejected. The attachments root is a SIBLING of the workdir that
|
|
@@ -300,7 +335,7 @@ module PineSandbox
|
|
|
300
335
|
read_call { |read| coordinator.write_file(token: read, name: name, path: filename, content: content) }
|
|
301
336
|
end
|
|
302
337
|
|
|
303
|
-
# Lists the session's artifacts — client uploads,
|
|
338
|
+
# Lists the session's artifacts — client uploads, browser downloads,
|
|
304
339
|
# and every completed turn's agent outputs (durably captured at each turn's
|
|
305
340
|
# result). Newest-first.
|
|
306
341
|
#
|
|
@@ -479,7 +514,7 @@ module PineSandbox
|
|
|
479
514
|
# reach the browser. Driving needs control of the session, not this
|
|
480
515
|
# token, so the same token serves a viewer and a driver. Re-mint before
|
|
481
516
|
# each (re)connect. Returns { token:, expires_at: }. 409
|
|
482
|
-
# (NoDesktopStream)
|
|
517
|
+
# (NoDesktopStream) for a session an older SDK created as blind; 503
|
|
483
518
|
# (DesktopUnavailable) when streaming is off.
|
|
484
519
|
def desktop_token
|
|
485
520
|
ct_call { coordinator.mint_desktop_token(token: parent.computer_token, name: name) }
|
|
@@ -5,7 +5,7 @@ module PineSandbox
|
|
|
5
5
|
# Control-plane middleware: send the expected gateway generation and reject a
|
|
6
6
|
# present, different response marker. Missing markers remain tolerated.
|
|
7
7
|
# The gateway does not negotiate this request value, and its marker does not
|
|
8
|
-
# qualify a Computer
|
|
8
|
+
# qualify a Computer sandbox's agent protocol. Coordinator capability discovery
|
|
9
9
|
# owns that check; this middleware is not installed on the Ruby data plane.
|
|
10
10
|
class SpecVersionMiddleware < Faraday::Middleware
|
|
11
11
|
REQUEST_HEADER = "Computer-Spec-Version".freeze
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
|
|
3
|
+
module PineSandbox
|
|
4
|
+
# @api private — what the SDK's streamed reads of a body share: a file read
|
|
5
|
+
# from a Computer (Adapters::CoordinatorAdapter) and an artifact read from
|
|
6
|
+
# storage (Artifacts). Only a 2xx body reaches the caller, chunk by chunk as
|
|
7
|
+
# it arrives; an error answer's body is kept, bounded, for its error.
|
|
8
|
+
module StreamedRead
|
|
9
|
+
# A failed answer keeps this much of its body for the error.
|
|
10
|
+
ERROR_BODY_CAP = 64 * 1024
|
|
11
|
+
# Chunk size for passing on a body that was spooled to a temp file.
|
|
12
|
+
SPOOL_READ_SIZE = 64 * 1024
|
|
13
|
+
|
|
14
|
+
# Raised through Faraday's read loop in place of an exception from the
|
|
15
|
+
# caller's block, so the block's own error is never mistaken for a
|
|
16
|
+
# dropped connection. The streaming method raises #error again as it was.
|
|
17
|
+
class BlockError < StandardError
|
|
18
|
+
attr_reader :error
|
|
19
|
+
|
|
20
|
+
def initialize(error)
|
|
21
|
+
@error = error
|
|
22
|
+
super(error.message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Calls the caller's block with `args` from inside Faraday's read loop,
|
|
29
|
+
# which would turn an IOError or EPIPE from the block into a dropped
|
|
30
|
+
# connection; the block's own error comes out as a BlockError.
|
|
31
|
+
def pass(block, *args)
|
|
32
|
+
block.call(*args)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
raise BlockError.new(e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Adds as much of `chunk` to `buffer` as keeps it within ERROR_BODY_CAP.
|
|
38
|
+
def keep_error_body(buffer, chunk)
|
|
39
|
+
remaining = ERROR_BODY_CAP - buffer.bytesize
|
|
40
|
+
buffer << chunk.byteslice(0, remaining) if remaining.positive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reads one streamed GET. Yields the proc to set as the request's
|
|
44
|
+
# on_data; the block sends the request and returns its response. Each
|
|
45
|
+
# chunk of a 2xx body goes to `deliver` with its answer (the Faraday env,
|
|
46
|
+
# or the response) as it arrives. faraday-net_http before 3.0 passes no
|
|
47
|
+
# env and learns the status only after the body, so its chunks go to a
|
|
48
|
+
# temp file, not memory, and on to `deliver` once the status is known. An
|
|
49
|
+
# error answer's body, kept up to ERROR_BODY_CAP, becomes the response's
|
|
50
|
+
# body. Returns the response.
|
|
51
|
+
def read(deliver)
|
|
52
|
+
error_body = +""
|
|
53
|
+
spool = nil
|
|
54
|
+
on_data = proc do |chunk, _received, env|
|
|
55
|
+
if env.nil?
|
|
56
|
+
(spool ||= Tempfile.new("pinesandbox-read", binmode: true)).write(chunk)
|
|
57
|
+
elsif env.status.to_i.between?(200, 299)
|
|
58
|
+
deliver.call(chunk.b, env) unless chunk.empty?
|
|
59
|
+
else
|
|
60
|
+
keep_error_body(error_body, chunk)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
response = yield on_data
|
|
64
|
+
if spool
|
|
65
|
+
spool.rewind
|
|
66
|
+
if response.success?
|
|
67
|
+
while (chunk = spool.read(SPOOL_READ_SIZE))
|
|
68
|
+
deliver.call(chunk, response)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
keep_error_body(error_body, spool.read(ERROR_BODY_CAP).to_s)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
response.env.body = error_body if !response.success? && response.body.to_s.empty?
|
|
75
|
+
response
|
|
76
|
+
ensure
|
|
77
|
+
spool&.close!
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/pinesandbox/version.rb
CHANGED
data/lib/pinesandbox/zone.rb
CHANGED
|
@@ -11,7 +11,7 @@ module PineSandbox
|
|
|
11
11
|
# endpoint; the gateway routes by path. The bare
|
|
12
12
|
# <zone> host is the HUMAN portal — the SDK
|
|
13
13
|
# never calls it.
|
|
14
|
-
# data plane : <
|
|
14
|
+
# data plane : <sandbox_id>.computer.<zone> the Computer API + desktop stream, by path
|
|
15
15
|
#
|
|
16
16
|
# `control_host` may be overridden (local/dev/private). `gateway_url` is for a caller inside the
|
|
17
17
|
# gateway's own cluster: every connection then dials that URL and keeps its host as the Host header,
|
|
@@ -20,11 +20,8 @@ module PineSandbox
|
|
|
20
20
|
# with an actionable message.
|
|
21
21
|
class Zone
|
|
22
22
|
ZONE_RE = /\A(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/
|
|
23
|
-
# The Computer host label (the
|
|
24
|
-
|
|
25
|
-
# Deprecated: use RUNTIME_ID_RE.
|
|
26
|
-
SANDBOX_ID_RE = RUNTIME_ID_RE
|
|
27
|
-
deprecate_constant :SANDBOX_ID_RE
|
|
23
|
+
# The Computer host label (the sandbox id): DNS-safe, <=63 chars.
|
|
24
|
+
SANDBOX_ID_RE = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
|
|
28
25
|
|
|
29
26
|
attr_reader :zone, :gateway_url
|
|
30
27
|
|
|
@@ -106,8 +103,8 @@ module PineSandbox
|
|
|
106
103
|
# subdomain selects the Computer, the PATH selects the role). For a `.localhost`
|
|
107
104
|
# / `.lvh.me` zone there is no L7, so the host carries the local gateway's
|
|
108
105
|
# high port — supplied by the caller as part of the zone string.
|
|
109
|
-
def data_host(
|
|
110
|
-
host = "#{
|
|
106
|
+
def data_host(sandbox_id)
|
|
107
|
+
host = "#{validate_sandbox_id(sandbox_id)}.computer.#{@zone}"
|
|
111
108
|
secure? ? host : "#{host}:#{@data_port}"
|
|
112
109
|
end
|
|
113
110
|
|
|
@@ -161,10 +158,10 @@ module PineSandbox
|
|
|
161
158
|
n
|
|
162
159
|
end
|
|
163
160
|
|
|
164
|
-
def
|
|
161
|
+
def validate_sandbox_id(id)
|
|
165
162
|
s = id.to_s
|
|
166
|
-
unless s.length <= 63 && s.match?(
|
|
167
|
-
raise ArgumentError, "invalid
|
|
163
|
+
unless s.length <= 63 && s.match?(SANDBOX_ID_RE)
|
|
164
|
+
raise ArgumentError, "invalid sandbox id #{id.inspect}: must be a DNS-safe label (<=63 chars, lowercase [a-z0-9-], no leading/trailing/double hyphen)"
|
|
168
165
|
end
|
|
169
166
|
s
|
|
170
167
|
end
|
data/lib/pinesandbox.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "pinesandbox/models/computer_session"
|
|
|
8
8
|
require_relative "pinesandbox/models/computer_tab"
|
|
9
9
|
require_relative "pinesandbox/models/file_entry"
|
|
10
10
|
require_relative "pinesandbox/models/artifact"
|
|
11
|
+
require_relative "pinesandbox/models/artifact_file"
|
|
11
12
|
require_relative "pinesandbox/sse_frame_parser"
|
|
12
13
|
require_relative "pinesandbox/control_events"
|
|
13
14
|
require_relative "pinesandbox/agent_events"
|
|
@@ -20,17 +21,18 @@ require_relative "pinesandbox/capture_keypair"
|
|
|
20
21
|
require_relative "pinesandbox/adapters/coordinator_adapter"
|
|
21
22
|
require_relative "pinesandbox/adapters/snapshot_adapter"
|
|
22
23
|
require_relative "pinesandbox/skills"
|
|
24
|
+
require_relative "pinesandbox/artifacts"
|
|
23
25
|
require_relative "pinesandbox/author_terminal_translator"
|
|
24
26
|
require_relative "pinesandbox/session"
|
|
25
27
|
require_relative "pinesandbox/control_token_source"
|
|
26
28
|
require_relative "pinesandbox/attach_credentials_source"
|
|
27
29
|
require_relative "pinesandbox/spec_version_middleware"
|
|
28
30
|
require_relative "pinesandbox/control_plane"
|
|
29
|
-
require_relative "pinesandbox/
|
|
31
|
+
require_relative "pinesandbox/sandbox_handle"
|
|
30
32
|
require_relative "pinesandbox/connection"
|
|
31
33
|
require_relative "pinesandbox/computer"
|
|
32
34
|
require_relative "pinesandbox/computer_binding"
|
|
33
|
-
require_relative "pinesandbox/
|
|
35
|
+
require_relative "pinesandbox/sandbox_details"
|
|
34
36
|
require_relative "pinesandbox/client"
|
|
35
37
|
|
|
36
38
|
# PineSandbox — the Pine Computer SDK. Create a Computer and attach to it with
|
data/pinesandbox.gemspec
CHANGED
|
@@ -10,13 +10,14 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.description = "Ruby SDK for Pine Computers: create a Computer for your project, attach to it, " \
|
|
11
11
|
"and drive its browser, desktop, shell and agent, with state that persists " \
|
|
12
12
|
"across stops and starts."
|
|
13
|
-
spec.homepage = "https://
|
|
13
|
+
spec.homepage = "https://pinecomputer.io"
|
|
14
14
|
spec.license = "Apache-2.0"
|
|
15
|
-
# SecureRandom.uuid_v7 (used for Computer ids)
|
|
16
|
-
spec.required_ruby_version = ">= 3.
|
|
15
|
+
# SecureRandom.uuid_v7 (used for Computer ids) needs Ruby 3.3+.
|
|
16
|
+
spec.required_ruby_version = ">= 3.3"
|
|
17
17
|
|
|
18
18
|
spec.metadata = {
|
|
19
19
|
"homepage_uri" => spec.homepage,
|
|
20
|
+
"documentation_uri" => "https://console.pinecomputer.io/docs",
|
|
20
21
|
"allowed_push_host" => "https://rubygems.org"
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
24
25
|
`git ls-files -z`.split("\x0").select do |file|
|
|
25
26
|
file.start_with?("lib/") || %w[
|
|
26
27
|
Gemfile
|
|
28
|
+
LICENSE
|
|
27
29
|
Rakefile
|
|
28
30
|
README.md
|
|
29
31
|
pinesandbox.gemspec
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pinesandbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pine AI
|
|
@@ -117,6 +117,7 @@ extensions: []
|
|
|
117
117
|
extra_rdoc_files: []
|
|
118
118
|
files:
|
|
119
119
|
- Gemfile
|
|
120
|
+
- LICENSE
|
|
120
121
|
- README.md
|
|
121
122
|
- Rakefile
|
|
122
123
|
- lib/pinesandbox.rb
|
|
@@ -125,6 +126,7 @@ files:
|
|
|
125
126
|
- lib/pinesandbox/agent_events.rb
|
|
126
127
|
- lib/pinesandbox/agent_mode.rb
|
|
127
128
|
- lib/pinesandbox/agent_turn.rb
|
|
129
|
+
- lib/pinesandbox/artifacts.rb
|
|
128
130
|
- lib/pinesandbox/attach_credentials_source.rb
|
|
129
131
|
- lib/pinesandbox/author_terminal_translator.rb
|
|
130
132
|
- lib/pinesandbox/bind_hpke.rb
|
|
@@ -146,26 +148,29 @@ files:
|
|
|
146
148
|
- lib/pinesandbox/errors.rb
|
|
147
149
|
- lib/pinesandbox/exec_stream_parser.rb
|
|
148
150
|
- lib/pinesandbox/models/artifact.rb
|
|
151
|
+
- lib/pinesandbox/models/artifact_file.rb
|
|
149
152
|
- lib/pinesandbox/models/computer_session.rb
|
|
150
153
|
- lib/pinesandbox/models/computer_tab.rb
|
|
151
154
|
- lib/pinesandbox/models/file_entry.rb
|
|
152
155
|
- lib/pinesandbox/operations.rb
|
|
153
156
|
- lib/pinesandbox/response_handler.rb
|
|
154
|
-
- lib/pinesandbox/
|
|
155
|
-
- lib/pinesandbox/
|
|
157
|
+
- lib/pinesandbox/sandbox_details.rb
|
|
158
|
+
- lib/pinesandbox/sandbox_handle.rb
|
|
156
159
|
- lib/pinesandbox/session.rb
|
|
157
160
|
- lib/pinesandbox/skills.rb
|
|
158
161
|
- lib/pinesandbox/spec_version_middleware.rb
|
|
159
162
|
- lib/pinesandbox/sse_frame_parser.rb
|
|
160
163
|
- lib/pinesandbox/state_hpke.rb
|
|
164
|
+
- lib/pinesandbox/streamed_read.rb
|
|
161
165
|
- lib/pinesandbox/version.rb
|
|
162
166
|
- lib/pinesandbox/zone.rb
|
|
163
167
|
- pinesandbox.gemspec
|
|
164
|
-
homepage: https://
|
|
168
|
+
homepage: https://pinecomputer.io
|
|
165
169
|
licenses:
|
|
166
170
|
- Apache-2.0
|
|
167
171
|
metadata:
|
|
168
|
-
homepage_uri: https://
|
|
172
|
+
homepage_uri: https://pinecomputer.io
|
|
173
|
+
documentation_uri: https://console.pinecomputer.io/docs
|
|
169
174
|
allowed_push_host: https://rubygems.org
|
|
170
175
|
rdoc_options: []
|
|
171
176
|
require_paths:
|
|
@@ -174,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
174
179
|
requirements:
|
|
175
180
|
- - ">="
|
|
176
181
|
- !ruby/object:Gem::Version
|
|
177
|
-
version: '3.
|
|
182
|
+
version: '3.3'
|
|
178
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
184
|
requirements:
|
|
180
185
|
- - ">="
|