withruntime 0.20.0 → 0.26.1

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: a7812f8e62878add0f508438d590167776350a52b7f9c213e77fee8447afadac
4
- data.tar.gz: 84b04befc8a18f0420a1026fed88a7a29d4e10efc01dbad3b3632ea4ea515829
3
+ metadata.gz: 7c6510ac6423ac945d6e5f55779d4f913ec4c13a4bd64992ec825637219d1373
4
+ data.tar.gz: dbfd02ae2ffe411a32158234ab0d0faf8cf52263149c763cba750aed4db23a94
5
5
  SHA512:
6
- metadata.gz: ad569904e75cd01d9a9725a85a77cbdb0e5a44fa709e5bec3e6c40919a0db3675b94b2c8ad6fd8b8f99a50e666147667229f26814be9cdf142396ac6eee27db6
7
- data.tar.gz: ba71b0cae765bfc87c8645587abd698643e96087452aa8e499630632e955df8f72cd0c961e37b798bd7dfa795827d498d84a1da455d4e742326f2aa5fad76cd8
6
+ metadata.gz: ae5ff418881522c0748184c85262a960e4425d6eb32526482178bde4c862a682580140f373e3858dd66039857117fc73eaf4c9ea0ba7b057bb5c7932d5010952
7
+ data.tar.gz: 6be184e2e55d835096df364ae1077b615485ddad59f6afe87b65c2ab6793a291962151720fbae4d223fb5c6ff5752ff08c5e32bd654229b33c58681fb1f639d6
@@ -42,7 +42,8 @@ module WithRuntime
42
42
  # +state+ (queued, running, waiting, done, failed or stopped), +reason+
43
43
  # (why it ended; among others "budget", "parent_ended", and "deleted" on a
44
44
  # deleted record), +result+, +sandbox_id+, +last_seq+, +labels+,
45
- # +created_at+, +started_at+, +ended_at+.
45
+ # +overrides+ (what its start overrode of its agent's definition, as given,
46
+ # or nil), +created_at+, +started_at+, +ended_at+.
46
47
  #
47
48
  # Its tree: +parent_session_id+ (nil for a top session), +root_session_id+
48
49
  # (itself for a top session) and +depth+ (0 to 3). Its use: +cost+ (this
@@ -51,7 +52,8 @@ module WithRuntime
51
52
  # session is deleted) and +attachment_bytes+ (its files and screenshots).
52
53
  # +stop_why+ is why a stop was asked ("stopped", "budget" or
53
54
  # "parent_ended") or nil, and +deleted_at+ is set once its history was
54
- # deleted (+sessions.delete+).
55
+ # deleted (+sessions.delete+). +workflow_id+ is the workflow that started
56
+ # it as one of its steps, or nil.
55
57
  class AgentSessionInfo < Record
56
58
  def cost = AgentCost.new(self["cost"])
57
59
  def tree_cost = AgentCost.new(self["treeCost"])
@@ -95,18 +97,24 @@ module WithRuntime
95
97
  # its tree; a tree at it stops, reason "budget"), +max_day_dollars+ (more
96
98
  # than 0, at most 100000, 100 unless set: the agent's sessions started that
97
99
  # UTC day; an agent at it starts nothing new, +budget_reached+
98
- # (402), until the next day) and +template+ ("support" or
99
- # "team").
100
+ # (402), until the next day), +max_children+ (1 to 1000, 100 unless set:
101
+ # the sessions one session's harness may start), +advisor+ ({ model:,
102
+ # provider:, secret:, base_url:, max_calls:, instructions: }, all but
103
+ # +model+ optional: a model its sessions may ask for advice with the
104
+ # advise tool, each call kept in the log as a note) and +template+
105
+ # ("support" or "team").
100
106
  class Agents < Product
101
- attr_reader :sessions, :triggers, :templates, :memory, :asks
107
+ attr_reader :sessions, :triggers, :slack, :templates, :memory, :asks, :workflows
102
108
 
