withruntime 0.2.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e403c64b0fd311fd7ba3fb2a9fab6304afda36100ebc1fe0bd30f6b8f82bcece
4
- data.tar.gz: a6eb390df90ab8f676704e8181c535d2dd23a41eebec44efdbf0219f46f675c6
3
+ metadata.gz: f471d00ffe082a8a507a514dda86ce4062342e2b5f0e27153309d094a27fb35b
4
+ data.tar.gz: 988ff09e0b418238acb50eba94f81bfa59c579e8103a81bd8b43e551c692f70e
5
5
  SHA512:
6
- metadata.gz: a9994df04fad6ef2af83e9c1bb92e037b42e048bdf2806465d5969e43f7bc4ba9862fcbc77036d61b7d826579f9e0c316e302b05822cf6c326e6739a657ef677
7
- data.tar.gz: de3343c87c44ea8dbed899137c0034de4683f64cb2ff44029ffcfbd6da98652f68d4fae8b53fbc90d3dc00a4ba5305d0239190eb756d8dff95ee54f0c0584043
6
+ metadata.gz: e4e8625a16d34f127de6bda5733b2645b733cb7a06ec3af65d87fc48a6b804d17c6d22f03cc657bd3cb64d8c29e3269cc42047aed28b0cf084f25a6cba9a048e
7
+ data.tar.gz: 33311be1f0d80a9ff3fd613acd21a02e0535945271e08af9cdf4b83d1021310479681bb7f07e8acb36663654d130fa29c635dcf796b1489cb9fbeec73d1f9a25
@@ -33,7 +33,7 @@ module WithRuntime
33
33
  # +timeout+: seconds for each call, retries included (300).
34
34
  # +max_retries+: of transport failures, 429, 502, 503 and 504 (4).
35
35
  # +max_connections+: calls in flight at once; streams do not count (32).
36
- # +wait_for_capacity+: seconds a sandbox create keeps retrying, with the
36
+ # +wait_for_capacity+: seconds a sandbox create, wake or restart keeps retrying, with the
37
37
  # same key and input, when the slots without credit, quota or region are full (120; 0 fails at once).
38
38
  def initialize(api_key: nil, base_url: nil, timeout: 300, max_retries: 4, max_connections: 32,
39
39
  wait_for_capacity: 120, env: ENV, in_sandbox: -> { Credentials.in_runtime_sandbox? })
@@ -104,7 +104,7 @@ module WithRuntime
104
104
  end
105
105
  end
106
106
 
107
- # +runtime.limits+: whether this key is read-only, and what its agent may still spend today.
107
+ # +runtime.limits+: whether this key is read-only, and what its agent may still spend today, plus +account+: the account's limits (limit, label, unit, max, used), or nil.
108
108
  class Limits < Product
109
109
  def get = Record.new(@t.json("GET", "/v1/limits"))
110
110
  end
@@ -19,6 +19,12 @@ module WithRuntime
19
19
  # free, up to +wait_for_capacity+ seconds (the client's, two minutes by default).
20
20
  #
21
21
  # runtime.sandboxes.create(labels: { team: "search" }, timeout_seconds: 900)
22
+ #
23
+ # +spread_from: [id, ...]+ (one to 16 ids of your own sandboxes or snapshots)
24
+ # starts it on a server none of them is on. With no other server that has
25
+ # room, the create raises +no_capacity+ and nothing is made or charged.
26
+ # +sbx.info.placement+ is an opaque tag for its server: the same tag means
27
+ # the same server.
22
28
  def create(wait: true, idempotency_key: nil, wait_for_capacity: nil, **fields)
23
29
  room = wait_for_capacity.nil? ? @t.wait_for_capacity : [0, wait_for_capacity].max
