withruntime 0.16.0 → 0.17.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: 92aca2f2d4a8d94b0871172606a8608e07d916e2e7236adab4b5752409fb466e
4
- data.tar.gz: 87b4a3b19035d177f07c5a98b5adf256e7eab48769c1f803850d10b552800f64
3
+ metadata.gz: 1c1a17e5090c78b095eebfac78fff07cbd5222b14ba973cc71c619fec9ed4722
4
+ data.tar.gz: ee609373f8a0c8dbfb12ede50c25930647456031fef41576d411a60e5743dfbb
5
5
  SHA512:
6
- metadata.gz: 28866196e471465715beba471c141ae01a1de33f5b4fc66266a53dc5fc27f170c1d0f67920bbeff930ee4bcaf00a574768e4800d71b292b8cbe88426cca049c8
7
- data.tar.gz: e1339ba49b930b10c79f043d5187906dfff71c0a2832f7dc5569b2cd0b5a62b1544f6b312ca9944d9bc384d7718eab8cfb32f11a8642e3782d29d06e6c96aee5
6
+ metadata.gz: f4ecd87a36108a2a1856e9e0051d010bdeae9393e5ada0681264d6bf81758afdabd33b3f4148e08a08ca94eb5018d718c2601bbf9f2cc1b7972a4b95886534d2
7
+ data.tar.gz: 555018e2954343260c2f88f6661d800ce8ecbd482f91acfdfe7c5f2e9b96ecd8880dfb2a16ace2c65264fd9d3311a361f398ab3637a257effd1d521e74b0062b
@@ -65,7 +65,10 @@ module WithRuntime
65
65
  # /workspace/repo), +vcpu+ and +memory_mib+ (4 vCPU and 8 GiB unless set; a
66
66
  # definition from a snapshot takes neither, InvalidRequestError, since a
67
67
  # snapshot keeps its own size), +instructions+, +env+, +secrets+ (names of
68
- # egress secrets: model keys go here), +mcp+ ([{ name:, url: }]), +volumes+
68
+ # egress secrets: model keys go here), +pool+ (1 to 16 Claude accounts,
69
+ # each an egress secret's name: each session takes the one with the most
70
+ # usage left, and the review template moves to the next when one is out
71
+ # of usage), +mcp+ ([{ name:, url: }]), +volumes+
69
72
  # ([{ volume:, path: }]), +idle_pause_seconds+, +max_session_seconds+,
70
73
  # +max_concurrent+, +max_session_dollars+ (more than 0, at most 10000, 5
71
74
  # unless set: the session and every session under it, the root's covering
@@ -75,7 +78,7 @@ module WithRuntime
75
78
  # (402), until the next day) and +template+ ("support" or
76
79
  # "team").
77
80
  class Agents < Product
78
- attr_reader :sessions, :triggers, :templates, :memory
81
+ attr_reader :sessions, :triggers, :templates, :memory, :asks
79
82
 
80
83
  def initialize(transport)
81
84
  super
@@ -83,7 +86,7 @@ module WithRuntime
83
86
  @triggers = AgentTriggers.new(transport)
84
87
  @templates = AgentTemplates.new
85
88
  @memory = AgentMemory.new(transport)
86
- # Approvals come later, here: agents.approvals.list / answer.
89
+ @asks = AgentAsks.new(transport)
87
90
  end
88
91
 
89
92
  # Makes an agent at version 1. A name already used is a ConflictError
@@ -297,20 +300,105 @@ module WithRuntime
297
300
  #
298
301
  # A slack or github trigger whose app is not installed for the account is
299
302
  # refused +not_installed+ (ConflictError).
303
+ #
304
+ # Each trigger has +rules+, every one optional: +priority+ (-100 to 100:
305
+ # queue order when the agent's maxConcurrent is full, higher first),
306
+ # +max_starts_per_hour+ (a call past it is held for you), +on_ended+
307
+ # ("start", "note" or "ask": what a call does when its key's session has
308
+ # ended), +notify+ ("email" or "webhook": where a held call is told) and,
309
+ # a webhook's only, +key+ ("{cause}": the key of a call that sends none,
310
+ # from its JSON's fields). A call no session can take is held, never
311
+ # dropped: +held+, +start_held+, +dismiss_held+.
300
312
  class AgentTriggers < Product