103
109
  def initialize(transport)
104
110
  super
105
111
  @sessions = AgentSessions.new(transport)
106
112
  @triggers = AgentTriggers.new(transport)
113
+ @slack = AgentSlack.new(transport)
107
114
  @templates = AgentTemplates.new
108
115
  @memory = AgentMemory.new(transport)
109
116
  @asks = AgentAsks.new(transport)
117
+ @workflows = AgentWorkflows.new(transport)
110
118
  end
111
119
 
112
120
  # Claude Managed Agents' API on Runtime: the options Anthropic's Ruby SDK
@@ -146,10 +154,16 @@ module WithRuntime
146
154
  # Starts a session with +input+, its first words. With +key:+, your own
147
155
  # name for the conversation (a Slack thread, a pull request, one of your
148
156
  # customers such as "customer:42"), words with the same key join the open
149
- # session instead of starting a second; +session.joined+ says so. An agent
150
- # at its day's budget is refused +budget_reached+ (402).
151
- def start(agent, input, key: nil, labels: nil, idempotency_key: nil)
152
- body = Fields.body(input: input, key: key, labels: labels)
157
+ # session instead of starting a second; +session.joined+ says so.
158
+ # +overrides:+ gives this session its own model, instructions, MCP
159
+ # servers, disallowed tools, size and budget within the agent's
160
+ # +sessionOverrides+ ({ model:, instructions:, instructions_mode:
161
+ # "replace", mcp: [names], disallowed_tools: ["Bash", "spawn"], vcpu:,
162
+ # memory_mib:, max_session_dollars: }); one it does not allow is refused
163
+ # +permission_denied+ (403), naming what it allows. An agent at its day's
164
+ # budget is refused +budget_reached+ (402).
165
+ def start(agent, input, key: nil, labels: nil, overrides: nil, idempotency_key: nil)
166
+ body = Fields.body(input: input, key: key, labels: labels, overrides: overrides)
153
167
  answer = @t.json("POST", "/v1/agents/#{seg(agent)}/sessions", body: body, key: idempotency_key)
154
168
  AgentSession.new(@t, answer["session"], joined: answer["joined"] == true, seq: answer["seq"])
155
169
  end
@@ -411,6 +425,23 @@ module WithRuntime
411
425
  end
412
426
  end
413
427
 
428
+ # +runtime.agents.slack+: Runtime's Slack app in your workspaces. Add it with
429
+ # +install+'s link; a slack trigger then names a workspace's +team+ and a
430
+ # channel the app is in.
431
+ class AgentSlack < Product
432
+ # A link that adds Runtime's Slack app to a workspace, good for 15
433
+ # minutes: +url+ and +expires_at+. Open it as a person who may add apps
434
+ # to the workspace; Slack sends them back to the console once it is added.
435
+ def install = Record.new(@t.json("POST", "/v1/agents/slack/installs", body: {}))
436
+
437
+ # The workspaces added to this account: +team+, +name+, +bot_user+, +installed_at+.
438
+ def teams = @t.json("GET", "/v1/agents/slack/teams")["data"].map { |row| Record.new(row) }
439
+
440
+ # Removes a workspace: its token is revoked, and its triggers refuse
441
+ # events until it is added again.
442
+ def remove(team) = @t.json("DELETE", "/v1/agents/slack/teams/#{seg(team)}") && nil
443
+ end
444
+
414
445
  # +runtime.agents.asks+: the account's inbox, the questions its agents'
415
446
  # sessions need answered to go on. Each has +why+ it matters, +options+
416
447
  # ({label, detail}), a recommended +pick+ and maybe an expiry. Asks of
@@ -459,6 +490,125 @@ module WithRuntime
459
490
  end
460
491
  end
461
492
 
