pinesandbox 0.3.18 → 0.3.19

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.
@@ -55,7 +55,7 @@ module PineSandbox
55
55
  # An HTTP error response from the Computer API. #status is the HTTP status;
56
56
  # #problem_type and #problem_extensions carry an RFC 9457 problem detail
57
57
  # (#field, #reason, #handoff_id, #request_id and #retryable? read its common
58
- # members). The message is resource-first: the runtime (#runtime_id) and the
58
+ # members). The message is resource-first: the Computer sandbox (#sandbox_id) and the
59
59
  # operation (#op, "<METHOD> <path>") come before #request_id, so a handler
60
60
  # that logs only the message can still route a report.
61
61
  class ApiError < Error
@@ -73,14 +73,8 @@ module PineSandbox
73
73
  super(@built_message)
74
74
  end
75
75
 
76
- # The runtime that answered, when the request reached one.
77
- def runtime_id = problem_extensions["sandbox_id"]
78
-
79
- # Deprecated: use #runtime_id.
80
- def sandbox_id
81
- PineSandbox.deprecated_name("ApiError#sandbox_id", "#runtime_id")
82
- runtime_id
83
- end
76
+ # The id of the Computer sandbox that answered, when the request reached a Computer.
77
+ def sandbox_id = problem_extensions["sandbox_id"]
84
78
 
85
79
  # The response's machine `code` ({code, message} bodies), when present.
86
80
  def code = error_code
@@ -105,12 +99,11 @@ module PineSandbox
105
99
  # The response's Retry-After in seconds, when it set one.
106
100
  attr_accessor :retry_after
107
101
 
108
- # Folds the runtime and operation onto an error raised before they were
102
+ # Folds the sandbox id and operation onto an error raised before they were
109
103
  # known, filling only what the server didn't send, and rebuilds the message.
110
104
  # Returns self, for re-raising.
111
- def attach_context!(runtime_id: nil, op: nil, sandbox_id: nil)
112
- runtime_id ||= sandbox_id
113
- problem_extensions["sandbox_id"] ||= runtime_id if runtime_id && !runtime_id.to_s.empty?
105
+ def attach_context!(sandbox_id: nil, op: nil)
106
+ problem_extensions["sandbox_id"] ||= sandbox_id if sandbox_id && !sandbox_id.to_s.empty?
114
107
  problem_extensions["op"] ||= op if op && !op.to_s.empty?
115
108
  @built_message = build_message
116
109
  self
@@ -131,7 +124,7 @@ module PineSandbox
131
124
  "API request failed with status #{status}"
132
125
  end
133
126
  parts = []
134
- parts << "runtime=#{runtime_id}" if runtime_id && !runtime_id.to_s.empty?
127
+ parts << "sandbox=#{sandbox_id}" if sandbox_id && !sandbox_id.to_s.empty?
135
128
  parts << "op=#{op}" if op && !op.to_s.empty?
136
129
  parts << "request_id=#{request_id}" if request_id && !request_id.to_s.empty?
137
130
  parts.empty? ? base : "#{base} (#{parts.join(', ')})"
@@ -177,6 +170,10 @@ module PineSandbox
177
170
  # suspended or out of credits): control is cut, reads stay open. Re-attach,
178
171
  # or report the suspension.
179
172
  class LeaseExpired < ForbiddenError; end
173
+ # The project, or this Computer's binding, doesn't have a feature the call
174
+ # needs, such as the shell grant for Session#exec or the artifacts feature for
175
+ # the artifact methods. Retrying won't help.
176
+ class FeatureDisabled < ForbiddenError; end
180
177
  # The access lapsed and renewing it failed for now. Retry; don't re-attach.
181
178
  class LeaseRefreshUnavailable < ServiceUnavailableError; end
182
179
  class AnotherSessionHuman < ConflictError; end