301
- def create(agent, kind, config = {})
302
- Record.new(@t.json("POST", "/v1/agents/#{seg(agent)}/triggers", body: { "kind" => kind, "config" => Fields.convert(config.to_h) }))
313
+ def create(agent, kind, config = {}, rules: nil)
314
+ body = { "kind" => kind, "config" => Fields.convert(config.to_h) }
315
+ body["rules"] = Fields.convert(rules.to_h) if rules
316
+ Record.new(@t.json("POST", "/v1/agents/#{seg(agent)}/triggers", body: body))
317
+ end
318
+
319
+ # Its rules, whole, in place: its secret stays. +{}+ is every default.
320
+ def set_rules(id, rules)
321
+ Record.new(@t.json("PATCH", "/v1/agents/triggers/#{seg(id)}", body: { "rules" => Fields.convert(rules.to_h) }))
322
+ end
323
+
324
+ # Calls a webhook trigger as its sender would, by its +secret+, never your
325
+ # key: +outcome+ started, joined (the open session with +key+), noted or
326
+ # held, with +session_id+, +seq+, +call_id+ and +why+. The same
327
+ # +idempotency_key+ is the same call.
328
+ def call(id, secret, text, key: nil, labels: nil, idempotency_key: nil)
329
+ body = Fields.body(text: text, key: key, labels: labels)
330
+ Record.new(@t.json("POST", "/v1/agents/triggers/#{seg(id)}/calls", body: body, bearer: secret,
331
+ key: idempotency_key))
332
+ end
333
+
334
+ # The calls it held for you, newest first; +each+ walks every page.
335
+ def held(id, state: nil, cursor: nil, limit: nil)
336
+ held_page(id, { "state" => state, "limit" => limit }, cursor)
337
+ end
338
+
339
+ # Starts a held call: one session with it and every call held for its key
340
+ # since, oldest first. +session+, +joined+ and +calls+.
341
+ def start_held(id, call_id)
342
+ Record.new(@t.json("POST", "/v1/agents/triggers/#{seg(id)}/held/#{seg(call_id)}/start", body: {}))
343
+ end
344
+
345
+ # Dismisses a held call: nothing runs.
346
+ def dismiss_held(id, call_id)
347
+ Record.new(@t.json("POST", "/v1/agents/triggers/#{seg(id)}/held/#{seg(call_id)}/dismiss", body: {}))
303
348
  end
304
349
 
305
350
  def list(agent) = @t.json("GET", "/v1/agents/#{seg(agent)}/triggers")["data"].map { |row| Record.new(row) }
306
351
 
307
- # Holds this trigger alone; its events are refused +trigger_paused+, with
308
- # +why+, where they came from. Nothing else is held.
352
+ # Holds this trigger alone, saying +why+: a webhook's calls are held for
353
+ # you (+held+), never dropped. Nothing else is held.
309
354
  def pause(id, why) = Record.new(@t.json("POST", "/v1/agents/triggers/#{seg(id)}/pause", body: { "why" => why }))
310
355
 
311
356
  def resume(id) = Record.new(@t.json("POST", "/v1/agents/triggers/#{seg(id)}/resume", body: {}))
312
357
 
313
358
  def delete(id) = @t.json("DELETE", "/v1/agents/triggers/#{seg(id)}") && nil