493
+ # +runtime.agents.workflows+: many sessions in phases. A workflow is data,
494
+ # not code: each phase names an +agent+ (or uses the workflow's), an
495
+ # +input+ template ({{input}}, {{item}}, {{index}}, and {{<phase>}} for an
496
+ # earlier phase's results) and +each+, a list of items (1 to 1000) or
497
+ # { from: "<phase>", split: "json" | "lines" } to fan out over an earlier
498
+ # phase's result. A phase waits for every phase before it, unless
499
+ # +pipeline+ is true: then each item starts as soon as its own item in the
500
+ # source phase ends. +max_sessions+ (100 unless set, at most 1000),
501
+ # +max_concurrent+ (20, at most 200) and +max_dollars+ (10, at most 10000)
502
+ # bound it; at the cap it fails with reason "budget".
503
+ #
504
+ # flow = runtime.agents.workflows.run(agent: "dev", phases: [
505
+ # { name: "fix", input: "Fix {{item}}", each: %w[a.ts b.ts] }
506
+ # ])
507
+ # flow.follow { |w| puts w.phases.map { |p| "#{p.name} #{p.done}/#{p.total}" } }
508
+ # puts flow.wait.result
509
+ class AgentWorkflows < Product
510
+ # Runs it now and answers an AgentWorkflow to follow, wait for and stop.
511
+ # An invalid spec is refused (InvalidRequestError), its details naming
512
+ # the field.
513
+ def run(spec = nil, idempotency_key: nil, **fields)
514
+ body = Fields.convert(spec || fields)
515
+ AgentWorkflow.new(@t, @t.json("POST", "/v1/agents/workflows", body: body, key: idempotency_key))
516
+ end
517
+
518
+ def get(id) = AgentWorkflow.new(@t, @t.json("GET", "/v1/agents/workflows/#{seg(id)}"))
519
+
520
+ # The first page, newest first, in a +state+ (one or a list) if given;
521
+ # +each+ walks every page.
522
+ def list(state: nil, cursor: nil, limit: nil) = workflows_page({ "state" => state, "limit" => limit }, cursor)
523
+
524
+ private
525
+
526
+ def workflows_page(query, cursor)
527
+ body = @t.json("GET", "/v1/agents/workflows", query: query.merge("cursor" => cursor))
528
+ Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| workflows_page(query, after) }
529
+ end
530
+ end
531
+
532
+ # One workflow. +info+: +id+, +name+, +state+ (running, done, failed or
533
+ # stopped), +reason+, +result+ (one step's result as it is, or a JSON
534
+ # array of the last phase's results), +parent_session_id+, +spec+, its
535
+ # limits, +started+, +version+ (one more on every change), +labels+,
536
+ # +phases+ (each +name+, +agent_id+, +total+, +ready+, +pending+,
537
+ # +running+, +done+, +failed+), +cost+ and its times.
538
+ class AgentWorkflow
539
+ POLL_SECONDS = AgentSession::POLL_SECONDS
540
+ POLL_MARGIN = AgentSession::POLL_MARGIN
541
+
542
+ attr_reader :info
543
+
544
+ def initialize(transport, info)
545
+ @t = transport
546
+ @info = Record.new(info)
547
+ end
548
+
549
+ def id = @info["id"]
550
+ def state = @info["state"]
551
+ def ended? = state != "running"
552
+ def result = @info["result"]
553
+
554
+ # Reads it again.
555
+ def refresh
556
+ @info = Record.new(@t.json("GET", path))
557
+ self
558
+ end
559
+
560
+ # Each change after version +after+ (0: from the start) as it happens;
561
+ # without a block, an Enumerator. Waits on the server and ends once the
562
+ # workflow has ended.
563
+ def follow(after: 0)
564
+ return enum_for(:follow, after: after) unless block_given?
565
+
566
+ loop do
567
+ now = poll(after)
568
+ @info = now
569
+ if now["version"] > after
570
+ after = now["version"]
571
+ yield now
572
+ end
573
+ return nil if ended?
574
+ end
575
+ end
576
+
577
+ # The workflow once it has ended; a failed one is returned, not raised.
578
+ # Waits on the server, with no polling.
579
+ def wait
580
+ refresh
581
+ @info = poll(@info["version"]) until ended?
582
+ @info
583
+ end
584
+
585
+ # The first page of its steps in phase and item order: +phase+, +item+,
586
+ # +item_text+, +state+ (pending, running, done, failed, stopped or
587
+ # skipped), +reason+, +result+, +session_id+ and times.
588
+ def steps(state: nil, cursor: nil, limit: nil) = steps_page({ "state" => state, "limit" => limit }, cursor)
589
+
590
+ # Stops it: nothing more starts, and its open sessions are asked to stop.
591
+ def stop
592
+ @info = Record.new(@t.json("POST", path("/stop"), body: {}))
593
+ end
594
+
595
+ def inspect = "#<#{self.class.name} #{id} #{state}>"
596
+
597
+ private
598
+
599
+ def path(suffix = "") = "/v1/agents/workflows/#{Transport.segment(id)}#{suffix}"
600
+
601
+ def poll(after)
602
+ Record.new(@t.json("GET", path, query: { "after" => after, "wait" => POLL_SECONDS },
603
+ timeout: [@t.timeout, POLL_SECONDS + POLL_MARGIN].max))
604
+ end
605
+
606
+ def steps_page(query, cursor)
607
+ body = @t.json("GET", path("/steps"), query: query.merge("cursor" => cursor))
608
+ Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| steps_page(query, after) }
609
+ end
610
+ end
611
+
462
612
  # A page of an agent's memory files; +usage+ is the agent's whole memory,
