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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +159 -157
- data/lib/pinesandbox/adapters/coordinator_adapter.rb +99 -74
- data/lib/pinesandbox/agent_events.rb +2 -2
- data/lib/pinesandbox/agent_turn.rb +24 -2
- data/lib/pinesandbox/attach_credentials_source.rb +13 -13
- data/lib/pinesandbox/bind_hpke.rb +4 -4
- data/lib/pinesandbox/client.rb +59 -36
- data/lib/pinesandbox/computer.rb +108 -108
- data/lib/pinesandbox/computer_binding.rb +14 -10
- data/lib/pinesandbox/control_plane.rb +10 -10
- data/lib/pinesandbox/error_taxonomy.rb +1 -0
- data/lib/pinesandbox/errors.rb +56 -76
- data/lib/pinesandbox/models/artifact.rb +1 -1
- data/lib/pinesandbox/operations.rb +0 -3
- data/lib/pinesandbox/{runtime_details.rb → sandbox_details.rb} +4 -4
- data/lib/pinesandbox/{runtime_handle.rb → sandbox_handle.rb} +14 -19
- data/lib/pinesandbox/session.rb +22 -2
- data/lib/pinesandbox/spec_version_middleware.rb +1 -1
- data/lib/pinesandbox/version.rb +1 -1
- data/lib/pinesandbox/zone.rb +8 -11
- data/lib/pinesandbox.rb +2 -2
- data/pinesandbox.gemspec +5 -3
- metadata +8 -6
data/lib/pinesandbox/errors.rb
CHANGED
|
@@ -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
|
|
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
|
|
77
|
-
def
|
|
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
|
|
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!(
|
|
112
|
-
|
|
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 << "
|
|
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
|
|
231
|
+
# The Computer reported failed, terminated or unavailable before the
|
|
234
232
|
# lifecycle operation finished.
|
|
235
|
-
class
|
|
236
|
-
attr_reader :
|
|
233
|
+
class SandboxFailedError < Error
|
|
234
|
+
attr_reader :sandbox_id, :state
|
|
237
235
|
|
|
238
|
-
def initialize(message = nil,
|
|
239
|
-
@
|
|
236
|
+
def initialize(message = nil, sandbox_id: nil, state: nil)
|
|
237
|
+
@sandbox_id = sandbox_id
|
|
240
238
|
@state = state
|
|
241
|
-
super(message || "Computer
|
|
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
|
|
299
|
-
# BindError superclass for integrations already rescuing Start conflicts.
|
|
300
|
-
class
|
|
301
|
-
attr_reader :
|
|
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(
|
|
304
|
-
@
|
|
305
|
-
super("Computer
|
|
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
|
|
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, :
|
|
380
|
+
attr_reader :current_binding_revision, :current_sandbox_id
|
|
386
381
|
|
|
387
|
-
def initialize(message, current_binding_revision: nil,
|
|
382
|
+
def initialize(message, current_binding_revision: nil, current_sandbox_id: nil, **kwargs)
|
|
388
383
|
@current_binding_revision = current_binding_revision
|
|
389
|
-
@
|
|
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
|
|
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, :
|
|
398
|
+
attr_reader :binding_revision, :sandbox_id, :credentials
|
|
410
399
|
|
|
411
|
-
def initialize(message, binding_revision:,
|
|
400
|
+
def initialize(message, binding_revision:, sandbox_id: nil, credentials: nil, **kwargs)
|
|
412
401
|
@binding_revision = binding_revision
|
|
413
|
-
@
|
|
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
|
|
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
|
-
#
|
|
443
|
-
#
|
|
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
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
class
|
|
450
|
-
# Deprecated: use
|
|
451
|
-
PodPoisonedError =
|
|
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
|
|
455
|
-
#
|
|
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
|
|
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
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
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
|
|
474
|
-
"
|
|
475
|
-
"
|
|
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
|
|
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
|
|
472
|
+
class SandboxGoneError < NotFoundError
|
|
490
473
|
def initialize(api_error)
|
|
491
474
|
detail = api_error.error_message.to_s
|
|
492
|
-
message = "the
|
|
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
|
|
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
|
|
3
|
-
|
|
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
|
-
|
|
17
|
+
SandboxDetails = Data.define(:id, :state, :reason, :message, :retryable,
|
|
18
18
|
:operation_id, :created_at, :expires_at, :lifecycle, :runtime_revision) do
|
|
19
|
-
def self.
|
|
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
|
|
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
|
|
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 :
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
57
|
-
raise BindError, "
|
|
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
|
|
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
|
|
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: "
|
|
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::
|
|
233
|
-
"Computer
|
|
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
|
data/lib/pinesandbox/session.rb
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
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
|
@@ -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/
|
|
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/
|
|
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://
|
|
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.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/
|
|
155
|
-
- lib/pinesandbox/
|
|
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://
|
|
165
|
+
homepage: https://pinecomputer.io
|
|
165
166
|
licenses:
|
|
166
167
|
- Apache-2.0
|
|
167
168
|
metadata:
|
|
168
|
-
homepage_uri: https://
|
|
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.
|
|
179
|
+
version: '3.3'
|
|
178
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
181
|
requirements:
|
|
180
182
|
- - ">="
|