24
30
  info = @t.json("POST", "/v1/sandboxes", body: Fields.body(fields), wait: wait ? 60 : 0,
@@ -118,8 +124,12 @@ module WithRuntime
118
124
 
119
125
  # Carries on a paused sandbox, with its memory and processes;
120
126
  # +timeout_seconds+ is its new time limit, for a sandbox that has one.
121
- def wake(wait: true, timeout_seconds: nil, idempotency_key: nil)
122
- lifecycle("wake", timeout_seconds ? { "timeoutSeconds" => timeout_seconds } : {}, wait, idempotency_key)
127
+ # When its server has no room (+no_capacity+) it waits for room, as +create+
128
+ # does, up to +wait_for_capacity+ seconds (the client's, two minutes by
129
+ # default; 0 fails at once), sending the same idempotency key each time.
130
+ def wake(wait: true, timeout_seconds: nil, idempotency_key: nil, wait_for_capacity: nil)
131
+ lifecycle("wake", timeout_seconds ? { "timeoutSeconds" => timeout_seconds } : {}, wait, idempotency_key,
132
+ room: room_for(wait_for_capacity))
123
133
  end
124
134
 
125
135
  # Moves a sandbox's time limit on, at most an hour ahead of now. A sandbox
@@ -130,7 +140,10 @@ module WithRuntime
130
140
  def set_retention(days, idempotency_key: nil) = lifecycle("retention", { "days" => days }, false, idempotency_key)
131
141
 
132
142
  # Starts a stopped persistent sandbox again from its disk. Memory is not kept.
133
- def restart(wait: true, idempotency_key: nil) = lifecycle("restart", {}, wait, idempotency_key)
143
+ # Waits for room like +wake+ when its server is full.
144
+ def restart(wait: true, idempotency_key: nil, wait_for_capacity: nil)
145
+ lifecycle("restart", {}, wait, idempotency_key, room: room_for(wait_for_capacity))
146
+ end
134
147
 
135
148
  # Changes name, labels, auto_wake, idle_pause_seconds, persistent or
136
149
  # max_total_cost_micros; fields left out stay as they are, and
@@ -203,8 +216,10 @@ module WithRuntime
203
216
  end
204
217
 
205
218
  # Keeps this sandbox's whole machine as a snapshot to start new sandboxes
206
- # from. A running sandbox is paused for the moment it takes, then woken; a
207
- # paused one stays paused. A sandbox with volumes cannot be snapshotted.
219
+ # from. A running sandbox is paused by the server for the moment it takes
220
+ # and woken once the capture is over (one still capturing when the call
221
+ # answers stays paused until it is ready); a paused one stays paused. A
222
+ # sandbox with volumes cannot be snapshotted.
208
223
  def snapshot(name: nil, labels: nil, retention_days: nil, idempotency_key: nil)
209
224
  refresh
210
225
  # Straight after a fork or a wake the sandbox is still resuming, and
@@ -213,15 +228,11 @@ module WithRuntime
213
228
  if %w[resuming starting].include?(state) then wait_for("running")
214
229
  elsif state == "pausing" then wait_for("paused")
215
230
  end
216
- running = state == "running"
217
- pause if running
218
- begin
219
- Record.new(@t.json("POST", path(":snapshot"),
220
- body: Fields.body(name: name, labels: labels, retention_days: retention_days),
221
- wait: 10, key: idempotency_key))
222
- ensure
223
- wake if running
224
- end
231
+ # The server pauses a running sandbox itself: a client pause and wake
232
+ # would be two more round trips.
233
+ Record.new(@t.json("POST", path(":snapshot"),
234
+ body: Fields.body(name: name, labels: labels, retention_days: retention_days),
235
+ wait: 10, key: idempotency_key))
225
236
  end
226
237
 
227
238
  # CPU and memory over a range: 15m, 1h, 6h, 24h, 7d or 30d.
@@ -230,19 +241,20 @@ module WithRuntime
230
241
  # Runs a command and returns its exit code and output. A String runs under
231
242
  # +bash -c+; an Array runs the program directly, with no shell.
232
243
  #
233
- # +cwd+ (default /workspace), +env+ (merged over the sandbox's; put secrets
234
- # here), +stdin+, +timeout+ (seconds, default 60, up to a day; a timeout is
235
- # a result, not an error), +on_stdout+ and +on_stderr+ (stream the command;
236
- # the result keeps everything), and +check+ (raise CommandError on a
237
- # non-zero exit or a timeout).
238
- def exec(command, cwd: nil, env: nil, stdin: nil, timeout: nil, on_stdout: nil, on_stderr: nil,
244
+ # +cwd+ (default /workspace), +user+ ("sandbox", the default, or "root",
245
+ # through the sandbox's passwordless sudo), +env+ (merged over the
246
+ # sandbox's; put secrets here), +stdin+, +timeout+ (seconds, default 60, up
247
+ # to a day; a timeout is a result, not an error), +on_stdout+ and
248
+ # +on_stderr+ (stream the command; the result keeps everything), and
249
+ # +check+ (raise CommandError on a non-zero exit or a timeout).
250
+ def exec(command, cwd: nil, user: nil, env: nil, stdin: nil, timeout: nil, on_stdout: nil, on_stderr: nil,
239
251
  check: false, idempotency_key: nil)
240
252
  streamed = on_stdout || on_stderr || (timeout && timeout > 60)
241
253
  result = if streamed
242
- collect(exec_stream(command, cwd: cwd, env: env, stdin: stdin, timeout: timeout,
254
+ collect(exec_stream(command, cwd: cwd, user: user, env: env, stdin: stdin, timeout: timeout,
243
255
  idempotency_key: idempotency_key), on_stdout, on_stderr)
244
256
  else
245
- body = command_body(command, cwd, env, stdin, timeout)
257
+ body = command_body(command, cwd, env, stdin, timeout, user)
246
258
  CommandResult.new(@t.json("POST", path(":exec"), body: body, key: idempotency_key,
247
259
  timeout: timeout && (timeout + 60)))
248
260
  end
@@ -254,14 +266,14 @@ module WithRuntime
254
266
  # Runs a command and yields its events as they happen: start, stdout,
255
267
  # stderr, exit (each a Hash). Resumes by itself when the server ends a long
256
268
  # stream, so it never drops output. Without a block, an Enumerator.
257
- def exec_stream(command, cwd: nil, env: nil, stdin: nil, timeout: nil, idempotency_key: nil, &block)
269
+ def exec_stream(command, cwd: nil, user: nil, env: nil, stdin: nil, timeout: nil, idempotency_key: nil, &block)
258
270
  unless block
259
- return enum_for(:exec_stream, command, cwd: cwd, env: env, stdin: stdin, timeout: timeout,
271
+ return enum_for(:exec_stream, command, cwd: cwd, user: user, env: env, stdin: stdin, timeout: timeout,
260
272
  idempotency_key: idempotency_key)
261
273
  end
262
274
 
263
275
  longest = ApiDefaults::STREAMED_EXEC_TIMEOUT_SECONDS
264
- body = command_body(command, cwd, env, stdin, timeout || longest).merge("stream" => true)
276
+ body = command_body(command, cwd, env, stdin, timeout || longest, user).merge("stream" => true)
265
277
  handed_over = nil
266
278
  process_id = nil
267
279
  cursor = 0
@@ -350,8 +362,8 @@ module WithRuntime
350
362
  # once. It outlives your connection; +process(id)+ gets it back.
351
363
  # +pipe_stdin: true+ keeps input open for +write+; +pty: { cols:, rows: }+
352
364
  # gives it a terminal.
353
- def spawn(command, cwd: nil, env: nil, timeout: nil, stdin: nil, pipe_stdin: false, pty: nil, idempotency_key: nil)
354
- body = command_body(command, cwd, env, pipe_stdin ? nil : stdin, timeout)
365
+ def spawn(command, cwd: nil, user: nil, env: nil, timeout: nil, stdin: nil, pipe_stdin: false, pty: nil, idempotency_key: nil)
366
+ body = command_body(command, cwd, env, pipe_stdin ? nil : stdin, timeout, user)
355
367
  body["stdinMode"] = "pipe" if pipe_stdin
356
368
  body["pty"] = Fields.body(pty) if pty
357
369
  SandboxProcess.new(self, @t.json("POST", path("/processes"), body: body, key: idempotency_key))
@@ -367,14 +379,17 @@ module WithRuntime
367
379
 
368
380
  private
369
381
 
370
- def lifecycle(verb, body, wait, key)
371
- @info = Record.new(@t.json("POST", path(":#{verb}"), body: body, wait: wait ? 60 : 0, key: key))
382
+ def lifecycle(verb, body, wait, key, room: 0)
383
+ @info = Record.new(@t.json("POST", path(":#{verb}"), body: body, wait: wait ? 60 : 0, key: key, room: room))
372
384
  self
373
385
  end
374
386
 
375
- def command_body(command, cwd, env, stdin, timeout)
387
+ def room_for(wait_for_capacity) = wait_for_capacity.nil? ? @t.wait_for_capacity : [0, wait_for_capacity].max
388
+
389
+ def command_body(command, cwd, env, stdin, timeout, user)
376
390
  body = command.is_a?(Array) ? { "argv" => command.map(&:to_s) } : { "command" => command.to_s }
377
391
  body["cwd"] = cwd if cwd
392
+ body["user"] = user.to_s if user
378
393
  body["env"] = env.to_h { |key, value| [key.to_s, value.to_s] } if env
379
394
  body["stdinBase64"] = [stdin.to_s.b].pack("m0") unless stdin.nil?
380
395
  body["timeoutMs"] = (timeout * 1000).round if timeout
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithRuntime
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: withruntime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Runtime
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-16 00:00:00.000000000 Z
11
+ date: 2026-10-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Linux sandboxes for agents, and every other Runtime Cloud product: streaming
14
14
  commands and terminals, files and watches, images, durable volumes and backups,