359
+
360
+ private
361
+
362
+ def held_page(id, query, cursor)
363
+ body = @t.json("GET", "/v1/agents/triggers/#{seg(id)}/held", query: query.merge("cursor" => cursor))
364
+ Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| held_page(id, query, after) }
365
+ end
366
+ end
367
+
368
+ # +runtime.agents.asks+: the account's inbox, the questions its agents'
369
+ # sessions need answered to go on. Each has +why+ it matters, +options+
370
+ # ({label, detail}), a recommended +pick+ and maybe an expiry. Asks of
371
+ # one agent's sessions sharing a memory, with the same +step+, are one
372
+ # row while it is open, and one answer reaches every session asking. +state+: open, answered,
373
+ # withdrawn, expired (no pick) or closed (every asking session ended).
374
+ class AgentAsks < Product
375
+ # The first page, newest first: open asks unless +state+ (one or a list)
376
+ # says otherwise; of one +agent+; carrying every one of +labels+ (who sees
377
+ # an ask). +each+ walks every page.
378
+ def list(state: nil, agent: nil, labels: nil, cursor: nil, limit: nil)
379
+ label = labels&.map { |k, v| "#{k}:#{v}" }
380
+ asks_page({ "state" => state, "agent" => agent, "label" => label, "limit" => limit }, cursor)
381
+ end
382
+
383
+ # One ask, with the sessions asking it.
384
+ def get(id) = Record.new(@t.json("GET", "/v1/agents/asks/#{seg(id)}"))
385
+
386
+ # Answers with an +option+'s label, +text+, or both: every session asking
387
+ # reads it next and wakes. One no longer open is refused +ask_closed+
388
+ # (ConflictError).
389
+ def answer(id, option = nil, text: nil, idempotency_key: nil)
390
+ raise ArgumentError, "an answer is an option's label, text, or both" if option.nil? && text.nil?
391
+
392
+ body = Fields.body(option: option, text: text)
393
+ Record.new(@t.json("POST", "/v1/agents/asks/#{seg(id)}/answer", body: body, key: idempotency_key))
394
+ end
395
+
396
+ private
397
+
398
+ def asks_page(query, cursor)
399
+ body = @t.json("GET", "/v1/agents/asks", query: query.merge("cursor" => cursor))
400
+ Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| asks_page(query, after) }
401
+ end
314
402
  end
315
403
 
316
404
  # A page of an agent's memory files; +usage+ is the agent's whole memory,
@@ -376,10 +464,11 @@ module WithRuntime
376
464
  end
377
465
  end
378
466
 
379
- # +runtime.agents.templates+: the five harnesses an agent can run, each
380
- # usable for either use, and the two uses.
467
+ # +runtime.agents.templates+: the harnesses an agent can run, each usable
468
+ # for either use, and the two uses. +review+ is a CI/CD agent step's
469
+ # reviewer, on a +pool+ of Claude accounts.
381
470
  #
382
- # runtime.agents.templates.map(&:key) # claude-code, codex, gemini-cli, claude-agent-sdk, openai-agents-sdk
471
+ # runtime.agents.templates.map(&:key) # claude-code, codex, gemini-cli, claude-agent-sdk, openai-agents-sdk, review, judge
383
472
  # runtime.agents.templates["codex"].api_key_secret
384
473
  # runtime.agents.templates.uses["support"].session_key
385
474
  # runtime.agents.templates.definition("claude-code", use: "support")
@@ -454,6 +543,36 @@ module WithRuntime
454
543
  "command" => "python3 ~/agent.py",
455
544
  "secrets" => ["OPENAI_API_KEY"]
456
545
  }
546
+ },
547
+ "review" => {
548
+ "key" => "review",
549
+ "title" => "Code review",
550
+ "command" => "python3 ~/.runtime-agent/runtime-agent review",
551
+ "apiKeySecret" => "ANTHROPIC_API_KEY",
552
+ "apiKeyHost" => "api.anthropic.com",
553
+ "subscription" => "a token per account: run `claude setup-token` on your own computer (Pro/Max) for each, store each with `runtime secrets set CLAUDE_1` scoped to api.anthropic.com, then name them all in pool (--pool CLAUDE_1 --pool CLAUDE_2)",
554
+ "definition" => {
555
+ "command" => "python3 ~/.runtime-agent/runtime-agent review",
556
+ "pool" => ["ANTHROPIC_API_KEY"],
557
+ "vcpu" => 2,
558
+ "memoryMiB" => 2048,
559
+ "maxSessionSeconds" => 1800
560
+ }
561
+ },
562
+ "judge" => {
563
+ "key" => "judge",
564
+ "title" => "Your rules",
565
+ "command" => "python3 ~/.runtime-agent/runtime-agent judge",
566
+ "apiKeySecret" => "ANTHROPIC_API_KEY",
567
+ "apiKeyHost" => "api.anthropic.com",
568
+ "subscription" => "a token per account, as for review: `runtime secrets set CLAUDE_1` scoped to api.anthropic.com for each, then name them all in pool; its instructions are the rules that block",
569
+ "definition" => {
570
+ "command" => "python3 ~/.runtime-agent/runtime-agent judge",
571
+ "pool" => ["ANTHROPIC_API_KEY"],
572
+ "vcpu" => 2,
573
+ "memoryMiB" => 2048,
574
+ "maxSessionSeconds" => 1800
575
+ }
457
576
  }
