withruntime 0.1.0 → 0.3.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 +4 -4
- data/lib/withruntime/api_defaults.rb +17 -0
- data/lib/withruntime/client.rb +44 -10
- data/lib/withruntime/credentials.rb +31 -5
- data/lib/withruntime/errors.rb +9 -4
- data/lib/withruntime/network_products.rb +9 -9
- data/lib/withruntime/parity_products.rb +1 -1
- data/lib/withruntime/products.rb +9 -2
- data/lib/withruntime/sandbox.rb +179 -80
- data/lib/withruntime/tar.rb +218 -33
- data/lib/withruntime/transport.rb +189 -16
- data/lib/withruntime/version.rb +1 -1
- data/lib/withruntime/wallet.rb +68 -0
- data/lib/withruntime.rb +5 -2
- metadata +9 -4
- data/GUIDE.md +0 -406
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53b4e63218b0587cacbd8e0ad8ec583ba4ce9b32ec0b792208b8f70750b5acd5
|
|
4
|
+
data.tar.gz: 3461a8000fc4817de3f06c6f6f40b6e5d29b49312a4e1f9c9dc94a649e91b8ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1bb2aea12c2959b3afcd694ad90124d1c04fcf60201fdece3f5f1264b40bfa3b3f8deec865c5265d806b6e8c389ebacfd4da833648133fdba93b9ece3d7352f
|
|
7
|
+
data.tar.gz: d0238775905bea2408a2d930a402f49a4001bf900c3a8c13acbd511aaf5c486b7b01da8b5f93dfc188e50b696e135b8b92dfd75a7f9c74f5d4dcbdfcce63b668
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WithRuntime
|
|
4
|
+
# Every value this SDK sends the API for a bounded time field when the caller
|
|
5
|
+
# names none, in one place. packages/cloud/tests/sdk-defaults.test.ts holds
|
|
6
|
+
# each inside the range the API's registry gives that field, and the same
|
|
7
|
+
# names in the JavaScript, Python, Go and Java SDKs to theirs. The suffix is
|
|
8
|
+
# the unit; a new default goes here, under a name that test maps to its field.
|
|
9
|
+
module ApiDefaults
|
|
10
|
+
# A server-side wait for a state (GET /v1/sandboxes/{id}?waitFor=).
|
|
11
|
+
WAIT_FOR_TIMEOUT_SECONDS = 60
|
|
12
|
+
# A streamed exec's command limit when none is given: the API's longest.
|
|
13
|
+
STREAMED_EXEC_TIMEOUT_SECONDS = 86_400
|
|
14
|
+
# The time keep_alive keeps ahead of a time limit, and so the most it extends by.
|
|
15
|
+
KEEP_ALIVE_MARGIN_SECONDS = 600
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/withruntime/client.rb
CHANGED
|
@@ -7,25 +7,36 @@ module WithRuntime
|
|
|
7
7
|
# is safe for use from many threads and keeps its connections open.
|
|
8
8
|
#
|
|
9
9
|
# runtime = WithRuntime::Client.new # RUNTIME_API_KEY, or this machine's saved connection
|
|
10
|
-
# sbx = runtime.sandboxes.create
|
|
11
|
-
#
|
|
10
|
+
# sbx = runtime.sandboxes.create
|
|
11
|
+
# sbx.files.write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n")
|
|
12
|
+
# puts sbx.exec("python3 /workspace/invoice.py", check: true).stdout
|
|
12
13
|
# sbx.stop
|
|
13
14
|
#
|
|
14
15
|
# Every product is +runtime.<product>.<verb>+. Money is integer microdollars
|
|
15
16
|
# (1,000,000 is one US dollar).
|
|
17
|
+
# The account's networks: +wireguard+, a tunnel from your own network into
|
|
18
|
+
# your sandboxes.
|
|
19
|
+
Networks = Struct.new(:wireguard)
|
|
20
|
+
|
|
21
|
+
# The day the 6 October 2026 rename stops serving its old names, set when it
|
|
22
|
+
# ships (packages/cloud/src/product-names.ts); nil until then.
|
|
23
|
+
RENAME_RETIRES = nil
|
|
24
|
+
|
|
16
25
|
class Client
|
|
17
26
|
attr_reader :transport, :sandboxes, :snapshots, :images, :volumes, :secrets, :limits, :referrals,
|
|
18
|
-
:switching, :events, :webhooks, :otel, :audit, :feedback, :support, :domains, :ports, :
|
|
27
|
+
:switching, :events, :webhooks, :otel, :audit, :feedback, :support, :domains, :ports, :ips, :networks, :sso, :mcp
|
|
19
28
|
|
|
20
29
|
# +api_key+: default RUNTIME_API_KEY, then this machine's saved connection.
|
|
21
30
|
# +base_url+: default RUNTIME_API_URL, then https://api.withruntime.com.
|
|
31
|
+
# Inside a Runtime sandbox, calls for https://api.withruntime.com go to
|
|
32
|
+
# http://runtime.internal.
|
|
22
33
|
# +timeout+: seconds for each call, retries included (300).
|
|
23
34
|
# +max_retries+: of transport failures, 429, 502, 503 and 504 (4).
|
|
24
35
|
# +max_connections+: calls in flight at once; streams do not count (32).
|
|
25
|
-
# +wait_for_capacity+: seconds a sandbox create keeps retrying, with the
|
|
26
|
-
# same key and input, when the
|
|
36
|
+
# +wait_for_capacity+: seconds a sandbox create, wake or restart keeps retrying, with the
|
|
37
|
+
# same key and input, when the slots without credit, quota or region are full (120; 0 fails at once).
|
|
27
38
|
def initialize(api_key: nil, base_url: nil, timeout: 300, max_retries: 4, max_connections: 32,
|
|
28
|
-
wait_for_capacity: 120, env: ENV)
|
|
39
|
+
wait_for_capacity: 120, env: ENV, in_sandbox: -> { Credentials.in_runtime_sandbox? })
|
|
29
40
|
origin = Credentials.origin(base_url || env["RUNTIME_API_URL"].then { |url| url.nil? || url.empty? ? DEFAULT_BASE_URL : url })
|
|
30
41
|
key = api_key
|
|
31
42
|
key ||= env["RUNTIME_API_KEY"] unless env["RUNTIME_API_KEY"].to_s.empty?
|
|
@@ -34,7 +45,10 @@ module WithRuntime
|
|
|
34
45
|
raise ArgumentError, "The Runtime API key contains whitespace." if key.match?(/\s/)
|
|
35
46
|
raise ArgumentError, "max_retries cannot be negative." if max_retries.negative?
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
# The saved key is found by the origin as given; the calls go where that
|
|
49
|
+
# origin is reachable from here.
|
|
50
|
+
@transport = Transport.new(api_key: key, base_url: Credentials.reachable(origin, in_sandbox: in_sandbox),
|
|
51
|
+
timeout: timeout, max_retries: max_retries,
|
|
38
52
|
max_connections: max_connections, wait_for_capacity: [0, wait_for_capacity].max)
|
|
39
53
|
@sandboxes = Sandboxes.new(@transport)
|
|
40
54
|
@snapshots = Snapshots.new(@transport)
|
|
@@ -52,18 +66,23 @@ module WithRuntime
|
|
|
52
66
|
@support = Support.new(@transport)
|
|
53
67
|
@domains = Domains.new(@transport)
|
|
54
68
|
@ports = Ports.new(@transport)
|
|
55
|
-
@
|
|
56
|
-
@
|
|
69
|
+
@ips = Addresses.new(@transport)
|
|
70
|
+
@networks = Networks.new(PrivateTunnel.new(@transport))
|
|
57
71
|
@sso = SSO.new(@transport)
|
|
58
72
|
@mcp = MCP.new(@transport)
|
|
59
73
|
end
|
|
60
74
|
|
|
75
|
+
# Renamed on 6 October 2026, one plural word per product: each old name is the product it became, with a warning,
|
|
76
|
+
# until RENAME_RETIRES, then raises name_retired.
|
|
77
|
+
def addresses = renamed("addresses", "ips") { @ips }
|
|
78
|
+
def tunnel = renamed("tunnel", "networks.wireguard") { @networks.wireguard }
|
|
79
|
+
|
|
61
80
|
def base_url = @transport.base_url
|
|
62
81
|
|
|
63
82
|
# Who this key is: organization, agent and credential.
|
|
64
83
|
def me = Record.new(@transport.json("GET", "/v1/me"))
|
|
65
84
|
|
|
66
|
-
# The account's credit, holds,
|
|
85
|
+
# The account's credit, holds, free time and per-resource charges.
|
|
67
86
|
def usage = Record.new(@transport.json("GET", "/v1/usage"))
|
|
68
87
|
|
|
69
88
|
# Calls any API path with this client's key, retries, idempotency keys and
|
|
@@ -75,5 +94,20 @@ module WithRuntime
|
|
|
75
94
|
body = {} if body.nil? && !%w[GET DELETE].include?(method)
|
|
76
95
|
@transport.json(method, path, query: query, body: body)
|
|
77
96
|
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def renamed(old, new, retires: RENAME_RETIRES, now: Time.now.utc)
|
|
101
|
+
if retires && now >= Time.utc(*retires.split("-").map(&:to_i))
|
|
102
|
+
raise Error.new("runtime.#{old} was renamed runtime.#{new} and stopped working on #{retires}.",
|
|
103
|
+
code: "name_retired", status: 410, hint: "Use runtime.#{new}.")
|
|
104
|
+
end
|
|
105
|
+
@renamed_said ||= {}
|
|
106
|
+
unless @renamed_said[old]
|
|
107
|
+
@renamed_said[old] = true
|
|
108
|
+
warn("Runtime: runtime.#{old} is now runtime.#{new}.#{retires ? " It stops working on #{retires}." : ""}")
|
|
109
|
+
end
|
|
110
|
+
yield
|
|
111
|
+
end
|
|
78
112
|
end
|
|
79
113
|
end
|
|
@@ -12,22 +12,48 @@ module WithRuntime
|
|
|
12
12
|
module Credentials
|
|
13
13
|
KEY = /\Artcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}\z/
|
|
14
14
|
|
|
15
|
+
BAD_ORIGIN = "Use an HTTPS API origin (or http://runtime.internal inside a sandbox, http://localhost for tests)."
|
|
16
|
+
# Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
17
|
+
# own host sends each request on to the public API over HTTPS. The API runs
|
|
18
|
+
# on that host, whose addresses a sandbox cannot reach directly. Plain HTTP
|
|
19
|
+
# because the hop never leaves the machine: from the program to the guest's
|
|
20
|
+
# own proxy, then over the sandbox's private channel to its host.
|
|
21
|
+
SANDBOX_BASE_URL = "http://runtime.internal"
|
|
22
|
+
# A file every Runtime sandbox has; the guest keeps it current.
|
|
23
|
+
SANDBOX_MARKER = "/run/runtime/environment.json"
|
|
24
|
+
|
|
15
25
|
module_function
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
def in_runtime_sandbox?(marker = SANDBOX_MARKER)
|
|
28
|
+
File.exist?(marker)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The origin calls for +api_origin+ are sent to from here. In a sandbox the
|
|
32
|
+
# public API is its own host, which it cannot reach directly, so calls for
|
|
33
|
+
# it go to runtime.internal; every other origin is left as it is.
|
|
34
|
+
def reachable(api_origin, in_sandbox: -> { in_runtime_sandbox? })
|
|
35
|
+
api_origin == DEFAULT_BASE_URL && in_sandbox.call ? SANDBOX_BASE_URL : api_origin
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# An HTTPS origin, plain HTTP to runtime.internal inside a sandbox, or plain
|
|
39
|
+
# HTTP to localhost for tests, with nothing after the host. runtime.internal
|
|
40
|
+
# is reserved and never resolves outside a sandbox, so a key sent there in
|
|
41
|
+
# plain HTTP never leaves the sandbox's host.
|
|
18
42
|
def origin(value)
|
|
19
43
|
uri = URI(value)
|
|
20
|
-
|
|
44
|
+
internal = uri.host == "runtime.internal"
|
|
45
|
+
local = %w[localhost 127.0.0.1 [::1] ::1].include?(uri.host) || internal
|
|
21
46
|
unless uri.host && (uri.scheme == "https" || (local && uri.scheme == "http")) && uri.userinfo.nil? &&
|
|
22
|
-
uri.query.nil? && uri.fragment.nil? && ["", "/"].include?(uri.path.to_s)
|
|
23
|
-
|
|
47
|
+
uri.query.nil? && uri.fragment.nil? && ["", "/"].include?(uri.path.to_s) &&
|
|
48
|
+
!(internal && (uri.scheme != "http" || uri.port != 80))
|
|
49
|
+
raise ArgumentError, BAD_ORIGIN
|
|
24
50
|
end
|
|
25
51
|
|
|
26
52
|
default_port = uri.scheme == "https" ? 443 : 80
|
|
27
53
|
host = uri.host.include?(":") && !uri.host.start_with?("[") ? "[#{uri.host}]" : uri.host
|
|
28
54
|
"#{uri.scheme}://#{host}#{uri.port == default_port ? "" : ":#{uri.port}"}"
|
|
29
55
|
rescue URI::InvalidURIError
|
|
30
|
-
raise ArgumentError,
|
|
56
|
+
raise ArgumentError, BAD_ORIGIN
|
|
31
57
|
end
|
|
32
58
|
|
|
33
59
|
# Where the CLI keeps this machine's connection for the two origins.
|
data/lib/withruntime/errors.rb
CHANGED
|
@@ -7,11 +7,16 @@ module WithRuntime
|
|
|
7
7
|
# Java SDKs.
|
|
8
8
|
class Error < StandardError
|
|
9
9
|
# 503s that are a deliberate state, not a passing one: retrying cannot change them.
|
|
10
|
-
DELIBERATE = %w[
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
DELIBERATE = %w[
|
|
11
|
+
unavailable unsupported fork_unavailable previews_unavailable
|
|
12
|
+
network_unavailable network_rules_unavailable secrets_unavailable identity_unavailable env_unavailable
|
|
13
|
+
resize_unavailable
|
|
14
|
+
].freeze
|
|
15
|
+
# Refusals that pass on their own: a host frees room, a sandbox of an
|
|
16
|
+
# account without credit stops.
|
|
17
|
+
PASSING = %w[no_capacity no_credit_running_limit no_credit_total_limit].freeze
|
|
13
18
|
# Refusals of a create that clear when a sandbox stops or a host frees room.
|
|
14
|
-
WAITS_FOR_ROOM = %w[
|
|
19
|
+
WAITS_FOR_ROOM = %w[no_credit_running_limit no_credit_total_limit no_credit_domain_limit no_credit_capacity_full quota_exceeded no_capacity volume_releasing].freeze
|
|
15
20
|
|
|
16
21
|
attr_reader :code, :status, :hint, :request_id, :details, :idempotency_key, :retry_after
|
|
17
22
|
|
|
@@ -20,24 +20,24 @@ module WithRuntime
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
class Addresses < Product
|
|
23
|
-
def reserve(family: 4) = Record.new(@t.json("POST", "/v1/
|
|
24
|
-
def list = @t.json("GET", "/v1/
|
|
25
|
-
def release(id) = @t.json("DELETE", "/v1/
|
|
23
|
+
def reserve(family: 4) = Record.new(@t.json("POST", "/v1/ips", body: { "family" => family }))
|
|
24
|
+
def list = @t.json("GET", "/v1/ips")["data"].map { |row| Record.new(row) }
|
|
25
|
+
def release(id) = @t.json("DELETE", "/v1/ips/#{seg(id)}") && nil
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Account WireGuard networking, distinct from Sandbox#open_tunnel.
|
|
29
29
|
class PrivateTunnel < Product
|
|
30
|
-
def get = Record.new(@t.json("GET", "/v1/
|
|
31
|
-
def create(subnet: nil) = Record.new(@t.json("POST", "/v1/
|
|
32
|
-
def delete = @t.json("DELETE", "/v1/
|
|
30
|
+
def get = Record.new(@t.json("GET", "/v1/networks/wireguard"))
|
|
31
|
+
def create(subnet: nil) = Record.new(@t.json("POST", "/v1/networks/wireguard", body: { "subnet" => subnet }.compact))
|
|
32
|
+
def delete = @t.json("DELETE", "/v1/networks/wireguard") && nil
|
|
33
33
|
# Save config even when config_ready is false: its private key is shown once.
|
|
34
34
|
def add_peer(name:, public_key: nil, routes: nil, idempotency_key: nil)
|
|
35
|
-
Record.new(@t.json("POST", "/v1/
|
|
35
|
+
Record.new(@t.json("POST", "/v1/networks/wireguard/peers", body: Fields.body(name: name, public_key: public_key, routes: routes), key: idempotency_key, no_retry: public_key.to_s.empty?))
|
|
36
36
|
end
|
|
37
37
|
def rotate_peer(id, public_key: nil)
|
|
38
|
-
Record.new(@t.json("POST", "/v1/
|
|
38
|
+
Record.new(@t.json("POST", "/v1/networks/wireguard/peers/#{seg(id)}:rotate", body: { "publicKey" => public_key }.compact, no_retry: public_key.to_s.empty?))
|
|
39
39
|
end
|
|
40
|
-
def remove_peer(id) = Record.new(@t.json("DELETE", "/v1/
|
|
40
|
+
def remove_peer(id) = Record.new(@t.json("DELETE", "/v1/networks/wireguard/peers/#{seg(id)}"))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
class SSO < Product
|
|
@@ -45,7 +45,7 @@ module WithRuntime
|
|
|
45
45
|
def get(id) = Record.new(@t.json("GET", path(id)))
|
|
46
46
|
def list = @t.json("GET", path)["data"].map { |row| Record.new(row) }
|
|
47
47
|
def stop(id) = Record.new(@t.json("POST", "#{path(id)}:stop", body: {}))
|
|
48
|
-
def download(id) = @t.
|
|
48
|
+
def download(id) = @t.file_bytes("#{path(id)}/video", accept: "video/mp4")
|
|
49
49
|
def delete(id) = @t.json("DELETE", path(id)) && nil
|
|
50
50
|
end
|
|
51
51
|
end
|
data/lib/withruntime/products.rb
CHANGED
|
@@ -71,6 +71,12 @@ module WithRuntime
|
|
|
71
71
|
|
|
72
72
|
def list(state: nil, name: nil, limit: nil) = page("/v1/volumes", { "state" => state, "name" => name, "limit" => limit })
|
|
73
73
|
|
|
74
|
+
# Grows a volume no sandbox holds to +size_mib+, on its host, and waits
|
|
75
|
+
# (up to +wait+ seconds) for it. The answer's +resize+ shows how far it got.
|
|
76
|
+
def resize(id, size_mib:, wait: 10, idempotency_key: nil)
|
|
77
|
+
Record.new(@t.json("POST", "/v1/volumes/#{seg(id)}:resize", body: Fields.body(size_mib: size_mib), wait: wait, key: idempotency_key))
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
# Deletes a volume and everything on it.
|
|
75
81
|
def delete(id) = Record.new(@t.json("POST", "/v1/volumes/#{seg(id)}:delete", body: {}))
|
|
76
82
|
end
|
|
@@ -98,12 +104,13 @@ module WithRuntime
|
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
|
|
101
|
-
# +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.
|
|
102
108
|
class Limits < Product
|
|
103
109
|
def get = Record.new(@t.json("GET", "/v1/limits"))
|
|
104
110
|
end
|
|
105
111
|
|
|
106
|
-
# +runtime.referrals+: the account's referral link and what it earned.
|
|
112
|
+
# +runtime.referrals+: the account's referral link and what it earned. Money is
|
|
113
|
+
# integer microdollars in strings.
|
|
107
114
|
class Referrals < Product
|
|
108
115
|
def get = Record.new(@t.json("GET", "/v1/referrals"))
|
|
109
116
|
end
|