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/client.rb
CHANGED
|
@@ -176,7 +176,7 @@ module PineSandbox
|
|
|
176
176
|
provider.available_locations
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
-
# Optional configuration discovery, not a
|
|
179
|
+
# Optional configuration discovery, not a lifecycle operation prerequisite.
|
|
180
180
|
def project_policy
|
|
181
181
|
provider = @connection.attach_provider
|
|
182
182
|
raise ArgumentError, "project policy requires a project API key provider" unless provider.respond_to?(:project_policy)
|
|
@@ -253,8 +253,8 @@ module PineSandbox
|
|
|
253
253
|
location ? provider.register_computer(computer_id, location: location) : provider.register_computer(computer_id)
|
|
254
254
|
end
|
|
255
255
|
|
|
256
|
-
# Re-attach an EXISTING persistent Computer by its stored id + state key.
|
|
257
|
-
#
|
|
256
|
+
# Re-attach an EXISTING persistent Computer by its stored id + state key. The
|
|
257
|
+
# Computer starts and is bound with its prior state restored.
|
|
258
258
|
# The id may be given positionally or as `id:` (matching #create_computer).
|
|
259
259
|
def start_computer(*args, **options)
|
|
260
260
|
if args.first.is_a?(Computer::Credentials)
|
|
@@ -264,11 +264,11 @@ module PineSandbox
|
|
|
264
264
|
credentials, *args = args
|
|
265
265
|
options = credentials_options(credentials, options)
|
|
266
266
|
end
|
|
267
|
-
start_existing_computer(*args, **options,
|
|
267
|
+
start_existing_computer(*args, **options, recover: false)
|
|
268
268
|
end
|
|
269
269
|
|
|
270
270
|
# Gracefully stop the binding observed for this ID, even when a caller lost
|
|
271
|
-
# its Start response before saving the
|
|
271
|
+
# its Start response before saving the Computer token. Never follow a newer
|
|
272
272
|
# binding during this invocation; project authorization is sufficient.
|
|
273
273
|
def stop_computer(computer_id, **wait_options)
|
|
274
274
|
raise ArgumentError, "id must be a lowercase UUIDv7" unless computer_id.to_s.match?(Computer::UUID_V7)
|
|
@@ -277,27 +277,27 @@ module PineSandbox
|
|
|
277
277
|
raise ArgumentError, "Stop by ID requires a provider with current_binding"
|
|
278
278
|
end
|
|
279
279
|
reference = provider.current_binding(computer_id)
|
|
280
|
-
|
|
281
|
-
return true if
|
|
280
|
+
sandbox_id = reference["sandbox_id"]
|
|
281
|
+
return true if sandbox_id.nil?
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
SandboxHandle.new(control_plane: @connection.control_plane, sandbox_id: sandbox_id).stop(**wait_options)
|
|
284
284
|
rescue UnknownComputerError
|
|
285
285
|
true
|
|
286
286
|
end
|
|
287
287
|
|
|
288
|
-
# Explicitly
|
|
289
|
-
# committed state. This loses unsaved memory; it never replaces a healthy
|
|
288
|
+
# Explicitly shut down a Computer that failed, wait for the shutdown, then
|
|
289
|
+
# Start from committed state. This loses unsaved memory; it never replaces a healthy
|
|
290
290
|
# paused or running Computer and never silently follows failed Resume.
|
|
291
291
|
def recover_computer(*args, **options)
|
|
292
292
|
if args.first.is_a?(Computer::Credentials)
|
|
293
293
|
credentials, *args = args
|
|
294
294
|
options = credentials_options(credentials, options)
|
|
295
295
|
end
|
|
296
|
-
start_existing_computer(*args, **options,
|
|
296
|
+
start_existing_computer(*args, **options, recover: true)
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def start_existing_computer(positional_id = nil, id: nil, key:, capture_keypair: nil, prior_capture_keypairs: [],
|
|
300
|
-
binding_revision: 0, prior_keys: {}, ephemeral: false, location: nil,
|
|
300
|
+
binding_revision: 0, prior_keys: {}, ephemeral: false, location: nil, recover: false, **attach_opts)
|
|
301
301
|
if positional_id && id && positional_id != id
|
|
302
302
|
raise ArgumentError, "attach_computer got two different ids (positional and id:)"
|
|
303
303
|
end
|
|
@@ -305,22 +305,22 @@ module PineSandbox
|
|
|
305
305
|
raise ArgumentError, "attach_computer requires the Computer id" if id.nil?
|
|
306
306
|
|
|
307
307
|
provider = @connection.attach_provider
|
|
308
|
-
if
|
|
308
|
+
if recover && !provider.respond_to?(:current_binding)
|
|
309
309
|
raise ArgumentError, "saved-state recovery requires a provider with current_binding"
|
|
310
310
|
end
|
|
311
311
|
if provider.respond_to?(:current_binding)
|
|
312
312
|
reference = provider.current_binding(id)
|
|
313
313
|
binding_revision = reference.fetch("binding_revision")
|
|
314
|
-
if (
|
|
314
|
+
if (sandbox_id = reference["sandbox_id"])
|
|
315
315
|
begin
|
|
316
|
-
|
|
317
|
-
unless
|
|
318
|
-
unless
|
|
319
|
-
raise
|
|
316
|
+
sandbox = @connection.control_plane.get(sandbox_id)
|
|
317
|
+
unless sandbox.status == :terminated && sandbox.reason == "COMPUTE_RELEASED"
|
|
318
|
+
unless recover || sandbox.status == :stopping
|
|
319
|
+
raise SandboxRetainedError.new(sandbox_id: sandbox_id, state: sandbox.status)
|
|
320
320
|
end
|
|
321
|
-
handle =
|
|
322
|
-
unless handle.
|
|
323
|
-
raise TimeoutError, "
|
|
321
|
+
handle = SandboxHandle.new(control_plane: @connection.control_plane, sandbox_id: sandbox_id)
|
|
322
|
+
unless handle.release_failed_sandbox(sandbox)
|
|
323
|
+
raise TimeoutError, "The failed Computer's shutdown is unconfirmed; nothing was started"
|
|
324
324
|
end
|
|
325
325
|
end
|
|
326
326
|
rescue NotFoundError
|
|
@@ -343,14 +343,14 @@ module PineSandbox
|
|
|
343
343
|
alias_method :attach_computer, :start_computer
|
|
344
344
|
|
|
345
345
|
# Adopt an ALREADY-bound, still-live Computer from its persisted `{id, key,
|
|
346
|
-
#
|
|
346
|
+
# sandbox_id, ct_}` — NO new start, NO re-bind. For driving a running Computer
|
|
347
347
|
# across requests (e.g. re-minting a desktop `dt_` for a reconnect) from a
|
|
348
|
-
# backend that cached the `ct_`. Use #start_computer to
|
|
349
|
-
#
|
|
350
|
-
def adopt_existing(id:, key:, computer_token:,
|
|
348
|
+
# backend that cached the `ct_`. Use #start_computer to start it again with state
|
|
349
|
+
# restored.
|
|
350
|
+
def adopt_existing(id:, key:, sandbox_id:, computer_token:, status: :running,
|
|
351
351
|
binding_revision: 0, ephemeral: false)
|
|
352
352
|
Computer.new(id, key: key, binding_revision: binding_revision, ephemeral: ephemeral)
|
|
353
|
-
.adopt(@connection,
|
|
353
|
+
.adopt(@connection, sandbox_id: sandbox_id, computer_token: computer_token,
|
|
354
354
|
status: status, binding_revision: binding_revision)
|
|
355
355
|
end
|
|
356
356
|
|
|
@@ -366,7 +366,7 @@ module PineSandbox
|
|
|
366
366
|
raise ArgumentError, "credentials and binding belong to different Computers"
|
|
367
367
|
end
|
|
368
368
|
computer = adopt_existing(id: credentials.id.dup, key: credentials.key.dup,
|
|
369
|
-
|
|
369
|
+
sandbox_id: binding.sandbox_id.dup, computer_token: binding.computer_token.dup,
|
|
370
370
|
binding_revision: binding.binding_revision, ephemeral: credentials.ephemeral)
|
|
371
371
|
configure_capture_keypairs(computer, credentials.capture_keypair, credentials.prior_capture_keypairs)
|
|
372
372
|
credentials.prior_keys.each { |version, key| computer.add_prior_key(version, key.dup) }
|
|
@@ -374,18 +374,41 @@ module PineSandbox
|
|
|
374
374
|
end
|
|
375
375
|
|
|
376
376
|
# Block form: yields a ready Computer and stops it on exit.
|
|
377
|
-
# Failed saves and unconfirmed deletion are raised to the caller.
|
|
377
|
+
# Failed saves and unconfirmed deletion are raised to the caller. When the
|
|
378
|
+
# block raises, its error propagates and a failed Stop is only warned about.
|
|
378
379
|
def with_computer(**opts)
|
|
379
380
|
computer = create_computer(**opts)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
381
|
+
failure = nil
|
|
382
|
+
begin
|
|
383
|
+
yield computer
|
|
384
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised
|
|
385
|
+
failure = e
|
|
386
|
+
raise
|
|
387
|
+
ensure
|
|
388
|
+
# Also runs when the block leaves early with return, break or throw.
|
|
389
|
+
if failure
|
|
390
|
+
stop_after_failure(computer, failure)
|
|
391
|
+
elsif !computer.stop
|
|
392
|
+
raise TimeoutError, "Computer #{computer.id} stop is unconfirmed; retry Stop with its saved binding"
|
|
393
|
+
end
|
|
384
394
|
end
|
|
385
395
|
end
|
|
386
396
|
|
|
387
397
|
private
|
|
388
398
|
|
|
399
|
+
# Stop after with_computer's block failed: the block's error is the one the
|
|
400
|
+
# caller gets, so a Stop that fails or stays unconfirmed is only reported.
|
|
401
|
+
# The failure is recorded where the block raised, never read from $!, which
|
|
402
|
+
# inside a caller's own rescue clause is the exception that clause handles.
|
|
403
|
+
def stop_after_failure(computer, failure)
|
|
404
|
+
return if computer.stop
|
|
405
|
+
|
|
406
|
+
warn "PineSandbox: Computer #{computer.id} stop is unconfirmed; retry Stop with its saved binding " \
|
|
407
|
+
"(while #{failure.class} propagates)"
|
|
408
|
+
rescue StandardError => e
|
|
409
|
+
warn "PineSandbox: Computer #{computer.id} stop failed (#{e.class}: #{e.message}) while #{failure.class} propagates"
|
|
410
|
+
end
|
|
411
|
+
|
|
389
412
|
def credentials_options(credentials, options)
|
|
390
413
|
unless credentials.is_a?(Computer::Credentials)
|
|
391
414
|
raise ArgumentError, "credentials must be a PineSandbox::Computer::Credentials"
|
|
@@ -401,13 +424,13 @@ module PineSandbox
|
|
|
401
424
|
computer.attach(@connection, **attach_opts)
|
|
402
425
|
rescue StandardError => e
|
|
403
426
|
revision = computer.binding_revision
|
|
404
|
-
|
|
405
|
-
raise if revision <= initial_revision ||
|
|
427
|
+
sandbox_id = computer.last_authorized_sandbox_id
|
|
428
|
+
raise if revision <= initial_revision || sandbox_id.to_s.empty?
|
|
406
429
|
|
|
407
430
|
raise AttachAuthorizationCommittedError.new(
|
|
408
|
-
"attach authorization committed at revision #{revision} for
|
|
431
|
+
"attach authorization committed at revision #{revision} for Computer sandbox #{sandbox_id}, " \
|
|
409
432
|
"but the bind failed: #{e.message}",
|
|
410
|
-
binding_revision: revision,
|
|
433
|
+
binding_revision: revision, sandbox_id: sandbox_id,
|
|
411
434
|
credentials: recovery_credentials,
|
|
412
435
|
status: (e.respond_to?(:status) ? e.status : nil), cause: e
|
|
413
436
|
)
|