463
613
  # every scope: +bytes+, +files+, +limit_bytes+ and +limit_files+.
464
614
  class AgentMemoryPage < Page
@@ -526,7 +676,7 @@ module WithRuntime
526
676
  # for either use, and the two uses. +review+ is a CI/CD agent step's
527
677
  # reviewer, on a +pool+ of Claude accounts.
528
678
  #
529
- # runtime.agents.templates.map(&:key) # claude-code, codex, gemini-cli, claude-agent-sdk, openai-agents-sdk, review, judge
679
+ # runtime.agents.templates.map(&:key) # claude-code, codex, gemini-cli, claude-agent-sdk, openai-agents-sdk, review, judge, browser
530
680
  # runtime.agents.templates["codex"].api_key_secret
531
681
  # runtime.agents.templates.uses["support"].session_key
532
682
  # runtime.agents.templates.definition("claude-code", use: "support")
@@ -631,6 +781,21 @@ module WithRuntime
631
781
  "memoryMiB" => 2048,
632
782
  "maxSessionSeconds" => 1800
633
783
  }
784
+ },
785
+ "browser" => {
786
+ "key" => "browser",
787
+ "title" => "Browser task",
788
+ "command" => "python3 ~/.runtime-agent/runtime-agent browse",
789
+ "apiKeySecret" => "ANTHROPIC_API_KEY",
790
+ "apiKeyHost" => "api.anthropic.com",
791
+ "subscription" => nil,
792
+ "definition" => {
793
+ "command" => "python3 ~/.runtime-agent/runtime-agent browse",
794
+ "secrets" => ["ANTHROPIC_API_KEY"],
795
+ "vcpu" => 2,
796
+ "memoryMiB" => 4096,
797
+ "maxSessionSeconds" => 1800
798
+ }
634
799
  }
635
800
  },
