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.
@@ -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
- def initialize(parent:, payload:)
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, claimed browser downloads,
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) when the session has no browser; 503
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 runtime's agent protocol. Coordinator capability discovery
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
@@ -1,3 +1,5 @@
1
1
  module PineSandbox
2
- VERSION = "0.3.18"
2
+ VERSION = "0.3.20"
3
+ # What the SDK's requests send as User-Agent.
4
+ USER_AGENT = "pinesandbox-ruby/#{VERSION}".freeze
3
5
  end
@@ -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 : <runtime_id>.computer.<zone> the Computer API + desktop stream, by path
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 runtime id): DNS-safe, <=63 chars.
24
- RUNTIME_ID_RE = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
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(runtime_id)
110
- host = "#{validate_runtime_id(runtime_id)}.computer.#{@zone}"
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 validate_runtime_id(id)
161
+ def validate_sandbox_id(id)
165
162
  s = id.to_s
166
- unless s.length <= 63 && s.match?(RUNTIME_ID_RE)
167
- raise ArgumentError, "invalid runtime id #{id.inspect}: must be a DNS-safe label (<=63 chars, lowercase [a-z0-9-], no leading/trailing/double hyphen)"
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/runtime_handle"
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/runtime_details"
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://pinesandbox.io"
13
+ spec.homepage = "https://pinecomputer.io"
14
14
  spec.license = "Apache-2.0"
15
- # SecureRandom.uuid_v7 (used for Computer ids) is Ruby 3.4+.
16
- spec.required_ruby_version = ">= 3.4"
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.18
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/runtime_details.rb
155
- - lib/pinesandbox/runtime_handle.rb
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://pinesandbox.io
168
+ homepage: https://pinecomputer.io
165
169
  licenses:
166
170
  - Apache-2.0
167
171
  metadata:
168
- homepage_uri: https://pinesandbox.io
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.4'
182
+ version: '3.3'
178
183
  required_rubygems_version: !ruby/object:Gem::Requirement
179
184
  requirements:
180
185
  - - ">="