458
577
  },
459
578
  "uses" => {
@@ -158,13 +158,17 @@ module WithRuntime
158
158
  # memory_mib, port, health_path, cdn, live_version, previous_version, error
159
159
  # ({code, message} or nil), line (while a copy waits for room: lane,
160
160
  # place, ahead by lane and words; nil otherwise), created_at, updated_at,
161
- # deleted_at, and release_watch (alert, the default; rollback; or off). A Function
162
- # also has sleep_after_ms (250 to 5000).
161
+ # deleted_at, and release_watch (alert, the default; rollback; or off). An App
162
+ # also has release_stages (at_once, the default, or staged), release_soak_seconds
163
+ # (60 to 3600) and staged_release (its staged release under way or its last:
164
+ # version, from, state rolling/back/done/failed/ended, stage, stages, copies,
165
+ # of, soak_seconds, detail, started_at, updated_at; nil before its first). A
166
+ # Function also has sleep_after_ms (250 to 5000).
163
167
  #
164
168
  # A deploy: id, version, url and state (building, warming, switching, live,
165
169
  # failed, superseded); a failed or superseded one has error with code
166
170
  # (deploy_build_failed, deploy_health_failed, deploy_stop_failed,
167
- # deploy_switch_failed, deploy_superseded), message, hint and last_output. A version: version,
171
+ # deploy_stage_failed, deploy_switch_failed, deploy_superseded), message, hint and last_output. A version: version,
168
172
  # live, previous, saved, deploy (its deploy's id) and build (nil for an
169
173
  # image). A copy: id, version,
170
174
  # region, state (starting, serving, draining, frozen, parked) and since.
@@ -186,7 +190,13 @@ module WithRuntime
186
190
  # nothing cached), +name:+, +labels:+, +release_watch:+ ("alert" by default: tell the
187
191
  # owners when a new version gets clearly worse in its first 15 minutes; "rollback" also
188
192
  # goes back to the previous version; "off" does neither), and for an App
189
- # +min_copies:+ (0). A Function leaves +min_copies+ out, zero included.
193
+ # +min_copies:+ (0). An App also takes +release_stages:+ ("at_once" by default;
194
+ # "staged" starts one copy of a new version beside the live version's, then
195
+ # about 10%, 50% and all of them, each stage judged on its visitors' errors
196
+ # and speed before the next; a stage that does worse goes back, its copies
197
+ # drain and the deploy fails with deploy_stage_failed, the live version never
198
+ # stopping) and +release_soak_seconds:+ (60 to 3600, default 300: how long
199
+ # each stage runs once its copies serve). A Function leaves +min_copies+ out, zero included.
190
200
  # A Function also takes +sleep_after_ms:+ (250 to 5000, default 1000): at
191
201
  # most how long a copy stays awake after its last request ends; awake time
192
202
  # with no request in flight is not billed. An App is refused it.
@@ -10,7 +10,7 @@ module WithRuntime
10
10
  # job = runtime.ci.checks.get(check.id).jobs.first
11
11
  # puts runtime.ci.jobs.get(job.id).log
12
12
  class CI < Product
13
- attr_reader :repositories, :checks, :jobs, :tests, :flakes
13
+ attr_reader :repositories, :checks, :jobs, :tests, :flakes, :releases
14
14
 
15
15
  def initialize(transport)
16
16
  super
@@ -19,13 +19,15 @@ module WithRuntime
19
19
  @jobs = CIJobs.new(transport)
20
20
  @tests = CITests.new(transport)
21
21
  @flakes = CIFlakes.new(transport)
22
+ @releases = CIReleases.new(transport)
22
23
  end
23
24
  end
24
25
 
25
26
  # +runtime.ci.repositories+: the repositories whose checks run.
26
27
  #
27
28
  # A repository: id, name, labels, repository (owner/name), production_branch,
28
- # config_path, installation, github_repository_id, state (connected or
29
+ # config_path, installation, github_repository_id, undo_culprits (whether a
30
+ # red production branch's culprit is undone: set_undo), state (connected or
29
31
  # disconnected), error, created_at, updated_at and deleted_at.
30
32
  class CIRepositories < Product
31
33
  # Connects +repository+ ("owner/name"), read through Runtime's GitHub App.
@@ -45,6 +47,15 @@ module WithRuntime
45
47
  page("/v1/ci/repositories", { "name" => name, "label" => label, "state" => state, "limit" => limit, "cursor" => cursor })
46
48
  end
47
49
 
50
+ # Turns on (or off) the undo of a red production branch's culprit: when
51
+ # main's check fails, CI/CD finds the first commit that fails it and opens
52
+ # a pull request reverting it, which lands once its own check passes. Off
53
+ # until set; only an owner's or admin's full key sets it.
54
+ def set_undo(id, on, idempotency_key: nil)
55
+ Record.new(@t.json("POST", "/v1/ci/repositories/#{seg(id)}:update", body: { "undoCulprits" => on },
56
+ key: idempotency_key))
57
+ end
58
+
48
59
  # Stops running its checks; the checks already run stay readable.
49
60
  def disconnect(id, idempotency_key: nil)
50
61
  Record.new(@t.json("DELETE", "/v1/ci/repositories/#{seg(id)}", key: idempotency_key))
@@ -68,15 +79,24 @@ module WithRuntime
68
79
 
69
80
  # The check with its +jobs+ in plan order (no log).
70
81
  def get(id) = Record.new(@t.json("GET", "/v1/ci/checks/#{seg(id)}"))
82
+
83
+ # Cancels a queued or running check: its jobs end and its machines are let
84
+ # go. A check that has ended is answered as it is.
85
+ def cancel(id) = Record.new(@t.json("POST", "/v1/ci/checks/#{seg(id)}:cancel"))
86
+
87
+ # Runs a check again, as GitHub's Re-run does: a new check of the same
88
+ # commit, trusted only as the commit's last check was.
89
+ def rerun(id) = Record.new(@t.json("POST", "/v1/ci/checks/#{seg(id)}:rerun"))
71
90
  end
72
91
 
73
92
  # +runtime.ci.jobs+: one command of a check, run in sandboxes.
74
93
  class CIJobs < Product
75
94
  # The job with its +log+ (the tail, at most 256 KiB). A failed job's
76
95
  # sandbox_ids may be kept paused: wake one or exec in it with
77
- # +runtime.sandboxes+. A review step's job has its +review+: the verdict
78
- # (pass or block), summary, findings (kind, file, line, what, fix), notes,
79
- # and the agent, account, model and session that gave it; nil otherwise.
96
+ # +runtime.sandboxes+. An agent step's job (+review:+ is its short form)
97
+ # has its +review+: the verdict (pass or block), summary, findings (kind,
98
+ # file, line, what, fix), notes, and the agent, the model the step asked
99
+ # for (nil when none) and the session that gave it; nil otherwise.
80
100
  def get(id) = Record.new(@t.json("GET", "/v1/ci/jobs/#{seg(id)}"))
81
101
  end
82
102
 
@@ -119,4 +139,18 @@ module WithRuntime
119
139
  Record.new(@t.json("POST", "/v1/ci/flakes/#{seg(id)}:update", body: body, key: idempotency_key))
120
140
  end
121
141
  end
142
+
143
+ # +runtime.ci.releases+: the steps each release took, as it took them: a
144
+ # migration's copy test or apply, a deploy ref moved, a server's canary or
145
+ # release, the website's deploy or promote, or a wait and why. Only
146
+ # repositories whose releases CI/CD runs have any.
147
+ #
148
+ # A step: id, repository_id, commit, source (mover, or the deploy target
149
+ # that walked it), host (or nil), line and at.
150
+ class CIReleases < Product
151
+ # Steps, newest first; +commit:+ keeps one release's.
152
+ def list(repository_id: nil, commit: nil, limit: nil, cursor: nil)
153
+ page("/v1/ci/releases", { "repositoryId" => repository_id, "commit" => commit, "limit" => limit, "cursor" => cursor })
154
+ end
155
+ end
122
156
  end
@@ -96,6 +96,20 @@ module WithRuntime
96
96
  # The account's credit, holds, free time and per-resource charges.
97
97
  def usage = Record.new(@transport.json("GET", "/v1/usage"))
98
98
 
99
+ # One page of settled usage for every product the key sees: settled at
100
+ # +since+ or later and before +until+ (UTC, ending in Z). Follow
101
+ # +next_cursor+ with the same range until it is nil; concatenate each
102
+ # page's +csv+ (the header is on the first only). Money and quantities are
103
+ # exact decimal strings. +final_through+: no row settled before it can
104
+ # still appear or change, so the range is final once it reaches +until+.
105
+ # A row's +final+ is true once its resource is deleted and can be charged
106
+ # nothing more.
107
+ def usage_export(since:, until:, cursor: nil, limit: nil)
108
+ # until is a Ruby keyword: its argument is read through the binding.
109
+ query = { "since" => since, "until" => binding.local_variable_get(:until), "cursor" => cursor, "limit" => limit }
110
+ Record.new(@transport.json("GET", "/v1/usage/export", query: query))
111
+ end
112
+
99
113
  # Calls any API path with this client's key, retries, idempotency keys and
100
114
  # errors, and returns the parsed JSON answer. For an endpoint newer than this SDK.
101
115
  def request(method, path, query: nil, body: nil)
@@ -81,7 +81,20 @@ module WithRuntime
81
81
  def backup(id, name: nil, labels: nil, retention_days: nil, wait: 60, idempotency_key: nil)
82
82
  Record.new(@t.json("POST", "/v1/volumes/#{seg(id)}:backup", body: Fields.body(name: name, labels: labels, retention_days: retention_days), wait: wait, key: idempotency_key))
83
83
  end
84
- def set_backup_policy(id, daily: nil, retention_days: nil) = Record.new(@t.json("POST", "/v1/volumes/#{seg(id)}:backup-policy", body: Fields.body(daily: daily, retention_days: retention_days)))
84
+ # Automatic backups on or off and the days a daily one is kept; or a
85
+ # schedule of the volume's own: every_minutes (15 or more) or a five-field
86
+ # UTC cron, keeping the keep counts (last, hourly, daily, weekly, monthly,
87
+ # yearly). no_schedule: true goes back to daily backups.
88
+ def set_backup_policy(id, daily: nil, retention_days: nil, every_minutes: nil, cron: nil, keep: nil, no_schedule: false)
89
+ raise ArgumentError, "Give one of every_minutes, cron and no_schedule." if [every_minutes, cron, (no_schedule || nil)].compact.size > 1
90
+
91
+ body = Fields.body(daily: daily, retention_days: retention_days, keep: keep)
92
+ if every_minutes then body["schedule"] = { "everyMinutes" => every_minutes }
93
+ elsif cron then body["schedule"] = { "cron" => cron }
94
+ elsif no_schedule then body["schedule"] = nil
95
+ end
96
+ Record.new(@t.json("POST", "/v1/volumes/#{seg(id)}:backup-policy", body: body))
97
+ end
85
98
  def backups(volume_id: nil, state: nil, limit: nil) = page("/v1/volume-backups", { "volumeId" => volume_id, "state" => state, "limit" => limit })
86
99
  def get_backup(id) = Record.new(@t.json("GET", "/v1/volume-backups/#{seg(id)}"))
87
100
  def delete_backup(id) = Record.new(@t.json("POST", "/v1/volume-backups/#{seg(id)}:delete", body: {}))
@@ -146,8 +159,20 @@ module WithRuntime
146
159
  Record.new(@t.json("PUT", "/v1/egress-secrets/#{seg(name)}/rules", body: body))
147
160
  end
148
161
 
149
- # Names, hosts and placeholders. Never values.
150
- def list = @t.json("GET", "/v1/egress-secrets")["secrets"].map { |secret| Record.new(secret) }
162
+ # Names, hosts and placeholders. Never values. With +limit:+ (1 to 100)
163
+ # or +after:+, one page by name: the secrets named after +after+; the next
164
+ # page's +after+ is this one's last name. Without, every secret.
165
+ def list(limit: nil, after: nil)
166
+ secrets = []
167
+ start = after
168
+ loop do
169
+ page = @t.json("GET", "/v1/egress-secrets", query: { "limit" => limit, "after" => start })
170
+ secrets.concat(page["secrets"].map { |secret| Record.new(secret) })
171
+ return secrets if limit || after || !page["next"]
172
+
173
+ start = page["next"]
174
+ end
175
+ end
151
176
 
152
177
  # Erases a secret; its placeholder stops working.
153
178
  def delete(name)
@@ -377,7 +402,8 @@ module WithRuntime
377
402
 
378
403
  # Replaces the rules: +internet:+, +allow:+ (example.com, *.example.com, an
379
404
  # address or a CIDR range), +deny:+, and +connect:+ (host:port pairs beyond
380
- # 80 and 443; paid accounts only).
405
+ # 80 and 443; paid accounts only). +internet: false+ with +allow:+ is
406
+ # allow-list only, stored as +internet: true+ with that list.
381
407
  def set(internet:, allow: nil, deny: nil, connect: nil)
382
408
  body = { "internet" => internet, "allow" => allow, "deny" => deny, "connect" => connect }.compact
383
409
  Record.new(@sandbox.transport.json("PUT", @sandbox.path("/network"), body: body))
@@ -214,8 +214,8 @@ module WithRuntime
214
214
  end
215
215
 
216
216
  # Starts copies of this sandbox as it is now (files, memory, running
217
- # processes), each its own sandbox, on the same server, answered once they
218
- # run. One copy without +count+; an array with it. If a copy fails, the
217
+ # processes), each its own sandbox, on its server while it has room and on
218
+ # another when it is full, answered once they run. One copy without +count+; an array with it. If a copy fails, the
219
219
  # error's +details["startedSandboxIds"]+ names the copies that did start.
220
220
  # +funding+ is accepted and ignored: copies run on free usage first, then credit.
221
221
  # +secrets: [name, ...]+ is the egress secrets the copies may use, by name:
@@ -378,8 +378,10 @@ module WithRuntime
378
378
 
379
379
  # Sends a call, retrying as the rules say, and returns the response once it
380
380
  # succeeded. With a block, the body is streamed to it.
381
+ # +bearer+ is sent instead of the API key: a webhook trigger's secret,
382
+ # which is all its calls carry.
381
383
  def send_call(http, method, path, query: nil, body: nil, raw: nil, accept: "application/json", wait: nil,
382
- key: nil, no_retry: false, room: nil, timeout: nil, cancel: nil, &stream)
384
+ key: nil, no_retry: false, room: nil, timeout: nil, cancel: nil, bearer: nil, &stream)
383
385
  write = method != "GET"
384
386
  key = write ? (key || SecureRandom.uuid) : nil
385
387
  # room: nil for a call with no room wait; 0 for a create, wake, restart
@@ -400,7 +402,7 @@ module WithRuntime
400
402
  raise connection_error(true, write, key) if left <= 0
401
403
 
402
404
  request = Net::HTTPGenericRequest.new(method, !payload.nil?, true, target(path, query))
403
- request["Authorization"] = "Bearer #{api_key}"
405
+ request["Authorization"] = "Bearer #{bearer || api_key}"
404
406
  request["Accept"] = accept
405
407
  request["X-Runtime-Client"] = @client
406
408
  request["User-Agent"] = "withruntime-ruby/#{VERSION}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithRuntime
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.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.16.0
4
+ version: 0.17.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-22 00:00:00.000000000 Z
11
+ date: 2026-10-25 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,