636
801
  "uses" => {
@@ -183,7 +183,7 @@ module WithRuntime
183
183
  super(transport, path, %w[failed deleted live])
184
184
  end
185
185
 
186
- # Creates one waiting for its first version: +max_copies:+ (1 to 1000),
186
+ # Creates one waiting for its first version: +max_copies:+ (1 to the account's App copies at once, its plan's sandboxes at once),
187
187
  # +vcpu:+ (1 to 16) and +memory_mib:+ (128 to 65536) are required;
188
188
  # +concurrency:+, +spares:+, +port:+ (8080), +health_path:+, +cdn:+ (true: Cloudflare's CDN in
189
189
  # front of its addresses; false sends visitors straight to Runtime's routers,
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WithRuntime
4
+ # +runtime.browsers+: a task in plain words, done by an agent that browses
5
+ # in a sandbox with Chromium, at /v1/browsers/runs. In a private beta: an
6
+ # account outside it is refused with product_in_beta (403,
7
+ # PermissionDeniedError).
8
+ #
9
+ # run = runtime.browsers.run("Find the price of the cheapest plan on withruntime.com")
10
+ # puts run.output || run.error
11
+ # runtime.browsers.runs.follow(run.id) { |step| puts "#{step.n} #{step.url}" }
12
+ #
13
+ # The model's key is your egress secret: ANTHROPIC_API_KEY for claude-
14
+ # models (the default, claude-sonnet-5-5), OPENAI_API_KEY for gpt- and
15
+ # o-series, GEMINI_API_KEY for gemini-. A run is billed as its sandbox's
16
+ # time, at the sandbox rates; the model's use is on your key.
17
+ #
18
+ # A run: id, state (queued, running, done, failed or stopped), task, model,
19
+ # start_url, output (the answer, once done), error (why it failed, and what
20
+ # to do), steps, url (the page it ended on), sandbox_id, labels,
21
+ # created_at, started_at and ended_at. A step: n (0 is the page before the
22
+ # first step; the last, the page at the end), url, title, goal, actions,
23
+ # frame (the page as a base64 JPEG), at and seq.
24
+ class Browsers < Product
25
+ attr_reader :runs
26
+
27
+ def initialize(transport)
28
+ super
29
+ @runs = BrowserRuns.new(transport)
30
+ end
31
+
32
+ # Does +task+ in a browser and returns the run once it has ended: +output+
33
+ # once done, +error+ once failed. Also +start_url:+, +model:+,
34
+ # +max_steps:+, +allowed_domains:+ and +labels:+.
35
+ def run(task, **fields)
36
+ @runs.wait(@runs.create(task, **fields).id)
37
+ end
38
+ end
39
+
40
+ # +runtime.browsers.runs+: start, read, follow and stop runs.
41
+ class BrowserRuns < Product
42
+ ENDED = %w[done failed stopped].freeze
43
+ # How long the API holds a steps read open, and the margin past it a
44
+ # read waits for its answer.
45
+ POLL_SECONDS = 30
46
+ POLL_MARGIN = 15
47
+
48
+ # Starts a run and returns it at once (state queued). +start_url:+ is
49
+ # opened before the first step; +max_steps:+ is 1 to 200 (30 unless
50
+ # set); +allowed_domains:+ limits the pages it opens.
51
+ def create(task, start_url: nil, model: nil, max_steps: nil, allowed_domains: nil, labels: nil, idempotency_key: nil)
52
+ body = Fields.body(task: task, start_url: start_url, model: model, max_steps: max_steps,
53
+ allowed_domains: allowed_domains, labels: labels)
54
+ Record.new(@t.json("POST", "/v1/browsers/runs", body: body, key: idempotency_key))
55
+ end
56
+
57
+ def get(id) = Record.new(@t.json("GET", path(id)))
58
+
59
+ # The first page of runs, newest first; +each+ walks every page.
60
+ def list(cursor: nil, limit: nil)
61
+ body = @t.json("GET", "/v1/browsers/runs", query: { "cursor" => cursor, "limit" => limit })
62
+ Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| list(cursor: after, limit: limit) }
63
+ end
64
+
65
+ # One page of its steps after seq +after+: {data, after, ended}. +wait:+
66
+ # (seconds, up to 30) holds the answer on the server for a step.
67
+ def steps(id, after: 0, limit: nil, frames: nil, wait: nil)
68
+ query = { "after" => after, "limit" => limit, "frames" => frames, "wait" => wait }
69
+ timeout = wait ? [@t.timeout, wait + POLL_MARGIN].max : nil
70
+ @t.json("GET", path(id, "/steps"), query: query, timeout: timeout)
71
+ end
72
+
73
+ # Each step as it comes, until the run ends: its recording. Without a
74
+ # block, an Enumerator. Each read waits on the server, not a timer.
75
+ def follow(id, after: 0, frames: true, &block)
76
+ return enum_for(:follow, id, after: after, frames: frames) unless block
77
+
78
+ loop do
79
+ page = steps(id, after: after, frames: frames ? nil : false, wait: POLL_SECONDS)
80
+ data = page["data"] || []
81
+ data.each { |step| yield Record.new(step) }
82
+ after = page["after"] || after
83
+ return nil if page["ended"] && data.empty?
84
+ end
85
+ end
86
+
87
+ # Stops it; its sandbox is deleted and its steps are kept.
88
+ def stop(id) = Record.new(@t.json("POST", path(id, ":stop"), body: {}))
89
+
90
+ # The run once it has ended (done, failed or stopped); a failed run is
91
+ # returned, not raised: read +error+.
92
+ def wait(id)
93
+ run = get(id)
94
+ return run if ENDED.include?(run.state)
95
+
96
+ follow(id, frames: false) { |_step| nil }
97
+ get(id)
98
+ end
99
+
100
+ private
101
+
102
+ def path(id, suffix = "") = "/v1/browsers/runs/#{seg(id)}#{suffix}"
103
+ end
104
+ end
@@ -12,7 +12,9 @@ module WithRuntime
12
12
  # A build: id, name, labels, repository_id, ref, branch, commit, state