@@ -198,6 +195,7 @@ module PineSandbox
198
195
  PROBLEM_TYPE_CLASSES = {
199
196
  "/errors/control-not-held" => ControlNotHeld,
200
197
  "/errors/lease-expired" => LeaseExpired,
198
+ "/errors/feature-disabled" => FeatureDisabled,
201
199
  "/errors/lease-refresh-unavailable" => LeaseRefreshUnavailable,
202
200
  "/errors/another-session-human" => AnotherSessionHuman,
203
201
  "/errors/concurrent-transition" => ConcurrentTransition,
@@ -230,20 +228,17 @@ module PineSandbox
230
228
  class ConnectionError < Error; end
231
229
  class ReadyTimeoutError < Error; end
232
230
 
233
- # The runtime reported failed, terminated or unavailable before the
231
+ # The Computer reported failed, terminated or unavailable before the
234
232
  # lifecycle operation finished.
235
- class RuntimeFailedError < Error
236
- attr_reader :runtime_id, :state
233
+ class SandboxFailedError < Error
234
+ attr_reader :sandbox_id, :state
237
235
 
238
- def initialize(message = nil, runtime_id: nil, state: nil)
239
- @runtime_id = runtime_id
236
+ def initialize(message = nil, sandbox_id: nil, state: nil)
237
+ @sandbox_id = sandbox_id
240
238
  @state = state
241
- super(message || "Computer runtime #{runtime_id} reported #{state.inspect} before completing the lifecycle operation")
239
+ super(message || "Computer sandbox #{sandbox_id} reported #{state.inspect} before completing the lifecycle operation")
242
240
  end
243
241
  end
244
- # Deprecated: use RuntimeFailedError.
245
- SandboxFailedError = RuntimeFailedError
246
- deprecate_constant :SandboxFailedError
247
242
 
248
243
  class StreamError < Error; end
249
244
  class StreamingUnsupportedError < Error; end
@@ -295,14 +290,14 @@ module PineSandbox
295
290
  end
296
291
  end
297
292
 
298
- # Start found an existing runtime. No replacement was allocated. Preserve the
299
- # BindError superclass for integrations already rescuing Start conflicts.
300
- class RuntimeRetainedError < BindError
301
- attr_reader :runtime_id, :state
293
+ # Start found the Computer still running or paused, and started nothing.
294
+ # Preserve the BindError superclass for integrations already rescuing Start conflicts.
295
+ class SandboxRetainedError < BindError
296
+ attr_reader :sandbox_id, :state
302
297
 
303
- def initialize(runtime_id:, state:)
304
- @runtime_id, @state = runtime_id, state
305
- super("Computer still has runtime #{runtime_id} (#{state}); Stop it or Resume retained memory before Start")
298
+ def initialize(sandbox_id:, state:)
299
+ @sandbox_id, @state = sandbox_id, state
300
+ super("Computer hasn't stopped (Computer sandbox #{sandbox_id}, state #{state}); Stop it or Resume retained memory before Start")
306
301
  end
307
302
  end
308
303
 
@@ -380,46 +375,34 @@ module PineSandbox
380
375
  class ProjectAccessDenied < AttachCredentialsError; end
381
376
 
382
377
  # 412 — another attach advanced the authorization revision. Terminal:
383
- # reload/adopt the winning integrator database binding; do not allocate a runtime.
378
+ # reload/adopt the winning integrator database binding; do not start the Computer again.
384
379
  class BindingRevisionConflictError < AttachCredentialsError
385
- attr_reader :current_binding_revision, :current_runtime_id
380
+ attr_reader :current_binding_revision, :current_sandbox_id
386
381
 
387
- def initialize(message, current_binding_revision: nil, current_runtime_id: nil, current_sandbox_id: nil, **kwargs)
382
+ def initialize(message, current_binding_revision: nil, current_sandbox_id: nil, **kwargs)
388
383
  @current_binding_revision = current_binding_revision
389
- @current_runtime_id = current_runtime_id || current_sandbox_id
384
+ @current_sandbox_id = current_sandbox_id
390
385
  super(message, **kwargs)
391
386
  end
392
387
 
393
388
  # The winning binding's revision (also #current_binding_revision).
394
389
  def current_revision = current_binding_revision
395
-
396
- # Deprecated: use #current_runtime_id.
397
- def current_sandbox_id
398
- PineSandbox.deprecated_name("BindingRevisionConflictError#current_sandbox_id", "#current_runtime_id")
399
- current_runtime_id
400
- end
401
390
  end
402
391
 
403
392
  # Portal committed a new authorization revision, but the later bind
404
- # failed. The SDK has cleaned up the fresh runtime best-effort. Persist the
393
+ # failed. The SDK has shut the Computer down best-effort. Persist the
405
394
  # revision before retrying so the next attach can supersede this attempt.
406
395
  # #credentials is present for create_computer, whose generated identity and
407
396
  # capture keypair would otherwise be lost because the method has no result.
408
397
  class AttachAuthorizationCommittedError < BindError
409
- attr_reader :binding_revision, :runtime_id, :credentials
398
+ attr_reader :binding_revision, :sandbox_id, :credentials
410
399
 
411
- def initialize(message, binding_revision:, runtime_id: nil, sandbox_id: nil, credentials: nil, **kwargs)
400
+ def initialize(message, binding_revision:, sandbox_id: nil, credentials: nil, **kwargs)
412
401
  @binding_revision = binding_revision
413
- @runtime_id = runtime_id || sandbox_id
402
+ @sandbox_id = sandbox_id
414
403
  @credentials = credentials
415
404
  super(message, **kwargs)
416
405
  end
417
-
418
- # Deprecated: use #runtime_id.
419
- def sandbox_id
420
- PineSandbox.deprecated_name("AttachAuthorizationCommittedError#sandbox_id", "#runtime_id")
421
- runtime_id
422
- end
423
406
  end
424
407
 
425
408
  # The Computer answered with an `X-Computer-Spec-Version` major this SDK
@@ -435,44 +418,44 @@ module PineSandbox
435
418
  end
436
419
 
437
420
  # The Computer rejected the portal-issued bind_token: bad signature, unknown
438
- # kid, iss/aud mismatch, expired, replayed jti, or issued for another runtime.
421
+ # kid, iss/aud mismatch, expired, replayed jti, or issued for another start.
439
422
  # Terminal — retrying with the same token cannot succeed.
440
423
  class BindAuthError < BindError; end
441
424
 
442
- # Another runtime holds the epoch lease for this Computer (a live attach
443
- # exists elsewhere, or you are attaching from two places at once).
425
+ # This Computer is already attached elsewhere: another attach holds its epoch
426
+ # lease, or you are attaching from two places at once.
444
427
  class ComputerAlreadyAttachedError < BindError; end
445
428
 
446
- # The allocated runtime can't be bound (it was already bound, or a prior
447
- # bind failed on it). The SDK deletes it before raising; retry attach for a
448
- # fresh runtime.
449
- class RuntimeUnusableError < BindError; end
450
- # Deprecated: use RuntimeUnusableError.
451
- PodPoisonedError = RuntimeUnusableError
429
+ # This Computer sandbox can't be attached (it was already attached, or a prior attach
430
+ # failed on it). The SDK shuts it down before raising; retry attach to start
431
+ # the Computer again.
432
+ class SandboxUnusableError < BindError; end
433
+ # Deprecated: use SandboxUnusableError.
434
+ PodPoisonedError = SandboxUnusableError
452
435
  deprecate_constant :PodPoisonedError
453
436
 
454
- # Saved state was unreachable during the bind; the runtime has been
455
- # cleaned up. Retry attach later (transient).
437
+ # Saved state was unreachable during the bind; the SDK has shut the Computer
438
+ # down. Retry attach later (transient).
456
439
  class SavedStateUnavailableError < BindError; end
457
440
  # Deprecated: use SavedStateUnavailableError.
458
441
  BrokerUnreachableError = SavedStateUnavailableError
459
442
  deprecate_constant :BrokerUnreachableError
460
443
 
461
444
  # The bind handshake exhausted its retries without a terminal
462
- # classification. Usually a runtime-allocation or network issue.
445
+ # classification. Usually a slow start or a network issue.
463
446
  class BindTimeoutError < BindError; end
464
447
 
465
448
  # The Computer returned 401 for its Computer token (ct_) — a REPORT that
466
449
  # it did not recognize the binding's token, NOT an instruction to
467
- # re-attach. ct_ is binding-lifetime (no idle expiry), so on a live,
468
- # current runtime this state is binding_auth_lost: surface it and
469
- # reconcile; only confirmed runtime-gone evidence justifies an attach
470
- # (a fresh runtime invalidates every ps_ and fences the old runtime's state).
450
+ # re-attach. ct_ is binding-lifetime (no idle expiry), so on a running
451
+ # Computer this state is binding_auth_lost: surface it and reconcile; only
452
+ # a Computer confirmed stopped justifies an attach (a new start invalidates
453
+ # every ps_ and fences the previous start's state).
471
454
  # A 401 for a session token (ps_) is an ordinary UnauthorizedError.
472
455
  class TokenRejectedError < BindError
473
- MESSAGE = "the Computer rejected the computer token (401). ct_ is binding-lifetime, so on a live " \
474
- "current runtime this is binding_auth_lost — reconcile; re-attach ONLY on confirmed " \
475
- "runtime-gone evidence (a fresh runtime invalidates every ps_)".freeze
456
+ MESSAGE = "the Computer rejected the computer token (401). ct_ is binding-lifetime, so on a " \
457
+ "running Computer this is binding_auth_lost — reconcile; re-attach ONLY once the " \
458
+ "Computer is confirmed stopped (a new start invalidates every ps_)".freeze
476
459
 
477
460
  # The rejection of `api_error` (a 401 answer to a Computer token), with
478
461
  # its operation and request id folded into the message.
@@ -482,14 +465,14 @@ module PineSandbox
482
465
  end
483
466
  end
484
467
 
485
- # The gateway can no longer route this Computer's attached runtime. This is
486
- # distinct from an /errors/session-not-found response on a live
468
+ # The gateway can no longer route to this Computer's host, which is also
469
+ # true while it's paused or recovering. This is distinct from an /errors/session-not-found response on a live
487
470
  # Computer. Inherit the legacy 404 tier so existing NotFoundError rescues
488
471
  # remain valid; the original typed ApiError is retained as Exception#cause.
489
- class RuntimeGoneError < NotFoundError
472
+ class SandboxGoneError < NotFoundError
490
473
  def initialize(api_error)
491
474
  detail = api_error.error_message.to_s
492
- message = "the attached runtime is gone"
475
+ message = "the Computer can't be reached at its host"
493
476
  message = "#{message}: #{detail}" unless detail.empty?
494
477
  super(
495
478
  status: api_error.status,
@@ -502,9 +485,6 @@ module PineSandbox
502
485
  self.retry_after = api_error.retry_after
503
486
  end
504
487
  end
505
- # Deprecated: use RuntimeGoneError.
506
- SandboxGoneError = RuntimeGoneError
507
- deprecate_constant :SandboxGoneError
508
488
 
509
489
  # 429 COMPUTER_CAPACITY_EXCEEDED on create/attach: the project is at its
510
490
  # concurrent Computer limit, so nothing was provisioned. Not retried by the
@@ -3,7 +3,7 @@ require "time"
3
3
  module PineSandbox
4
4
  module Models
5
5
  # A session artifact's metadata — a managed session output (a client upload,
6
- # a claimed browser download, or an agent task output) with a download
6
+ # a browser download, or an agent task output) with a download
7
7
  # endpoint. Returned by {PineSandbox::Session#list_artifacts} and
8
8
  # {PineSandbox::Session#upload_artifact}. Mirrors the spec `Artifact` schema.
9
9
  Artifact = Struct.new(
@@ -34,7 +34,6 @@ module PineSandbox
34
34
  ["cancelAgentV2", "POST", "/v2/sessions/{name}/agent/cancel", :computer, true, false],
35
35
  ["cancelAuthorRun", "POST", "/v2/sessions/{name}/skills/author/{author_id}/cancel", :computer, false, false],
36
36
  ["captureCheckpoint", "POST", "/v1/coord/capture", :computer, false, false],
37
- ["claimDownloadOrphan", "POST", "/downloads/orphans/{guid}/claim", :computer, false, false],
38
37
  ["computerUse", "POST", "/v1/sessions/{name}/computer-use", :computer, false, false],
39
38
  ["createComputer", "POST", "/computer-sandboxes", :control, true, false],
40
39
  ["createDiagnostic", "POST", "/v1/diagnostics", :control, true, true],
@@ -49,7 +48,6 @@ module PineSandbox
49
48
  ["deleteSession", "DELETE", "/sessions/{name}", :computer, true, false],
50
49
  ["deleteSkillVersion", "DELETE", "/v1/skills/{name}/versions/{version}", :computer, true, false],
51
50
  ["destroyComputer", "DELETE", "/sandboxes/{sandboxId}", :control, true, false],
52
- ["discardDownloadOrphan", "DELETE", "/downloads/orphans/{guid}", :computer, true, false],
53
51
  ["getAgentEventsPageV2", "GET", "/v2/sessions/{name}/agent/events/page", :computer, true, false],
54
52
  ["getAgentEventsV2", "GET", "/v2/sessions/{name}/agent/events", :computer, true, false],
55
53
  ["getAgentTurnV2", "GET", "/v2/sessions/{name}/agent/turns/{turn_id}", :computer, true, false],
@@ -74,7 +72,6 @@ module PineSandbox
74
72
  ["listAllSkillVersions", "GET", "/v1/skills/versions", :computer, true, false],
75
73
  ["listArtifacts", "GET", "/v1/sessions/{name}/artifacts", :computer, true, false],
76
74
  ["listAvailableLocations", "GET", "/v1/locations", :control, true, false],
77
- ["listDownloadOrphans", "GET", "/downloads/orphans", :computer, true, false],
78
75
  ["listHandoffs", "GET", "/v1/sessions/{name}/handoffs", :computer, true, false],
79
76
  ["listOrphans", "GET", "/orphans", :computer, true, false],
80
77
  ["listPasskeyCeremonies", "GET", "/v1/passkeys/ceremonies", :computer, true, false],
@@ -1,6 +1,6 @@
1
1
  module PineSandbox
2
- # An observation of one runtime, not the identity or a cached health verdict.
3
- RuntimeLifetime = Data.define(:idle_timeout_seconds, :min_idle_timeout_seconds,
2
+ # An observation of the started Computer, not its identity or a cached health verdict.
3
+ SandboxLifetime = Data.define(:idle_timeout_seconds, :min_idle_timeout_seconds,
4
4
  :max_idle_timeout_seconds, :max_runtime_age_seconds,
5
5
  :stop_at, :deadline) do
6
6
  def self.from_payload(value)
@@ -14,9 +14,9 @@ module PineSandbox
14
14
  end
15
15
  end
16
16
 
17
- RuntimeDetails = Data.define(:id, :state, :reason, :message, :retryable,
17
+ SandboxDetails = Data.define(:id, :state, :reason, :message, :retryable,
18
18
  :operation_id, :created_at, :expires_at, :lifecycle, :runtime_revision) do
19
- def self.from_runtime(info)
19
+ def self.from_info(info)
20
20
  new(id: info.id, state: info.status, reason: info.reason, message: info.message,
21
21
  retryable: info.retryable, operation_id: info.operation_id,
22
22
  created_at: info.created_at, expires_at: info.expires_at, lifecycle: info.lifecycle, runtime_revision: info.runtime_revision)
@@ -3,26 +3,25 @@ require_relative "errors"
3
3
  require_relative "clock"
4
4
 
5
5
  module PineSandbox
6
- # Lifecycle handle for a Computer's runtime, backed by the project-scoped
6
+ # Lifecycle handle for a Computer sandbox, backed by the project-scoped
7
7
  # ControlPlane. A powered-on Computer holds one of these;
8
8
  # Computer#stop/#kill/#alive? delegate here. It carries NO data-plane knowledge —
9
9
  # the coordinator host is derived separately from the Zone.
10
10
  # @api private
11
- class RuntimeHandle
11
+ class SandboxHandle
12
12
  READY = :running
13
13
  WAIT_FAILURE_STATES = %i[failed terminated unavailable].freeze
14
14
  LIVE = %i[running pending stopping].freeze
15
15
  DEFAULT_POLL_INTERVAL = 2
16
16
 
17
17
  attr_reader :id
18
- alias_method :runtime_id, :id
18
+ alias_method :sandbox_id, :id
19
19
 
20
20
  # `clock` returns monotonic seconds and `sleeper` waits; both default to
21
- # the control plane's clock. `sandbox_id:` is the deprecated spelling of
22
- # `runtime_id:`.
23
- def initialize(control_plane:, runtime_id: nil, sandbox_id: nil, status: nil, clock: nil, sleeper: nil)
24
- id = runtime_id || sandbox_id
25
- raise ArgumentError, "runtime_id required" if id.to_s.empty?
21
+ # the control plane's clock.
22
+ def initialize(control_plane:, sandbox_id: nil, status: nil, clock: nil, sleeper: nil)
23
+ id = sandbox_id
24
+ raise ArgumentError, "sandbox_id required" if id.to_s.empty?
26
25
 
27
26
  source = control_plane.respond_to?(:clock) ? control_plane.clock : PineSandbox::SYSTEM_CLOCK
28
27
  @control_plane = control_plane
@@ -53,13 +52,13 @@ module PineSandbox
53
52
  end
54
53
 
55
54
  def set_idle_timeout(seconds:)
56
- observed = runtime_details
57
- raise BindError, "Runtime has no admitted lifetime policy; choose idle timeout on the next Start" unless observed.lifecycle
55
+ observed = sandbox_details
56
+ raise BindError, "Computer was started without a lifetime policy; choose the idle timeout on the next Start" unless observed.lifecycle
58
57
 
59
58
  @control_plane.set_idle_timeout(@id, seconds: seconds, etag: observed.etag)
60
59
  end
61
60
 
62
- def runtime_details
61
+ def sandbox_details
63
62
  @control_plane.get(@id)
64
63
  end
65
64
 
@@ -101,11 +100,11 @@ module PineSandbox
101
100
 
102
101
  # Explicit saved-state recovery discards retained memory. It is allowed
103
102
  # only after a recorded failure and is fenced by the observed HTTP ETag.
104
- def release_failed_runtime(info, wait_timeout: 90, interval: DEFAULT_POLL_INTERVAL)
103
+ def release_failed_sandbox(info, wait_timeout: 90, interval: DEFAULT_POLL_INTERVAL)
105
104
  deadline = @clock.call + wait_timeout
106
105
  unless info.status == :stopping
107
106
  unless %i[save_failed unavailable failed terminated].include?(info.status) && info.reason != "CaptureTimeout"
108
- raise PineSandbox::ConflictError.new(status: 409, error_message: "Runtime has no recorded failure; use Stop or Resume")
107
+ raise PineSandbox::ConflictError.new(status: 409, error_message: "The Computer has no recorded failure; use Stop or Resume")
109
108
  end
110
109
  raise PineSandbox::Error, "Server did not supply a conditional release version" if info.etag.to_s.empty?
111
110
  @control_plane.destroy(@id, if_match: info.etag, request_timeout: remaining(deadline))
@@ -229,8 +228,8 @@ module PineSandbox
229
228
  end
230
229
  return self if state == target
231
230
  if WAIT_FAILURE_STATES.include?(state)
232
- raise PineSandbox::RuntimeFailedError.new(
233
- "Computer runtime #{@id} reported #{state.inspect} before reaching #{target}", runtime_id: @id, state: state
231
+ raise PineSandbox::SandboxFailedError.new(
232
+ "Computer sandbox #{@id} reported #{state.inspect} before reaching #{target}", sandbox_id: @id, state: state
234
233
  )
235
234
  end
236
235
  end
@@ -245,8 +244,4 @@ module PineSandbox
245
244
  end
246
245
 
247
246
  end
248
-
249
- # Deprecated: use RuntimeHandle.
250
- SandboxHandle = RuntimeHandle
251
- deprecate_constant :SandboxHandle
252
247
  end
@@ -37,6 +37,9 @@ module PineSandbox
37
37
  info.label
38
38
  end
39
39
 
40
+ # The session token (ps_). A session read with Computer#session or
41
+ # Computer#sessions has none: only Computer#create_session returns it, so
42
+ # save it then.
40
43
  def token
41
44
  info.token
42
45
  end
@@ -286,6 +289,23 @@ module PineSandbox
286
289
  read_call { |read| coordinator.read_file(token: read, name: name, path: path, &block) }
287
290
  end
288
291
 
292
+ # A workdir file's bytes as a stream, for copying a large file onward
293
+ # without buffering it, such as a file a turn's result names: yields binary
294
+ # chunks as they arrive. Without a block, returns an Enumerator of the
295
+ # chunks that sends the request only when iterated
296
+ # (`session.open_file(path).each { |chunk| io.write(chunk) }`).
297
+ # An error answer raises before any chunk.
298
+ #
299
+ # @param path [String] workdir-relative file path.
300
+ # @yieldparam chunk [String] the next bytes, binary encoding.
301
+ # @return [nil, Enumerator]
302
+ def open_file(path, &block)
303
+ raise ArgumentError, "file path is required" if path.to_s.empty?
304
+ return enum_for(:open_file, path) unless block
305
+
306
+ read_call { |read| coordinator.read_file(token: read, name: name, path: path, &block) }
307
+ end
308
+
289
309
  # Deposits a material (an agent INPUT file) into the session's attachments/
290
310
  # root. `filename` is a single name — path separators ("/", "..") are
291
311
  # rejected. The attachments root is a SIBLING of the workdir that
@@ -300,7 +320,7 @@ module PineSandbox
300
320
  read_call { |read| coordinator.write_file(token: read, name: name, path: filename, content: content) }
301
321
  end
302
322
 
303
- # Lists the session's artifacts — client uploads, claimed browser downloads,
323
+ # Lists the session's artifacts — client uploads, browser downloads,
304
324
  # and every completed turn's agent outputs (durably captured at each turn's
305
325
  # result). Newest-first.
306
326
  #
@@ -479,7 +499,7 @@ module PineSandbox
479
499
  # reach the browser. Driving needs control of the session, not this
480
500
  # token, so the same token serves a viewer and a driver. Re-mint before
481
501
  # each (re)connect. Returns { token:, expires_at: }. 409
482
- # (NoDesktopStream) when the session has no browser; 503
502
+ # (NoDesktopStream) when the session is blind; 503
483
503
  # (DesktopUnavailable) when streaming is off.
484
504
  def desktop_token
485
505
  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
@@ -1,3 +1,3 @@
1
1
  module PineSandbox
2
- VERSION = "0.3.18"
2
+ VERSION = "0.3.19"
3
3
  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
@@ -26,11 +26,11 @@ require_relative "pinesandbox/control_token_source"
26
26
  require_relative "pinesandbox/attach_credentials_source"
27
27
  require_relative "pinesandbox/spec_version_middleware"
28
28
  require_relative "pinesandbox/control_plane"
29
- require_relative "pinesandbox/runtime_handle"
29
+ require_relative "pinesandbox/sandbox_handle"
30
30
  require_relative "pinesandbox/connection"
31
31
  require_relative "pinesandbox/computer"
32
32
  require_relative "pinesandbox/computer_binding"
33
- require_relative "pinesandbox/runtime_details"
33
+ require_relative "pinesandbox/sandbox_details"
34
34
  require_relative "pinesandbox/client"
35
35
 
36
36
  # 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.19
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
@@ -151,8 +152,8 @@ files:
151
152
  - lib/pinesandbox/models/file_entry.rb
152
153
  - lib/pinesandbox/operations.rb
153
154
  - lib/pinesandbox/response_handler.rb
154
- - lib/pinesandbox/runtime_details.rb
155
- - lib/pinesandbox/runtime_handle.rb
155
+ - lib/pinesandbox/sandbox_details.rb
156
+ - lib/pinesandbox/sandbox_handle.rb
156
157
  - lib/pinesandbox/session.rb
157
158
  - lib/pinesandbox/skills.rb
158
159
  - lib/pinesandbox/spec_version_middleware.rb
@@ -161,11 +162,12 @@ files:
161
162
  - lib/pinesandbox/version.rb
162
163
  - lib/pinesandbox/zone.rb
163
164
  - pinesandbox.gemspec
164
- homepage: https://pinesandbox.io
165
+ homepage: https://pinecomputer.io
165
166
  licenses:
166
167
  - Apache-2.0
167
168
  metadata:
168
- homepage_uri: https://pinesandbox.io
169
+ homepage_uri: https://pinecomputer.io
170
+ documentation_uri: https://console.pinecomputer.io/docs
169
171
  allowed_push_host: https://rubygems.org
170
172
  rdoc_options: []
171
173
  require_paths:
@@ -174,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
174
176
  requirements:
175
177
  - - ">="
176
178
  - !ruby/object:Gem::Version
177
- version: '3.4'
179
+ version: '3.3'
178
180
  required_rubygems_version: !ruby/object:Gem::Requirement
179
181
  requirements:
180
182
  - - ">="