runtime-sdk 0.4.49__py3-none-win_amd64.whl
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.
- runtime_sdk/__init__.py +18 -0
- runtime_sdk/account.py +243 -0
- runtime_sdk/arguments.py +10 -0
- runtime_sdk/auth.py +175 -0
- runtime_sdk/cli.py +985 -0
- runtime_sdk/client.py +616 -0
- runtime_sdk/completion.py +348 -0
- runtime_sdk/computers.py +578 -0
- runtime_sdk/config.py +201 -0
- runtime_sdk/console.py +118 -0
- runtime_sdk/files.py +233 -0
- runtime_sdk/github.py +229 -0
- runtime_sdk/goals.py +368 -0
- runtime_sdk/output.py +69 -0
- runtime_sdk/providers.py +422 -0
- runtime_sdk/proxy.py +600 -0
- runtime_sdk/runtime-tui.exe +0 -0
- runtime_sdk/secrets.py +155 -0
- runtime_sdk/services.py +173 -0
- runtime_sdk/ssh.py +197 -0
- runtime_sdk/terminal.py +330 -0
- runtime_sdk-0.4.49.dist-info/METADATA +273 -0
- runtime_sdk-0.4.49.dist-info/RECORD +26 -0
- runtime_sdk-0.4.49.dist-info/WHEEL +5 -0
- runtime_sdk-0.4.49.dist-info/entry_points.txt +2 -0
- runtime_sdk-0.4.49.dist-info/top_level.txt +1 -0
runtime_sdk/terminal.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import struct
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urljoin, urlparse
|
|
12
|
+
|
|
13
|
+
from websockets.exceptions import ConnectionClosed, InvalidHandshake
|
|
14
|
+
from websockets.sync.client import connect as websocket_connect
|
|
15
|
+
|
|
16
|
+
from runtime_sdk import console, output as cli_output
|
|
17
|
+
from runtime_sdk.auth import authenticated_client
|
|
18
|
+
from runtime_sdk.client import DEFAULT_WARMUP_TIMEOUT, RuntimeAPIError
|
|
19
|
+
from runtime_sdk.config import RuntimeConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_ws_url(base_url: str, ws_url: Any) -> str:
|
|
23
|
+
text = str(ws_url or "").strip()
|
|
24
|
+
if not text:
|
|
25
|
+
return ""
|
|
26
|
+
if text.startswith("ws://") or text.startswith("wss://"):
|
|
27
|
+
return text
|
|
28
|
+
|
|
29
|
+
parsed = urlparse(base_url)
|
|
30
|
+
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
31
|
+
root = f"{scheme}://{parsed.netloc}"
|
|
32
|
+
if text.startswith("/"):
|
|
33
|
+
return root + text
|
|
34
|
+
return urljoin(root + "/", text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def strip_controls(text: str) -> str:
|
|
38
|
+
text = re.sub(r"\x1b\].*?(?:\x07|\x1b\\)", "", text, flags=re.S)
|
|
39
|
+
text = re.sub(r"\x1bP.*?\x1b\\", "", text, flags=re.S)
|
|
40
|
+
text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
|
|
41
|
+
text = re.sub(r"\x1b[@-Z\\-_]", "", text)
|
|
42
|
+
text = "".join(ch for ch in text if ch in "\n\r\t" or ord(ch) >= 32)
|
|
43
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enter_computer(
|
|
47
|
+
config: RuntimeConfig, computer_id: str, *, command: str | None = None
|
|
48
|
+
) -> int:
|
|
49
|
+
client = authenticated_client(config)
|
|
50
|
+
ticket_payload = client.create_terminal_ticket(computer_id)
|
|
51
|
+
ws_url = resolve_ws_url(config.base_url, ticket_payload.get("ws_url"))
|
|
52
|
+
if not ws_url:
|
|
53
|
+
return cli_output.report_error(
|
|
54
|
+
"terminal endpoint did not return a websocket url"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
print("Connecting to console...")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
return _run_terminal_session(ws_url, bootstrap_command=command)
|
|
61
|
+
except RuntimeAPIError as exc:
|
|
62
|
+
return cli_output.report_error(str(exc), status_code=exc.status_code)
|
|
63
|
+
except (InvalidHandshake, TimeoutError, OSError) as exc:
|
|
64
|
+
return cli_output.report_error(f"terminal failed: {exc}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
TERMINAL_FRAME_DATA = 0x01
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
TERMINAL_FRAME_WINDOW_SIZE = 0x02
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
TERMINAL_FRAME_SHUTDOWN = 0x03
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
TERMINAL_FRAME_HEARTBEAT = 0x04
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
TERMINAL_FRAME_SESSION_CLOSE = 0x05
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
TERMINAL_FRAME_HEADER_SIZE = 5
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
TERMINAL_HEARTBEAT_INTERVAL = 15.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _encode_terminal_frame(frame_type: int, payload: bytes = b"") -> bytes:
|
|
89
|
+
return bytes([frame_type]) + struct.pack(">I", len(payload)) + payload
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_terminal_frame(raw: bytes) -> tuple[int, bytes]:
|
|
93
|
+
if len(raw) < TERMINAL_FRAME_HEADER_SIZE:
|
|
94
|
+
raise RuntimeAPIError("terminal received a truncated frame")
|
|
95
|
+
payload_len = struct.unpack(">I", raw[1:TERMINAL_FRAME_HEADER_SIZE])[0]
|
|
96
|
+
if len(raw) != TERMINAL_FRAME_HEADER_SIZE + payload_len:
|
|
97
|
+
raise RuntimeAPIError("terminal received a malformed frame")
|
|
98
|
+
return raw[0], raw[TERMINAL_FRAME_HEADER_SIZE:]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_terminal_session(ws_url: str, *, bootstrap_command: str | None = None) -> int:
|
|
102
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
103
|
+
return cli_output.report_error("enter requires an interactive terminal")
|
|
104
|
+
|
|
105
|
+
fd = sys.stdin.fileno()
|
|
106
|
+
stop = threading.Event()
|
|
107
|
+
result: dict[str, Any] = {"code": 0, "error": None}
|
|
108
|
+
bootstrap_echo = (
|
|
109
|
+
f"exec {bootstrap_command.strip()}"
|
|
110
|
+
if bootstrap_command and bootstrap_command.strip()
|
|
111
|
+
else ""
|
|
112
|
+
)
|
|
113
|
+
output_state: dict[str, Any] = {
|
|
114
|
+
"buffer": "",
|
|
115
|
+
"suppressing_preamble": True,
|
|
116
|
+
"bootstrap_echo": bootstrap_echo,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
with websocket_connect(
|
|
120
|
+
ws_url, open_timeout=DEFAULT_WARMUP_TIMEOUT, close_timeout=1
|
|
121
|
+
) as ws:
|
|
122
|
+
_send_terminal_resize(ws)
|
|
123
|
+
_send_terminal_bootstrap(ws, bootstrap_command)
|
|
124
|
+
|
|
125
|
+
def recv_loop() -> None:
|
|
126
|
+
try:
|
|
127
|
+
while not stop.is_set():
|
|
128
|
+
raw = ws.recv()
|
|
129
|
+
if raw is None:
|
|
130
|
+
result["code"] = 0
|
|
131
|
+
stop.set()
|
|
132
|
+
return
|
|
133
|
+
if not isinstance(raw, bytes):
|
|
134
|
+
raise RuntimeAPIError("terminal received a non-binary frame")
|
|
135
|
+
frame_type, payload = _parse_terminal_frame(raw)
|
|
136
|
+
if frame_type == TERMINAL_FRAME_HEARTBEAT:
|
|
137
|
+
continue
|
|
138
|
+
if frame_type == TERMINAL_FRAME_DATA:
|
|
139
|
+
data = _filter_terminal_output(
|
|
140
|
+
payload.decode("utf-8", errors="ignore"), output_state
|
|
141
|
+
)
|
|
142
|
+
if data:
|
|
143
|
+
sys.stdout.write(data)
|
|
144
|
+
sys.stdout.flush()
|
|
145
|
+
continue
|
|
146
|
+
if frame_type in (
|
|
147
|
+
TERMINAL_FRAME_SHUTDOWN,
|
|
148
|
+
TERMINAL_FRAME_SESSION_CLOSE,
|
|
149
|
+
):
|
|
150
|
+
stop.set()
|
|
151
|
+
return
|
|
152
|
+
except ConnectionClosed:
|
|
153
|
+
if (
|
|
154
|
+
not stop.is_set()
|
|
155
|
+
and not result.get("error")
|
|
156
|
+
and int(result.get("code") or 0) == 0
|
|
157
|
+
):
|
|
158
|
+
result["code"] = 1
|
|
159
|
+
result["error"] = "terminal connection closed unexpectedly"
|
|
160
|
+
stop.set()
|
|
161
|
+
except (
|
|
162
|
+
Exception
|
|
163
|
+
) as exc: # pragma: no cover - defensive for real TTY sessions
|
|
164
|
+
result["code"] = 1
|
|
165
|
+
result["error"] = str(exc)
|
|
166
|
+
stop.set()
|
|
167
|
+
|
|
168
|
+
recv_thread = threading.Thread(target=recv_loop, daemon=True)
|
|
169
|
+
recv_thread.start()
|
|
170
|
+
|
|
171
|
+
last_size = shutil.get_terminal_size((100, 24))
|
|
172
|
+
last_heartbeat = time.monotonic()
|
|
173
|
+
|
|
174
|
+
def mark_connection_closed() -> None:
|
|
175
|
+
if (
|
|
176
|
+
not stop.is_set()
|
|
177
|
+
and not result.get("error")
|
|
178
|
+
and int(result.get("code") or 0) == 0
|
|
179
|
+
):
|
|
180
|
+
result["code"] = 1
|
|
181
|
+
result["error"] = "terminal connection closed unexpectedly"
|
|
182
|
+
stop.set()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
with console.raw_terminal_input(fd) as read_input:
|
|
186
|
+
while not stop.is_set():
|
|
187
|
+
current_size = shutil.get_terminal_size((100, 24))
|
|
188
|
+
if current_size != last_size:
|
|
189
|
+
_send_terminal_resize(ws)
|
|
190
|
+
last_size = current_size
|
|
191
|
+
|
|
192
|
+
now = time.monotonic()
|
|
193
|
+
if now - last_heartbeat >= TERMINAL_HEARTBEAT_INTERVAL:
|
|
194
|
+
try:
|
|
195
|
+
_send_terminal_heartbeat(ws)
|
|
196
|
+
except ConnectionClosed:
|
|
197
|
+
mark_connection_closed()
|
|
198
|
+
break
|
|
199
|
+
last_heartbeat = now
|
|
200
|
+
|
|
201
|
+
data = read_input(0.1)
|
|
202
|
+
if data is None:
|
|
203
|
+
continue
|
|
204
|
+
if not data:
|
|
205
|
+
stop.set()
|
|
206
|
+
break
|
|
207
|
+
try:
|
|
208
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_DATA, data))
|
|
209
|
+
except ConnectionClosed:
|
|
210
|
+
mark_connection_closed()
|
|
211
|
+
break
|
|
212
|
+
finally:
|
|
213
|
+
stop.set()
|
|
214
|
+
with contextlib.suppress(Exception):
|
|
215
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_SHUTDOWN))
|
|
216
|
+
with contextlib.suppress(Exception):
|
|
217
|
+
ws.close()
|
|
218
|
+
recv_thread.join(timeout=1)
|
|
219
|
+
|
|
220
|
+
sys.stdout.write("\n")
|
|
221
|
+
sys.stdout.flush()
|
|
222
|
+
tail = _flush_terminal_output(output_state)
|
|
223
|
+
if tail:
|
|
224
|
+
sys.stdout.write(tail)
|
|
225
|
+
sys.stdout.flush()
|
|
226
|
+
if result.get("error"):
|
|
227
|
+
print(str(result["error"]), file=sys.stderr)
|
|
228
|
+
return int(result.get("code") or 0)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _filter_terminal_output(chunk: str, state: dict[str, Any]) -> str:
|
|
232
|
+
if not chunk:
|
|
233
|
+
return ""
|
|
234
|
+
if not state.get("suppressing_preamble", False):
|
|
235
|
+
return _strip_terminal_noise(chunk, str(state.get("bootstrap_echo") or ""))
|
|
236
|
+
|
|
237
|
+
buffer = str(state.get("buffer") or "") + chunk
|
|
238
|
+
marker = "\x1b[1;34mruntime@"
|
|
239
|
+
idx = buffer.find(marker)
|
|
240
|
+
if idx != -1:
|
|
241
|
+
state["suppressing_preamble"] = False
|
|
242
|
+
state["buffer"] = ""
|
|
243
|
+
return _strip_terminal_noise(
|
|
244
|
+
buffer[idx:], str(state.get("bootstrap_echo") or "")
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
cleaned = _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
248
|
+
if cleaned and _should_release_terminal_output(cleaned):
|
|
249
|
+
state["suppressing_preamble"] = False
|
|
250
|
+
state["buffer"] = ""
|
|
251
|
+
return cleaned
|
|
252
|
+
|
|
253
|
+
if len(buffer) > 8192:
|
|
254
|
+
state["suppressing_preamble"] = False
|
|
255
|
+
state["buffer"] = ""
|
|
256
|
+
return _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
257
|
+
|
|
258
|
+
state["buffer"] = buffer
|
|
259
|
+
return ""
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _should_release_terminal_output(text: str) -> bool:
|
|
263
|
+
if not text:
|
|
264
|
+
return False
|
|
265
|
+
if "\n" in text or "\r" in text:
|
|
266
|
+
return True
|
|
267
|
+
if len(text) >= 256:
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
stripped = text.rstrip()
|
|
271
|
+
return text.endswith(("$ ", "# ", "% ", "> ")) or stripped.endswith(
|
|
272
|
+
("$", "#", "%", ">")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _flush_terminal_output(state: dict[str, Any]) -> str:
|
|
277
|
+
buffer = str(state.get("buffer") or "")
|
|
278
|
+
state["buffer"] = ""
|
|
279
|
+
return _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _strip_terminal_noise(text: str, bootstrap_echo: str = "") -> str:
|
|
286
|
+
if not text:
|
|
287
|
+
return ""
|
|
288
|
+
|
|
289
|
+
lines = text.splitlines(keepends=True)
|
|
290
|
+
filtered: list[str] = []
|
|
291
|
+
for line in lines:
|
|
292
|
+
stripped = line.rstrip("\r\n")
|
|
293
|
+
if stripped.startswith(
|
|
294
|
+
"Running bootstrap command: export PS1='\\[\\033[1;34m\\]runtime@"
|
|
295
|
+
):
|
|
296
|
+
continue
|
|
297
|
+
if stripped == "Connected! Press Ctrl+] to exit.":
|
|
298
|
+
continue
|
|
299
|
+
if _is_terminal_bootstrap_echo(stripped, bootstrap_echo):
|
|
300
|
+
continue
|
|
301
|
+
filtered.append(line)
|
|
302
|
+
return "".join(filtered)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_terminal_bootstrap_echo(line: str, bootstrap_echo: str) -> bool:
|
|
306
|
+
bootstrap_echo = bootstrap_echo.strip()
|
|
307
|
+
if not bootstrap_echo:
|
|
308
|
+
return False
|
|
309
|
+
clean = ANSI_ESCAPE_RE.sub("", line).strip()
|
|
310
|
+
return clean == bootstrap_echo or clean.endswith(bootstrap_echo)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _send_terminal_bootstrap(ws: Any, command: str | None) -> None:
|
|
314
|
+
command = (command or "").strip()
|
|
315
|
+
if not command:
|
|
316
|
+
return
|
|
317
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_DATA, f"exec {command}\n".encode()))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _send_terminal_resize(ws: Any) -> None:
|
|
321
|
+
size = shutil.get_terminal_size((100, 24))
|
|
322
|
+
ws.send(
|
|
323
|
+
_encode_terminal_frame(
|
|
324
|
+
TERMINAL_FRAME_WINDOW_SIZE, struct.pack(">II", size.columns, size.lines)
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _send_terminal_heartbeat(ws: Any) -> None:
|
|
330
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_HEARTBEAT))
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runtime-sdk
|
|
3
|
+
Version: 0.4.49
|
|
4
|
+
Summary: Runtime Python SDK and CLI
|
|
5
|
+
Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
|
|
6
|
+
Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.28.1
|
|
20
|
+
Requires-Dist: websockets>=15.0
|
|
21
|
+
|
|
22
|
+
# runtime-sdk
|
|
23
|
+
|
|
24
|
+
Python SDK and CLI for Runtime.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Supported host OS for scriptable CLI commands: **Windows, macOS, and Linux**.
|
|
29
|
+
|
|
30
|
+
The interactive dashboard ships in native wheels for Windows ARM64/x64,
|
|
31
|
+
macOS 13+ ARM64/x64, and 64-bit Linux with glibc 2.17+. Other supported
|
|
32
|
+
environments receive the universal wheel with the complete scriptable CLI but
|
|
33
|
+
without the OpenTUI dashboard.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install runtime-sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To upgrade an existing install:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool upgrade runtime-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configure
|
|
46
|
+
|
|
47
|
+
The CLI talks to `https://api.runruntime.dev` by default.
|
|
48
|
+
|
|
49
|
+
For local or self-hosted Runtime, override it with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export RUNTIME_BASE_URL=http://127.0.0.1:8080
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or pass `--base-url` per command.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Auth
|
|
61
|
+
runtime # open the interactive dashboard; sign in first if needed
|
|
62
|
+
runtime login # WorkOS device login; opens the verification page
|
|
63
|
+
runtime login --no-open # print the URL and code for a browserless host
|
|
64
|
+
runtime whoami
|
|
65
|
+
runtime logout
|
|
66
|
+
runtime login --api-key sk_... # explicit headless credential mode
|
|
67
|
+
# Or set RUNTIME_API_KEY for one process without saving it.
|
|
68
|
+
runtime api-keys list
|
|
69
|
+
runtime api-keys create "my laptop"
|
|
70
|
+
runtime api-keys revoke key_01...
|
|
71
|
+
runtime integrate github
|
|
72
|
+
runtime github list
|
|
73
|
+
runtime github disconnect acme
|
|
74
|
+
|
|
75
|
+
# Computers
|
|
76
|
+
runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
|
|
77
|
+
runtime switch -c feature/login --repo owner/repo # create a new branch workspace from the repo default branch
|
|
78
|
+
runtime checkout --repo owner/repo feature/login # alias for switch
|
|
79
|
+
runtime create # creates a computer with the starter app already published
|
|
80
|
+
runtime create myapp --command "python3 app.py" --cwd /home/runtime --port 3000
|
|
81
|
+
runtime create locked --network allowlist --allow-host api.example.com
|
|
82
|
+
runtime enter <name-or-id> # open an interactive shell; accepts slug/name or computer id
|
|
83
|
+
runtime enter <name-or-id> -- claude # open a real TTY and run an interactive command
|
|
84
|
+
runtime enter <name-or-id> -- python # use enter for REPLs, agents, prompts, and full-screen apps
|
|
85
|
+
runtime ssh <name-or-id> # open SSH through Runtime's authenticated tunnel
|
|
86
|
+
runtime ssh <name-or-id> -- uptime # run a command with the local ssh client
|
|
87
|
+
runtime list # open the computer dashboard in a terminal
|
|
88
|
+
runtime list --json # one JSON response for scripts and agents
|
|
89
|
+
runtime list --json --watch 2 # newline-delimited JSON updates
|
|
90
|
+
runtime info <id>
|
|
91
|
+
runtime share public <id>
|
|
92
|
+
runtime share private <id>
|
|
93
|
+
runtime start <id>
|
|
94
|
+
runtime run <id> "echo hello" # one-shot foreground command only
|
|
95
|
+
runtime run <id> "apt install -y nodejs" --uid 0
|
|
96
|
+
runtime exec <id> -- bash -lc 'for i in 1 2 3; do echo $i; sleep 1; done'
|
|
97
|
+
printf 'hello' | runtime exec <id> --stdin -- cat
|
|
98
|
+
# Use runtime exec for automation and exact exit codes; use runtime enter -- <cmd> for interactive TTY commands.
|
|
99
|
+
runtime files ls <id> /home/runtime
|
|
100
|
+
runtime files read <id> /home/runtime/app.py --output app.py
|
|
101
|
+
runtime files write <id> /home/runtime/app.py --input app.py --mode 0644
|
|
102
|
+
runtime files mkdir <id> /home/runtime/data --parents
|
|
103
|
+
runtime files upload <id> ./local-app /home/runtime/app
|
|
104
|
+
runtime files download <id> /home/runtime/app ./downloaded-app
|
|
105
|
+
runtime files rm <id> /home/runtime/data --recursive
|
|
106
|
+
runtime startup show <id>
|
|
107
|
+
runtime startup set <id> --command "python3 app.py" --cwd /home/runtime --port 3000
|
|
108
|
+
runtime startup clear <id> # low-level durable service config
|
|
109
|
+
runtime service show <id> # user-facing alias for the durable published app
|
|
110
|
+
runtime service clear <id>
|
|
111
|
+
runtime publish <id> 3000 -- python3 app.py
|
|
112
|
+
runtime delete <id>
|
|
113
|
+
|
|
114
|
+
# Network policy
|
|
115
|
+
runtime network default show
|
|
116
|
+
runtime network default allowlist api.example.com '*.github.com'
|
|
117
|
+
runtime network show <id>
|
|
118
|
+
runtime network denials <id>
|
|
119
|
+
runtime network set <id> none
|
|
120
|
+
runtime network set <id> open
|
|
121
|
+
|
|
122
|
+
# Inside a running computer, the helper installed by Runtime can manage the
|
|
123
|
+
# durable app without leaving the sandbox:
|
|
124
|
+
# runtime-env publish 3000 -- python3 app.py
|
|
125
|
+
# runtime-env service show
|
|
126
|
+
# runtime-env service clear
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Private public URLs authenticate the browser before forwarding traffic. Apps
|
|
130
|
+
behind a private URL receive trusted `X-Runtime-Account-ID`,
|
|
131
|
+
`X-Runtime-User-ID`, and `X-Runtime-User-Email` headers. Runtime strips
|
|
132
|
+
client-supplied versions of those headers from both public and private traffic;
|
|
133
|
+
public URLs receive no Runtime identity headers.
|
|
134
|
+
|
|
135
|
+
## Python
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from runtime_sdk import RuntimeClient
|
|
139
|
+
|
|
140
|
+
client = RuntimeClient(base_url="https://api.runruntime.dev", credential="sk_...")
|
|
141
|
+
|
|
142
|
+
# Create a computer. New computers start with the starter app already published.
|
|
143
|
+
computer = client.create_computer()
|
|
144
|
+
print(computer["public_url"]) # https://goldbird.runruntime.dev
|
|
145
|
+
|
|
146
|
+
# Or create one with an explicit durable app command.
|
|
147
|
+
# That saved service is replayed after cold restore / start.
|
|
148
|
+
app = client.create_computer(
|
|
149
|
+
slug="myapp",
|
|
150
|
+
command="python3 app.py",
|
|
151
|
+
cwd="/home/runtime",
|
|
152
|
+
port=3000,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Account defaults are copied only to computers created later.
|
|
156
|
+
client.set_default_network("allowlist", ["api.example.com"])
|
|
157
|
+
restricted = client.create_computer()
|
|
158
|
+
client.set_computer_network(restricted["id"], "none")
|
|
159
|
+
|
|
160
|
+
# Run a command
|
|
161
|
+
result = client.run_command(computer["id"], "echo hello")
|
|
162
|
+
print(result["stdout"])
|
|
163
|
+
|
|
164
|
+
client.write_file(computer["id"], "/home/runtime/hello.txt", b"hello\n")
|
|
165
|
+
print(client.read_file(computer["id"], "/home/runtime/hello.txt"))
|
|
166
|
+
print(client.list_files(computer["id"], "/home/runtime"))
|
|
167
|
+
|
|
168
|
+
# Wake a cold computer explicitly
|
|
169
|
+
client.start_computer(app["id"])
|
|
170
|
+
|
|
171
|
+
# Promote the running app on a local port to the durable public app.
|
|
172
|
+
# Runtime inspects the listening process and saves its command + cwd when possible.
|
|
173
|
+
client.publish_port(computer["id"], 3000)
|
|
174
|
+
|
|
175
|
+
# List, info, delete
|
|
176
|
+
computers = client.list_computers()
|
|
177
|
+
info = client.get_computer(computer["id"])
|
|
178
|
+
client.delete_computer(computer["id"])
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
For fast local backend iteration:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
make local-backend
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For deploying and testing against the Hetzner production server:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
make sync SERVER_IP=x.x.x.x SSH_USER=root
|
|
193
|
+
make smoke
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Use `make deploy` instead of `make sync` when migrations, env
|
|
197
|
+
files, Caddy, or systemd units changed.
|
|
198
|
+
|
|
199
|
+
Cold restore and explicit `runtime start` replay the saved published app command.
|
|
200
|
+
New computers seed that durable app from the starter workspace. Later,
|
|
201
|
+
`runtime publish <id> <port> -- <command...>` replaces the durable app command
|
|
202
|
+
and starts it. Inside a running computer,
|
|
203
|
+
`runtime-env publish <port> -- <command...>` does the same thing using a
|
|
204
|
+
computer-scoped token installed by Runtime. `runtime service show|clear` and
|
|
205
|
+
`runtime-env service show|clear`
|
|
206
|
+
expose that same durable app state directly. The low-level `runtime startup ...`
|
|
207
|
+
commands still map to the same durable state. A one-off `runtime run` stays
|
|
208
|
+
one-shot: the filesystem is restored after going cold, but that ad-hoc process
|
|
209
|
+
is not.
|
|
210
|
+
|
|
211
|
+
## GitHub branch workspaces
|
|
212
|
+
|
|
213
|
+
`runtime switch` gives a repo branch its own Runtime computer so you can jump
|
|
214
|
+
between parallel tasks without local worktrees.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
runtime integrate github
|
|
218
|
+
runtime github list
|
|
219
|
+
runtime switch --repo owner/repo feature/login
|
|
220
|
+
runtime switch -c feature/search --repo owner/repo --from main
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Rules:
|
|
224
|
+
- GitHub must already be connected with `runtime integrate github`.
|
|
225
|
+
- Run `runtime integrate github` again when you want to add another personal account or org.
|
|
226
|
+
- New GitHub connects require user authorization during installation and
|
|
227
|
+
`GITHUB_APP_CLIENT_ID` / `GITHUB_APP_CLIENT_SECRET` on the Runtime API.
|
|
228
|
+
In GitHub App settings, enable user authorization during installation and set
|
|
229
|
+
the callback URL to `https://api.runruntime.dev/github/callback`.
|
|
230
|
+
- `runtime switch` opens an existing branch workspace, or creates the computer if
|
|
231
|
+
the branch exists but the workspace does not yet.
|
|
232
|
+
- `runtime switch -c` creates a new branch first, then opens that branch's
|
|
233
|
+
workspace.
|
|
234
|
+
- Each `repo + branch` maps to one dedicated computer.
|
|
235
|
+
- The repo is cloned into `/home/runtime/<repo>` and new shells in that computer
|
|
236
|
+
start there automatically.
|
|
237
|
+
- `runtime checkout ...` is an alias for `runtime switch ...`.
|
|
238
|
+
|
|
239
|
+
Run the SDK unit tests through the backend project environment:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv run python -m unittest discover -s scripts/tests -p 'test_*.py'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Release
|
|
246
|
+
|
|
247
|
+
Set and commit the release version first:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
cd backend
|
|
251
|
+
uv version <next-version>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Merge the release version to `main`, then run the Runtime SDK workflow with the
|
|
255
|
+
exact committed version. The workflow tests the Python CLI and OpenTUI, builds
|
|
256
|
+
all platform wheels, runs the installed Windows wheel against production, and
|
|
257
|
+
only then publishes those same artifacts to PyPI.
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
VERSION="$(uv version --short)"
|
|
261
|
+
gh workflow run runtime-sdk.yml --ref main -f version="${VERSION}" -f publish=true
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Preview the runner commands without building:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
./scripts/release_runtime_sdk.sh --dry-run
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`RUNTIME_WINDOWS_SMOKE_API_KEY` is the production smoke credential;
|
|
271
|
+
`UV_PUBLISH_TOKEN` is used only by the publish job after every wheel and the
|
|
272
|
+
Windows production smoke pass. The build script never publishes, changes the
|
|
273
|
+
source version, or loads a local `.env` file.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
runtime_sdk/__init__.py,sha256=_fcdLGAGl00mqptSUWHoPwjgEL8kASrWVMy_tZqUUf0,501
|
|
2
|
+
runtime_sdk/account.py,sha256=2GEG5pZiF1Dh3ZhkpxaAKE0B03RGubVMnIzLwVgbel4,8468
|
|
3
|
+
runtime_sdk/arguments.py,sha256=-zfsxKFoCr-2epM8cSk9g8NltBaRZ-ih3vDNeChWPk0,264
|
|
4
|
+
runtime_sdk/auth.py,sha256=GXuTMaT3an7dv2Fgl8arIi1zSgZ2CDLAskML3X8GxSI,5914
|
|
5
|
+
runtime_sdk/cli.py,sha256=xUKaQBwIizcXt_0l6XiSaDW__aPhT2MwE2x48mTNJ8o,50587
|
|
6
|
+
runtime_sdk/client.py,sha256=Kthr1FjPFMMh2oO_ZzlxAC7OrUmwm8wI2mgo09v1YVY,23250
|
|
7
|
+
runtime_sdk/completion.py,sha256=yaGk0YVYK8i4RLdrqWuJUVndONbp5G173gyPGhEyAlo,13617
|
|
8
|
+
runtime_sdk/computers.py,sha256=ZKRb8jAtsP3mYpeiCWAddLworKqFrS32SykizSdCR7s,20084
|
|
9
|
+
runtime_sdk/config.py,sha256=CdjCDs-5qa12DWrHgqSf0YQOZ5_o3cjYvEzosaEXT-4,6158
|
|
10
|
+
runtime_sdk/console.py,sha256=KvvgmNJzEixNa9SHp9JF2WGRQdrO9bqrtLeU1zoCGus,4035
|
|
11
|
+
runtime_sdk/files.py,sha256=LyZohpK6fjXhSv5X4M2kuGG0y75791ZvPAD_QEjJsRw,8936
|
|
12
|
+
runtime_sdk/github.py,sha256=cPxLNJScawmptCeraMeDbZviDJ1gXo9M9tGSdnIMXYM,8189
|
|
13
|
+
runtime_sdk/goals.py,sha256=FWM9OhIFiFTEujUUSDzkgymgEcAV1LkaM9QNpEO5p4Y,11491
|
|
14
|
+
runtime_sdk/output.py,sha256=NNzG_xpwe91j2bNnIKDj-Z9Xk5Tg8DFHaZD-VXnhVes,2046
|
|
15
|
+
runtime_sdk/providers.py,sha256=TQan5VB63pIxEuhpMxS8d5VoxG_dqlO93aVxu61_LXE,14478
|
|
16
|
+
runtime_sdk/proxy.py,sha256=KJVmoVGJ7btZYmsjnT0JUAd9cjNK98ASvMQED7VG5xU,19744
|
|
17
|
+
runtime_sdk/runtime-tui.exe,sha256=o-3ZwrRicYA8WHSWO2N-7dgY1BwJgRkGXcSNssty_6Y,107655168
|
|
18
|
+
runtime_sdk/secrets.py,sha256=aAXC-4ywbv8kbn9Zl55CicpTRD0wcnFc4lyk8JGAB8s,5900
|
|
19
|
+
runtime_sdk/services.py,sha256=gmGNLa_u9WAoMmAjQWrPaeVvN-zVqT-oxNDp2ftjptQ,6411
|
|
20
|
+
runtime_sdk/ssh.py,sha256=e9fRSkqeVE6DYux_W1yks3YVGX_7Ba7S3rJcfYs4JZ0,6492
|
|
21
|
+
runtime_sdk/terminal.py,sha256=PzPXfTPA5TZb4VFpNxWlt-2Y56XT38wOzFwBEIvGuk8,11030
|
|
22
|
+
runtime_sdk-0.4.49.dist-info/METADATA,sha256=sPhn3uDPsdpA9Xsz58RwRs6P5FXwxzxGkxHpYpx-KZM,10182
|
|
23
|
+
runtime_sdk-0.4.49.dist-info/WHEEL,sha256=hVx9elvUDfBjRmbl8JwIcXXikst35RZTZK9nspfI_28,98
|
|
24
|
+
runtime_sdk-0.4.49.dist-info/entry_points.txt,sha256=Hk_zo0mQTeqWOYb1oTxGnGN-b61_6uKYAt3THED8xeE,49
|
|
25
|
+
runtime_sdk-0.4.49.dist-info/top_level.txt,sha256=RmTYN3o3Mn1j6OozW1LbYaYvy3FjQU3E118nfNv7Ye0,12
|
|
26
|
+
runtime_sdk-0.4.49.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
runtime_sdk
|