13
13
  # (queued, fetching, building, deploying, succeeded, failed, cancelled,
14
14
  # superseded), reason, timings (milliseconds per step once done, or nil),
15
- # output_snapshot_id, error, created_at, updated_at and deleted_at.
15
+ # output_snapshot_id, framework (detected for the commands the repository
16
+ # leaves unset: nextjs, vite, fastapi, rails, go, docker, ...; or nil),
17
+ # error, created_at, updated_at and deleted_at.
16
18
  class Builds < Product
17
19
  TERMINAL = %w[succeeded failed cancelled superseded].freeze
18
20
  # Reconnections in a row with no new line before a follow gives up.
@@ -72,10 +72,13 @@ module WithRuntime
72
72
  # created_at, updated_at, started_at and ended_at.
73
73
  class CIChecks < Product
74
74
  # Checks, newest first. +state+ is any of queued, running, passed, failed,
75
- # cancelled; +commit+ is a full sha.
76
- def list(repository_id: nil, state: nil, branch: nil, commit: nil, pull_request: nil, limit: nil, cursor: nil)
77
- page("/v1/ci/checks", { "repositoryId" => repository_id, "state" => state, "branch" => branch, "commit" => commit,
78
- "pullRequest" => pull_request, "limit" => limit, "cursor" => cursor })
75
+ # cancelled; +event+ any of push, pull_request, merge_group (GitHub's merge
76
+ # queue) or rerun; +commit+ is a full sha.
77
+ def list(repository_id: nil, state: nil, event: nil, branch: nil, commit: nil, pull_request: nil, limit: nil,
78
+ cursor: nil)
79
+ page("/v1/ci/checks", { "repositoryId" => repository_id, "state" => state, "event" => event, "branch" => branch,
80
+ "commit" => commit, "pullRequest" => pull_request, "limit" => limit,
81
+ "cursor" => cursor })
79
82
  end
80
83
 
81
84
  # The check with its +jobs+ in plan order (no log).
@@ -25,7 +25,7 @@ module WithRuntime
25
25
  class Client
26
26
  attr_reader :transport, :sandboxes, :snapshots, :images, :volumes, :secrets, :reservations, :limits, :jobs, :emails, :referrals, :plans,
