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.
@@ -4,7 +4,7 @@ require "time"
4
4
  require_relative "errors"
5
5
  require_relative "clock"
6
6
  require_relative "operations"
7
- require_relative "runtime_details"
7
+ require_relative "sandbox_details"
8
8
  require_relative "spec_version_middleware"
9
9
 
10
10
  module PineSandbox
@@ -14,7 +14,7 @@ module PineSandbox
14
14
  # specs/computer-api.yaml lifecycle routes (root, no /v1).
15
15
  # @api private
16
16
  class ControlPlane
17
- Runtime = Struct.new(:id, :image, :status, :metadata, :entrypoint,
17
+ SandboxInfo = Struct.new(:id, :image, :status, :metadata, :entrypoint,
18
18
  :expires_at, :created_at, :operation_id,
19
19
  :retryable, :reason, :message, :etag, :lifecycle, :runtime_revision, keyword_init: true)
20
20
 
@@ -49,7 +49,7 @@ module PineSandbox
49
49
  # Internal adapter for the public Computer contract. No generic sandbox
50
50
  # create, image selection or task dispatch is available through this client.
51
51
  def create_computer(body:, request_timeout: nil)
52
- # A create ID permits identical retries without extending runtime expiry.
52
+ # A create ID permits identical retries without extending the Computer's expiry.
53
53
  keyed = !!body[:create_id]
54
54
  deadline = monotonic_now + request_timeout if request_timeout
55
55
  busy_repeats = 0
@@ -135,7 +135,7 @@ module PineSandbox
135
135
  true
136
136
  end
137
137
 
138
- # These methods report acceptance. RuntimeHandle waits for completion.
138
+ # These methods report acceptance. SandboxHandle waits for completion.
139
139
  # Keep the boolean result for integrations using older Docker servers.
140
140
  # Neither is replay-safe, so a transport fault raises instead of resending.
141
141
  def pause(sandbox_id, request_timeout: nil)
@@ -224,10 +224,10 @@ module PineSandbox
224
224
  def to_sandbox!(resp, ok:)
225
225
  raise_for_status!(resp) unless ok.include?(resp.status)
226
226
  payload = parse_json(resp)
227
- Runtime.new(id: payload["id"], image: payload["image"], metadata: payload["metadata"] || {},
228
- entrypoint: payload["entrypoint"], status: runtime_state(payload.dig("status", "state")),
229
- expires_at: runtime_time(payload["expiresAt"]), created_at: runtime_time(payload["createdAt"]),
230
- lifecycle: RuntimeLifetime.from_payload(payload["lifecycle"]),
227
+ SandboxInfo.new(id: payload["id"], image: payload["image"], metadata: payload["metadata"] || {},
228
+ entrypoint: payload["entrypoint"], status: sandbox_state(payload.dig("status", "state")),
229
+ expires_at: parse_time(payload["expiresAt"]), created_at: parse_time(payload["createdAt"]),
230
+ lifecycle: SandboxLifetime.from_payload(payload["lifecycle"]),
231
231
  runtime_revision: payload["runtime_revision"],
232
232
  operation_id: payload.dig("status", "operation_id"),
233
233
  retryable: payload.dig("status", "retryable"),
@@ -235,11 +235,11 @@ module PineSandbox
235
235
  end
236
236
 
237
237
  # "Running" → :running, "PendingResume" → :pending_resume.
238
- def runtime_state(state)
238
+ def sandbox_state(state)
239
239
  state&.to_s&.gsub(/([a-z\d])([A-Z])/, '\1_\2')&.downcase&.to_sym
240
240
  end
241
241
 
242
- def runtime_time(value) = value.nil? || value.is_a?(Time) ? value : Time.iso8601(value)
242
+ def parse_time(value) = value.nil? || value.is_a?(Time) ? value : Time.iso8601(value)
243
243
 
244
244
  def raise_for_status!(resp)
245
245
  body = parse_json(resp)
@@ -9,6 +9,7 @@ module PineSandbox
9
9
  "/errors/sandbox-not-ready" => true,
10
10
  "/errors/unauthorized" => false,
11
11
  "/errors/forbidden" => false,
12
+ "/errors/feature-disabled" => false,
12
13
  "/errors/precondition-required" => false,
13
14
  "/errors/precondition-failed" => true,
14
15
  "/errors/malformed-json" => false,
@@ -37,6 +38,8 @@ module PineSandbox
37
38
  "/errors/no-desktop-stream" => false,
38
39
  "/errors/desktop-unavailable" => false,
39
40
  "/errors/skills-unavailable" => false,
41
+ "/errors/artifacts-unavailable" => true,
42
+ "/errors/artifacts-not-configured" => false,
40
43
  "/errors/secret-in-skill" => false,
41
44
  "/errors/skill-author-failed" => true,
42
45
  "/errors/author-in-flight" => true,
@@ -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
@@ -194,10 +191,23 @@ module PineSandbox
194
191
  class HandoffExpired < GoneError; end
195
192
  class HandoffPersistenceFailed < ServerError; end
196
193
 
194
+ # A Computer's artifacts have no file at #path (the path you passed,
195
+ # relative to the Computer's artifacts). A NotFoundError, so tier rescues
196
+ # keep catching it.
197
+ class ArtifactNotFoundError < NotFoundError
198
+ attr_reader :path
199
+
200
+ def initialize(path:, **fields)
201
+ @path = path
202
+ super(**fields)
203
+ end
204
+ end
205
+
197
206
  # Problem `type` → its typed error; anything else falls back to the status tier.
198
207
  PROBLEM_TYPE_CLASSES = {
199
208
  "/errors/control-not-held" => ControlNotHeld,
200
209
  "/errors/lease-expired" => LeaseExpired,
210
+ "/errors/feature-disabled" => FeatureDisabled,
201
211
  "/errors/lease-refresh-unavailable" => LeaseRefreshUnavailable,
202
212
  "/errors/another-session-human" => AnotherSessionHuman,
203
213
  "/errors/concurrent-transition" => ConcurrentTransition,
@@ -230,20 +240,32 @@ module PineSandbox
230
240
  class ConnectionError < Error; end
231
241
  class ReadyTimeoutError < Error; end
232
242
 
233
- # The runtime reported failed, terminated or unavailable before the
243
+ # The Computer reported failed, terminated or unavailable before the
234
244
  # lifecycle operation finished.
235
- class RuntimeFailedError < Error
236
- attr_reader :runtime_id, :state
245
+ class SandboxFailedError < Error
246
+ attr_reader :sandbox_id, :state
237
247
 
238
- def initialize(message = nil, runtime_id: nil, state: nil)
239
- @runtime_id = runtime_id
248
+ def initialize(message = nil, sandbox_id: nil, state: nil)
249
+ @sandbox_id = sandbox_id
240
250
  @state = state
241
- super(message || "Computer runtime #{runtime_id} reported #{state.inspect} before completing the lifecycle operation")
251
+ super(message || "Computer sandbox #{sandbox_id} reported #{state.inspect} before completing the lifecycle operation")
252
+ end
253
+ end
254
+
255
+ # The Computer's artifacts take up more than its storage limit, so an
256
+ # upload or a copy into them is refused before anything is sent. Delete
257
+ # files to make room. #used_bytes and #limit_bytes are as Pine counted
258
+ # them when it issued the storage token.
259
+ class ArtifactStorageFullError < Error
260
+ attr_reader :used_bytes, :limit_bytes
261
+
262
+ def initialize(used_bytes:, limit_bytes:)
263
+ @used_bytes = used_bytes
264
+ @limit_bytes = limit_bytes
265
+ super("the Computer's artifacts use #{used_bytes} bytes, more than their limit of #{limit_bytes}; " \
266
+ "delete files before writing more")
242
267
  end
243
268
  end
244
- # Deprecated: use RuntimeFailedError.
245
- SandboxFailedError = RuntimeFailedError
246
- deprecate_constant :SandboxFailedError
247
269
 
248
270
  class StreamError < Error; end
249
271
  class StreamingUnsupportedError < Error; end
@@ -295,14 +317,14 @@ module PineSandbox
295
317
  end
296
318
  end
297
319
 
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
320
+ # Start found the Computer still running or paused, and started nothing.
321
+ # Preserve the BindError superclass for integrations already rescuing Start conflicts.
322
+ class SandboxRetainedError < BindError
323
+ attr_reader :sandbox_id, :state
302
324
 
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")
325
+ def initialize(sandbox_id:, state:)
326
+ @sandbox_id, @state = sandbox_id, state
327
+ super("Computer hasn't stopped (Computer sandbox #{sandbox_id}, state #{state}); Stop it or Resume retained memory before Start")
306
328
  end
307
329
  end
308
330
 
@@ -380,46 +402,34 @@ module PineSandbox
380
402
  class ProjectAccessDenied < AttachCredentialsError; end
381
403
 
382
404
  # 412 — another attach advanced the authorization revision. Terminal:
383
- # reload/adopt the winning integrator database binding; do not allocate a runtime.
405
+ # reload/adopt the winning integrator database binding; do not start the Computer again.
384
406
  class BindingRevisionConflictError < AttachCredentialsError
385
- attr_reader :current_binding_revision, :current_runtime_id
407
+ attr_reader :current_binding_revision, :current_sandbox_id
386
408
 
387
- def initialize(message, current_binding_revision: nil, current_runtime_id: nil, current_sandbox_id: nil, **kwargs)
409
+ def initialize(message, current_binding_revision: nil, current_sandbox_id: nil, **kwargs)
388
410
  @current_binding_revision = current_binding_revision
389
- @current_runtime_id = current_runtime_id || current_sandbox_id
411
+ @current_sandbox_id = current_sandbox_id
390
412
  super(message, **kwargs)
391
413
  end
392
414
 
393
415
  # The winning binding's revision (also #current_binding_revision).
394
416
  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
417
  end
402
418
 
403
419
  # Portal committed a new authorization revision, but the later bind
404
- # failed. The SDK has cleaned up the fresh runtime best-effort. Persist the
420
+ # failed. The SDK has shut the Computer down best-effort. Persist the
405
421
  # revision before retrying so the next attach can supersede this attempt.
406
422
  # #credentials is present for create_computer, whose generated identity and
407
423
  # capture keypair would otherwise be lost because the method has no result.
408
424
  class AttachAuthorizationCommittedError < BindError
409
- attr_reader :binding_revision, :runtime_id, :credentials
425
+ attr_reader :binding_revision, :sandbox_id, :credentials
410
426
 
411
- def initialize(message, binding_revision:, runtime_id: nil, sandbox_id: nil, credentials: nil, **kwargs)
427
+ def initialize(message, binding_revision:, sandbox_id: nil, credentials: nil, **kwargs)
412
428
  @binding_revision = binding_revision
413
- @runtime_id = runtime_id || sandbox_id
429
+ @sandbox_id = sandbox_id
414
430
  @credentials = credentials
415
431
  super(message, **kwargs)
416
432
  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
433
  end
424
434
 
425
435
  # The Computer answered with an `X-Computer-Spec-Version` major this SDK
@@ -435,44 +445,44 @@ module PineSandbox
435
445
  end
436
446
 
437
447
  # The Computer rejected the portal-issued bind_token: bad signature, unknown
438
- # kid, iss/aud mismatch, expired, replayed jti, or issued for another runtime.
448
+ # kid, iss/aud mismatch, expired, replayed jti, or issued for another start.
439
449
  # Terminal — retrying with the same token cannot succeed.
440
450
  class BindAuthError < BindError; end
441
451
 
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).
452
+ # This Computer is already attached elsewhere: another attach holds its epoch
453
+ # lease, or you are attaching from two places at once.
444
454
  class ComputerAlreadyAttachedError < BindError; end
445
455
 
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
456
+ # This Computer sandbox can't be attached (it was already attached, or a prior attach
457
+ # failed on it). The SDK shuts it down before raising; retry attach to start
458
+ # the Computer again.
459
+ class SandboxUnusableError < BindError; end
460
+ # Deprecated: use SandboxUnusableError.
461
+ PodPoisonedError = SandboxUnusableError
452
462
  deprecate_constant :PodPoisonedError
453
463
 
454
- # Saved state was unreachable during the bind; the runtime has been
455
- # cleaned up. Retry attach later (transient).
464
+ # Saved state was unreachable during the bind; the SDK has shut the Computer
465
+ # down. Retry attach later (transient).
456
466
  class SavedStateUnavailableError < BindError; end
457
467
  # Deprecated: use SavedStateUnavailableError.
458
468
  BrokerUnreachableError = SavedStateUnavailableError
459
469
  deprecate_constant :BrokerUnreachableError
460
470
 
461
471
  # The bind handshake exhausted its retries without a terminal
462
- # classification. Usually a runtime-allocation or network issue.
472
+ # classification. Usually a slow start or a network issue.
463
473
  class BindTimeoutError < BindError; end
464
474
 
465
475
  # The Computer returned 401 for its Computer token (ct_) — a REPORT that
466
476
  # 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).
477
+ # re-attach. ct_ is binding-lifetime (no idle expiry), so on a running
478
+ # Computer this state is binding_auth_lost: surface it and reconcile; only
479
+ # a Computer confirmed stopped justifies an attach (a new start invalidates
480
+ # every ps_ and fences the previous start's state).
471
481
  # A 401 for a session token (ps_) is an ordinary UnauthorizedError.
472
482
  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
483
+ MESSAGE = "the Computer rejected the computer token (401). ct_ is binding-lifetime, so on a " \
484
+ "running Computer this is binding_auth_lost — reconcile; re-attach ONLY once the " \
485
+ "Computer is confirmed stopped (a new start invalidates every ps_)".freeze
476
486
 
477
487
  # The rejection of `api_error` (a 401 answer to a Computer token), with
478
488
  # its operation and request id folded into the message.
@@ -482,14 +492,14 @@ module PineSandbox
482
492
  end
483
493
  end
484
494
 
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
495
+ # The gateway can no longer route to this Computer's host, which is also
496
+ # true while it's paused or recovering. This is distinct from an /errors/session-not-found response on a live
487
497
  # Computer. Inherit the legacy 404 tier so existing NotFoundError rescues
488
498
  # remain valid; the original typed ApiError is retained as Exception#cause.
489
- class RuntimeGoneError < NotFoundError
499
+ class SandboxGoneError < NotFoundError
490
500
  def initialize(api_error)
491
501
  detail = api_error.error_message.to_s
492
- message = "the attached runtime is gone"
502
+ message = "the Computer can't be reached at its host"
493
503
  message = "#{message}: #{detail}" unless detail.empty?
494
504
  super(
495
505
  status: api_error.status,
@@ -502,9 +512,6 @@ module PineSandbox
502
512
  self.retry_after = api_error.retry_after
503
513
  end
504
514
  end
505
- # Deprecated: use RuntimeGoneError.
506
- SandboxGoneError = RuntimeGoneError
507
- deprecate_constant :SandboxGoneError
508
515
 
509
516
  # 429 COMPUTER_CAPACITY_EXCEEDED on create/attach: the project is at its
510
517
  # 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(
@@ -0,0 +1,40 @@
1
+ require "time"
2
+
3
+ module PineSandbox
4
+ module Models
5
+ # One file in a Computer's artifacts. Returned by
6
+ # {PineSandbox::Artifacts#list}, #upload and #copy. `path` is relative to
7
+ # the Computer's artifacts ("research/q3.csv"); `note` is the note the file
8
+ # was uploaded with, if any.
9
+ ArtifactFile = Struct.new(
10
+ :path,
11
+ :size,
12
+ :updated_at,
13
+ :content_type,
14
+ :note,
15
+ keyword_init: true
16
+ ) do
17
+ # From a storage object, whose name is `prefix` followed by the path.
18
+ def self.from_object(object, prefix:)
19
+ new(
20
+ path: object["name"].to_s.delete_prefix(prefix),
21
+ # Storage sends the size as a decimal string.
22
+ size: object["size"].nil? ? nil : Integer(object["size"].to_s, 10),
23
+ updated_at: object["updated"] && Time.parse(object["updated"]),
24
+ content_type: object["contentType"],
25
+ note: object.dig("metadata", "pine-note")
26
+ )
27
+ end
28
+ end
29
+
30
+ # One page of {PineSandbox::Artifacts#list}: the files, the folders
31
+ # directly below the listed prefix (each ending in "/"; none when listing
32
+ # recursively), and the token for the next page (nil on the last one).
33
+ ArtifactPage = Struct.new(
34
+ :files,
35
+ :folders,
36
+ :next_page_token,
37
+ keyword_init: true
38
+ )
39
+ end
40
+ end
@@ -12,12 +12,15 @@ module PineSandbox
12
12
  :token,
13
13
  :session_dir,
14
14
  :browser,
15
- :blind,
15
+ :artifact_access,
16
16
  :created_at,
17
17
  :last_active_at,
18
18
  keyword_init: true
19
19
  ) do
20
- def self.from_payload(payload)
20
+ # `artifact_access_known` is false for a session whose artifact access
21
+ # only the Computer knows (Computer#adopt_session): #artifact_access is
22
+ # then nil.
23
+ def self.from_payload(payload, artifact_access_known: true)
21
24
  return nil unless payload
22
25
  browser_payload = payload["browser"]
23
26
  new(
@@ -26,7 +29,8 @@ module PineSandbox
26
29
  token: payload["token"],
27
30
  session_dir: payload["session_dir"],
28
31
  browser: browser_payload && Browser.from_payload(browser_payload),
29
- blind: payload.fetch("blind", false),
32
+ # An older Computer doesn't report it; its sessions have no access.
33
+ artifact_access: artifact_access_known ? payload.fetch("artifact_access", "none") : nil,
30
34
  created_at: payload["created_at"] && Time.parse(payload["created_at"]),
31
35
  last_active_at: payload["last_active_at"] && Time.parse(payload["last_active_at"])
32
36
  )
@@ -34,9 +34,10 @@ 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],
38
+ ["createArtifactLink", "POST", "/v1/artifacts/links", :computer, false, false],
39
39
  ["createComputer", "POST", "/computer-sandboxes", :control, true, false],
40
+ ["createComputerArtifactToken", "POST", "/v1/computers/{computer_id}/artifacts/token", :control, false, false],
40
41
  ["createDiagnostic", "POST", "/v1/diagnostics", :control, true, true],
41
42
  ["createSession", "POST", "/sessions", :computer, false, false],
42
43
  ["deactivateSkill", "POST", "/v1/skills/{name}/deactivate", :computer, false, false],
@@ -49,7 +50,6 @@ module PineSandbox
49
50
  ["deleteSession", "DELETE", "/sessions/{name}", :computer, true, false],
50
51
  ["deleteSkillVersion", "DELETE", "/v1/skills/{name}/versions/{version}", :computer, true, false],
51
52
  ["destroyComputer", "DELETE", "/sandboxes/{sandboxId}", :control, true, false],
52
- ["discardDownloadOrphan", "DELETE", "/downloads/orphans/{guid}", :computer, true, false],
53
53
  ["getAgentEventsPageV2", "GET", "/v2/sessions/{name}/agent/events/page", :computer, true, false],
54
54
  ["getAgentEventsV2", "GET", "/v2/sessions/{name}/agent/events", :computer, true, false],
55
55
  ["getAgentTurnV2", "GET", "/v2/sessions/{name}/agent/turns/{turn_id}", :computer, true, false],
@@ -74,7 +74,6 @@ module PineSandbox
74
74
  ["listAllSkillVersions", "GET", "/v1/skills/versions", :computer, true, false],
75
75
  ["listArtifacts", "GET", "/v1/sessions/{name}/artifacts", :computer, true, false],
76
76
  ["listAvailableLocations", "GET", "/v1/locations", :control, true, false],
77
- ["listDownloadOrphans", "GET", "/downloads/orphans", :computer, true, false],
78
77
  ["listHandoffs", "GET", "/v1/sessions/{name}/handoffs", :computer, true, false],
79
78
  ["listOrphans", "GET", "/orphans", :computer, true, false],
80
79
  ["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,12 @@ 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
- 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")
106
+ # Accepts CAPTURE_TIMEOUT as well as CaptureTimeout to unify on SNAKE_CASE while staying backward compatible.
107
+ unless %i[save_failed unavailable failed terminated].include?(info.status) && !%w[CAPTURE_TIMEOUT CaptureTimeout].include?(info.reason)
108
+ raise PineSandbox::ConflictError.new(status: 409, error_message: "The Computer has no recorded failure; use Stop or Resume")
109
109
  end
110
110
  raise PineSandbox::Error, "Server did not supply a conditional release version" if info.etag.to_s.empty?
111
111
  @control_plane.destroy(@id, if_match: info.etag, request_timeout: remaining(deadline))
@@ -229,8 +229,8 @@ module PineSandbox
229
229
  end
230
230
  return self if state == target
231
231
  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
232
+ raise PineSandbox::SandboxFailedError.new(
233
+ "Computer sandbox #{@id} reported #{state.inspect} before reaching #{target}", sandbox_id: @id, state: state
234
234
  )
235
235
  end
236
236
  end
@@ -245,8 +245,4 @@ module PineSandbox
245
245
  end
246
246
 
247
247
  end
248
-
249
- # Deprecated: use RuntimeHandle.
250
- SandboxHandle = RuntimeHandle
251
- deprecate_constant :SandboxHandle
252
248
  end