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 +4 -4
- data/lib/withruntime/agents.rb +175 -10
- data/lib/withruntime/apps.rb +1 -1
- data/lib/withruntime/browsers.rb +104 -0
- data/lib/withruntime/builds.rb +3 -1
- data/lib/withruntime/ci.rb +7 -4
- data/lib/withruntime/client.rb +2 -1
- data/lib/withruntime/products.rb +30 -7
- data/lib/withruntime/sandbox.rb +3 -1
- data/lib/withruntime/version.rb +1 -1
- data/lib/withruntime.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c6510ac6423ac945d6e5f55779d4f913ec4c13a4bd64992ec825637219d1373
|
|
4
|
+
data.tar.gz: dbfd02ae2ffe411a32158234ab0d0faf8cf52263149c763cba750aed4db23a94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae5ff418881522c0748184c85262a960e4425d6eb32526482178bde4c862a682580140f373e3858dd66039857117fc73eaf4c9ea0ba7b057bb5c7932d5010952
|
|
7
|
+
data.tar.gz: 6be184e2e55d835096df364ae1077b615485ddad59f6afe87b65c2ab6793a291962151720fbae4d223fb5c6ff5752ff08c5e32bd654229b33c58681fb1f639d6
|
data/lib/withruntime/agents.rb
CHANGED
|
@@ -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
|
-
# +
|
|
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)
|
|
99
|
-
#
|
|
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.
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
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" => {
|
data/lib/withruntime/apps.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/withruntime/builds.rb
CHANGED
|
@@ -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,
|
|
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.
|
data/lib/withruntime/ci.rb
CHANGED
|
@@ -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; +
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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).
|
data/lib/withruntime/client.rb
CHANGED
|
@@ -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,
|
data/lib/withruntime/products.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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)
|
|
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.
|
data/lib/withruntime/sandbox.rb
CHANGED
|
@@ -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),
|
|
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)
|
data/lib/withruntime/version.rb
CHANGED
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.
|
|
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-
|
|
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
|