27
27
  :switching, :events, :webhooks, :otel, :audit, :feedback, :support, :domains, :ports, :ips, :networks, :sso, :mcp, :agents,
28
- :apps, :functions, :builds, :ci, :containers
28
+ :apps, :functions, :builds, :ci, :containers, :browsers
29
29
 
30
30
  # +api_key+: default RUNTIME_API_KEY, then this machine's saved connection.
31
31
  # +base_url+: default RUNTIME_API_URL, then https://api.withruntime.com.
@@ -82,6 +82,7 @@ module WithRuntime
82
82
  @builds = Builds.new(@transport)
83
83
  @ci = CI.new(@transport)
84
84
  @containers = Containers.new(@transport)
85
+ @browsers = Browsers.new(@transport)
85
86
  end
86
87
 
87
88
  # Renamed on 6 October 2026, one plural word per product: each old name is the product it became, with a warning,
@@ -117,7 +117,8 @@ module WithRuntime
117
117
  # +runtime.secrets+: a value your sandboxes use without seeing it. Every
118
118
  # sandbox of the organization has an environment variable of the secret's
119
119
  # name holding a placeholder; the egress proxy puts the value into HTTPS
120
- # requests to its hosts. The value is never returned.
120
+ # requests to its hosts. With +exposed: true+, the sandboxes and Apps its
121
+ # +only:+ names by id get the value itself instead. The value is never returned.
121
122
  class Secrets < Product
122
123
  # Stores or replaces a secret for +hosts+ ("api.openai.com", "*.github.com").
123
124
  # With +header:+, that header is set on every request to them, +format:+'s
@@ -139,15 +140,36 @@ module WithRuntime
139
140
  # variable sandboxes see it in, when not the secret's name. Secrets scoped to different
140
141
  # sandboxes may share one, such as one GITHUB_TOKEN per repository; a write that would
141
142
  # put two in one sandbox is refused (409). Absent when replacing: the variable it has stays.
142
- def set(name, value:, hosts:, header: nil, format: nil, rules: nil, only: nil, github_app: nil, git: nil,
143
- api: nil, env: nil)
143
+ #
144
+ # +exposed: true+, with no +hosts:+, gives sandboxes the value itself in the variable instead
145
+ # of a placeholder: for what is not an HTTPS header, such as a database password, an SSH key
146
+ # or a service account's JSON. The value may then be any text of up to 64 KiB, several lines
147
+ # included, and it takes no +header:+, +format:+, +rules:+, +github_app:+, +git:+ or +api:+.
148
+ # It needs +only:+ naming the sandboxes or Apps that get it by id ({ "sandboxes" => [ids] },
149
+ # { "apps" => [App ids] }), never labels or every sandbox; a replacement may leave +only:+
150
+ # out to keep the scope it has. Which machines get it changes only with the value: +scope+
151
+ # is refused for it, so +set+ it again.
152
+ # Code in the sandbox can read it and send it anywhere, and it can be in the sandbox's
153
+ # snapshots and forks; for an HTTPS API, prefer +hosts:+. It is not kept: give it on every
154
+ # write of such a secret. +expected_version:+ writes only if the secret is at that version
155
+ # (its +version+ from +get+ or +list+; 0: it must not exist yet), or raises 409
156
+ # version_conflict with the current one in +details["version"]+.
157
+ def set(name, value:, hosts: nil, header: nil, format: nil, rules: nil, only: nil, github_app: nil, git: nil,
158
+ api: nil, env: nil, exposed: nil, expected_version: nil)
144
159
  body = { "value" => value, "hosts" => hosts, "header" => header, "format" => format, "rules" => rules,
145
- "only" => only, "githubApp" => github_app, "git" => git, "api" => api, "env" => env }.compact
160
+ "only" => only, "githubApp" => github_app, "git" => git, "api" => api, "env" => env,
161
+ "exposed" => exposed, "expectedVersion" => expected_version }.compact
146
162
  Record.new(@t.json("PUT", "/v1/egress-secrets/#{seg(name)}", body: body))
147
163
  end
148
164
 
165
+ # One secret by name: its kind (+exposed+), +version+, variable, hosts and placeholder. Never
166
+ # its value. Raises 404 not_found when there is none.
167
+ def get(name) = Record.new(@t.json("GET", "/v1/egress-secrets/#{seg(name)}"))
168
+
149
169
  # Changes which sandboxes get a secret, without its value: +{}+ is every sandbox of the
150
- # organization again. Returns the secret, with +only+ and +enforced+.
170
+ # organization again. Returns the secret, with +only+ and +enforced+. Refused (400) for a
171
+ # real-value secret (+exposed+), whose machines change only with its value: +set+ it again
172
+ # with +value:+, +exposed: true+ and +only:+.
151
173
  def scope(name, only)
152
174
  Record.new(@t.json("PUT", "/v1/egress-secrets/#{seg(name)}/sandboxes", body: only))
153
175
  end
@@ -159,7 +181,7 @@ module WithRuntime
159
181
  Record.new(@t.json("PUT", "/v1/egress-secrets/#{seg(name)}/rules", body: body))
160
182
  end
161
183
 
162
- # Names, hosts and placeholders. Never values. With +limit:+ (1 to 100)
184
+ # Names, kinds, versions, hosts and placeholders. Never values. With +limit:+ (1 to 100)
163
185
  # or +after:+, one page by name: the secrets named after +after+; the next
164
186
  # page's +after+ is this one's last name. Without, every secret.
165
187
  def list(limit: nil, after: nil)
@@ -353,7 +375,8 @@ module WithRuntime
353
375
  end
354
376
  end
355
377
 
356
- # +runtime.otel+: events (as logs) and CPU and memory (as metrics) pushed to
378
+ # +runtime.otel+: events (as logs), CPU, memory, disk and network (as metrics)
379
+ # and a span for every API call (as traces, with signals including "traces") pushed to
357
380
  # an OpenTelemetry endpoint over OTLP/HTTP.
358
381
  class Otel < Product
359
382
  # +headers+ authenticate to the endpoint and are never shown back.
@@ -152,7 +152,9 @@ module WithRuntime
152
152
  end
153
153
 
154
154
  # Changes name, labels, auto_wake, user ("sandbox" or "root": who later
155
- # commands run as when one names no user), idle_pause_seconds, persistent or
155
+ # commands run as when one names no user), lease_seconds (with no time
156
+ # limit, how far ahead each renewal funds it, 300 to 43200),
157
+ # idle_pause_seconds, persistent or
156
158
  # max_total_cost_micros; fields left out stay as they are, and
157
159
  # +max_total_cost_micros: :remove+ removes the cap.
158
160
  def update(idempotency_key: nil, **settings)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithRuntime
4
- VERSION = "0.20.0"
4
+ VERSION = "0.26.1"
5
5
  end
data/lib/withruntime.rb CHANGED
@@ -37,6 +37,7 @@ require_relative "withruntime/apps"
37
37
  require_relative "withruntime/builds"
38
38
  require_relative "withruntime/ci"
39
39
  require_relative "withruntime/containers"
40
+ require_relative "withruntime/browsers"
40
41
  require_relative "withruntime/network_products"
41
42
  require_relative "withruntime/parity_products"
42
43
  require_relative "withruntime/file_watch"
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.20.0
4
+ version: 0.26.1
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-26 00:00:00.000000000 Z
11
+ date: 2026-10-27 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,
@@ -23,6 +23,7 @@ files:
23
23
  - lib/withruntime/agents.rb
24
24
  - lib/withruntime/api_defaults.rb
25
25
  - lib/withruntime/apps.rb
26
+ - lib/withruntime/browsers.rb
26
27
  - lib/withruntime/builds.rb
27
28
  - lib/withruntime/ci.rb
28
29
  - lib/withruntime/client.rb