runtime-sdk 0.4.10__tar.gz → 0.4.11__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.10
3
+ Version: 0.4.11
4
4
  Summary: Runtime Python SDK and CLI
5
5
  Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
6
6
  Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
@@ -54,16 +54,27 @@ runtime logout
54
54
  runtime login --api-key rt_live_...
55
55
 
56
56
  # Computers
57
- runtime create # in a real TTY, creates then drops you into the console
57
+ runtime create # creates a computer with the starter app already published
58
58
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
59
59
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
60
60
  runtime list
61
61
  runtime info <id>
62
62
  runtime start <id>
63
- runtime run <id> "echo hello"
63
+ runtime run <id> "echo hello" # one-shot foreground command only
64
64
  runtime run <id> "apt install -y nodejs" --uid 0
65
- runtime publish <id> 3000
65
+ runtime startup show <id>
66
+ runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
67
+ runtime startup clear <id> # low-level durable service config
68
+ runtime service show <id> # user-facing alias for the durable published app
69
+ runtime service clear <id>
70
+ runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
66
71
  runtime delete <id>
72
+
73
+ # Inside a running computer, the helper installed by Runtime can manage the
74
+ # durable app without leaving the sandbox:
75
+ # runtime-env publish 3000
76
+ # runtime-env service show
77
+ # runtime-env service clear
67
78
  ```
68
79
 
69
80
  ## Python
@@ -73,12 +84,12 @@ from runtime_sdk import RuntimeClient
73
84
 
74
85
  client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
75
86
 
76
- # Create a computer
87
+ # Create a computer. New computers start with the starter app already published.
77
88
  computer = client.create_computer()
78
89
  print(computer["public_url"]) # https://goldbird.runruntime.dev
79
90
 
80
- # Or create one with a durable startup command.
81
- # That startup config is replayed after cold restore / auto-wake.
91
+ # Or create one with an explicit durable app command.
92
+ # That saved service is replayed after cold restore / start.
82
93
  app = client.create_computer(
83
94
  slug="myapp",
84
95
  command="python3 app.py",
@@ -93,7 +104,8 @@ print(result["stdout"])
93
104
  # Wake a cold computer explicitly
94
105
  client.start_computer(app["id"])
95
106
 
96
- # Pin the public URL to a local app port
107
+ # Promote the running app on a local port to the durable public app.
108
+ # Runtime inspects the listening process and saves its command + cwd when possible.
97
109
  client.publish_port(computer["id"], 3000)
98
110
 
99
111
  # List, info, delete
@@ -120,10 +132,16 @@ make smoke
120
132
  Use `make deploy` instead of `make sync` when migrations, env
121
133
  files, Caddy, or systemd units changed.
122
134
 
123
- Cold restore and public auto-wake replay the saved startup command only for
124
- computers created with `--command` + `--port` (or the SDK `command`/`port`
125
- arguments). If you start an app later via a one-off `runtime run`, the
126
- filesystem is restored after going cold, but that ad-hoc process is not.
135
+ Cold restore and explicit `runtime start` replay the saved published app command.
136
+ New computers seed that durable app from the starter workspace. Later,
137
+ `runtime publish <id> <port>` can promote a running listener into the saved
138
+ public app definition by inspecting the live process. Inside a running computer,
139
+ `runtime-env publish <port>` does the same thing using a computer-scoped token
140
+ installed by Runtime. `runtime service show|clear` and `runtime-env service
141
+ show|clear` expose that same durable app state directly. The low-level
142
+ `runtime startup ...` commands still map to the same durable state. A one-off
143
+ `runtime run` stays one-shot: the filesystem is restored after going cold, but
144
+ that ad-hoc process is not.
127
145
 
128
146
  Run the SDK unit tests through the backend project environment:
129
147
 
@@ -35,16 +35,27 @@ runtime logout
35
35
  runtime login --api-key rt_live_...
36
36
 
37
37
  # Computers
38
- runtime create # in a real TTY, creates then drops you into the console
38
+ runtime create # creates a computer with the starter app already published
39
39
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
40
40
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
41
41
  runtime list
42
42
  runtime info <id>
43
43
  runtime start <id>
44
- runtime run <id> "echo hello"
44
+ runtime run <id> "echo hello" # one-shot foreground command only
45
45
  runtime run <id> "apt install -y nodejs" --uid 0
46
- runtime publish <id> 3000
46
+ runtime startup show <id>
47
+ runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
48
+ runtime startup clear <id> # low-level durable service config
49
+ runtime service show <id> # user-facing alias for the durable published app
50
+ runtime service clear <id>
51
+ runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
47
52
  runtime delete <id>
53
+
54
+ # Inside a running computer, the helper installed by Runtime can manage the
55
+ # durable app without leaving the sandbox:
56
+ # runtime-env publish 3000
57
+ # runtime-env service show
58
+ # runtime-env service clear
48
59
  ```
49
60
 
50
61
  ## Python
@@ -54,12 +65,12 @@ from runtime_sdk import RuntimeClient
54
65
 
55
66
  client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
56
67
 
57
- # Create a computer
68
+ # Create a computer. New computers start with the starter app already published.
58
69
  computer = client.create_computer()
59
70
  print(computer["public_url"]) # https://goldbird.runruntime.dev
60
71
 
61
- # Or create one with a durable startup command.
62
- # That startup config is replayed after cold restore / auto-wake.
72
+ # Or create one with an explicit durable app command.
73
+ # That saved service is replayed after cold restore / start.
63
74
  app = client.create_computer(
64
75
  slug="myapp",
65
76
  command="python3 app.py",
@@ -74,7 +85,8 @@ print(result["stdout"])
74
85
  # Wake a cold computer explicitly
75
86
  client.start_computer(app["id"])
76
87
 
77
- # Pin the public URL to a local app port
88
+ # Promote the running app on a local port to the durable public app.
89
+ # Runtime inspects the listening process and saves its command + cwd when possible.
78
90
  client.publish_port(computer["id"], 3000)
79
91
 
80
92
  # List, info, delete
@@ -101,10 +113,16 @@ make smoke
101
113
  Use `make deploy` instead of `make sync` when migrations, env
102
114
  files, Caddy, or systemd units changed.
103
115
 
104
- Cold restore and public auto-wake replay the saved startup command only for
105
- computers created with `--command` + `--port` (or the SDK `command`/`port`
106
- arguments). If you start an app later via a one-off `runtime run`, the
107
- filesystem is restored after going cold, but that ad-hoc process is not.
116
+ Cold restore and explicit `runtime start` replay the saved published app command.
117
+ New computers seed that durable app from the starter workspace. Later,
118
+ `runtime publish <id> <port>` can promote a running listener into the saved
119
+ public app definition by inspecting the live process. Inside a running computer,
120
+ `runtime-env publish <port>` does the same thing using a computer-scoped token
121
+ installed by Runtime. `runtime service show|clear` and `runtime-env service
122
+ show|clear` expose that same durable app state directly. The low-level
123
+ `runtime startup ...` commands still map to the same durable state. A one-off
124
+ `runtime run` stays one-shot: the filesystem is restored after going cold, but
125
+ that ad-hoc process is not.
108
126
 
109
127
  Run the SDK unit tests through the backend project environment:
110
128
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.10"
3
+ version = "0.4.11"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -165,16 +165,16 @@ def build_parser() -> argparse.ArgumentParser:
165
165
  subparsers.add_parser("whoami")
166
166
  subparsers.add_parser("logout")
167
167
 
168
- create_cmd = subparsers.add_parser("create", help="Create a new computer")
168
+ create_cmd = subparsers.add_parser("create", help="Create a new computer with the starter app published by default")
169
169
  create_cmd.add_argument(
170
170
  "name",
171
171
  nargs="?",
172
172
  default=None,
173
173
  help="Subdomain name (e.g. redsox → redsox.runruntime.dev). Random if skipped.",
174
174
  )
175
- create_cmd.add_argument("--command", dest="startup_command", help="App startup command to run after create")
176
- create_cmd.add_argument("--cwd", help="Working directory for the startup command")
177
- create_cmd.add_argument("--port", type=int, help="App port to publish after startup")
175
+ create_cmd.add_argument("--command", dest="startup_command", help="Durable startup command to save on create")
176
+ create_cmd.add_argument("--cwd", help="Working directory for the durable startup command")
177
+ create_cmd.add_argument("--port", type=int, help="App port to publish for durable startup")
178
178
  subparsers.add_parser("list", aliases=["ls"], help="List computers")
179
179
 
180
180
  enter_cmd = subparsers.add_parser("enter", help="Enter a computer's console")
@@ -192,7 +192,7 @@ def build_parser() -> argparse.ArgumentParser:
192
192
  delete_cmd = subparsers.add_parser("delete", aliases=["rm"], help="Delete a computer")
193
193
  delete_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
194
194
 
195
- run_cmd = subparsers.add_parser("run", help="Run a command in a computer")
195
+ run_cmd = subparsers.add_parser("run", help="Run a one-shot command in a computer")
196
196
  run_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
197
197
  run_cmd.add_argument(
198
198
  "run_command",
@@ -207,11 +207,30 @@ def build_parser() -> argparse.ArgumentParser:
207
207
  run_cmd.add_argument("--shell", help="Shell to use")
208
208
 
209
209
  publish_cmd = subparsers.add_parser(
210
- "publish", help="Pin the public URL to a local app port"
210
+ "publish", help="Promote the running app on a local port into the durable public app"
211
211
  )
212
212
  publish_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
213
213
  publish_cmd.add_argument("port", nargs="?", type=int, default=None, help="Local app port")
214
214
 
215
+ startup_cmd = subparsers.add_parser("startup", help="Manage durable startup config")
216
+ startup_subparsers = startup_cmd.add_subparsers(dest="startup_action", required=True)
217
+ startup_show = startup_subparsers.add_parser("show", help="Show durable startup config")
218
+ startup_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
219
+ startup_set = startup_subparsers.add_parser("set", help="Set durable startup config")
220
+ startup_set.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
221
+ startup_set.add_argument("--command", dest="startup_value_command", required=True, help="Durable startup command")
222
+ startup_set.add_argument("--cwd", help="Working directory for the durable startup command")
223
+ startup_set.add_argument("--port", required=True, type=int, help="App port to publish for durable startup")
224
+ startup_clear = startup_subparsers.add_parser("clear", help="Clear durable startup config")
225
+ startup_clear.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
226
+
227
+ service_cmd = subparsers.add_parser("service", help="Manage the durable published app service")
228
+ service_subparsers = service_cmd.add_subparsers(dest="service_action", required=True)
229
+ service_show = service_subparsers.add_parser("show", help="Show the durable published app service")
230
+ service_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
231
+ service_clear = service_subparsers.add_parser("clear", help="Clear the durable published app service")
232
+ service_clear.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
233
+
215
234
  proxy_cmd = subparsers.add_parser("proxy", help="Manage background local port proxies")
216
235
  proxy_subparsers = proxy_cmd.add_subparsers(dest="proxy_command", required=True)
217
236
  proxy_start = proxy_subparsers.add_parser("start", help="Start one or more background local port proxies")
@@ -287,6 +306,20 @@ def main(argv: list[str] | None = None) -> int:
287
306
  )
288
307
  if args.command == "publish":
289
308
  return handle_publish(config, args.id, args.port)
309
+ if args.command == "startup":
310
+ if args.startup_action == "show":
311
+ return handle_startup_show(config, args.id)
312
+ if args.startup_action == "set":
313
+ return handle_startup_set(config, args.id, args.startup_value_command, args.cwd, args.port)
314
+ if args.startup_action == "clear":
315
+ return handle_startup_clear(config, args.id)
316
+ return report_error("unknown startup command")
317
+ if args.command == "service":
318
+ if args.service_action == "show":
319
+ return handle_service_show(config, args.id)
320
+ if args.service_action == "clear":
321
+ return handle_service_clear(config, args.id)
322
+ return report_error("unknown service command")
290
323
  if args.command == "proxy":
291
324
  if args.proxy_command == "start":
292
325
  return handle_proxy_start(config, args.id, args.ports)
@@ -664,7 +697,7 @@ def _resolve_computer_ref(client: RuntimeClient, ref: str) -> str:
664
697
  return resolved or ref
665
698
  if len(matches) > 1:
666
699
  raise RuntimeAPIError(f"multiple computers matched {ref!r}; use the exact computer id")
667
- return ref
700
+ raise RuntimeAPIError(f"computer {ref!r} not found")
668
701
 
669
702
 
670
703
 
@@ -999,6 +1032,27 @@ def _render_network_panel(payload: dict[str, Any], *, title: str = "url") -> Non
999
1032
  _UI.console().print(Panel(body, title=title, border_style="cyan", expand=False, padding=(0, 1)))
1000
1033
 
1001
1034
 
1035
+ def _render_startup_panel(payload: dict[str, Any], *, title: str = "startup") -> None:
1036
+ mods = _UI.rich()
1037
+ Panel, Text = mods["Panel"], mods["Text"]
1038
+ body = Text()
1039
+ configured = bool(payload.get("configured"))
1040
+ rows: list[tuple[str, str, str | None]] = [("configured", "yes" if configured else "no", "bold white" if configured else "dim")]
1041
+ if configured:
1042
+ rows.extend(
1043
+ [
1044
+ ("command", str(payload.get("command") or "—"), "white"),
1045
+ ("cwd", str(payload.get("cwd") or "/home/ubuntu"), "dim"),
1046
+ ("port", str(payload.get("port") or "—"), "bold white"),
1047
+ ]
1048
+ )
1049
+ for label, value, style in rows:
1050
+ body.append(f"{label:<10}", style="dim")
1051
+ body.append(value, style=style or "white")
1052
+ body.append("\n")
1053
+ _UI.console().print(Panel(body, title=title, border_style="cyan", expand=False, padding=(0, 1)))
1054
+
1055
+
1002
1056
  def _render_whoami_panel(payload: dict[str, Any]) -> None:
1003
1057
  mods = _UI.rich()
1004
1058
  Panel, Text = mods["Panel"], mods["Text"]
@@ -1260,6 +1314,111 @@ def handle_url(config: RuntimeConfig, computer_id: str | None) -> int:
1260
1314
  return report_success(result, lambda p: _render_network_panel(p, title="url"))
1261
1315
 
1262
1316
 
1317
+ def handle_startup_show(config: RuntimeConfig, computer_id: str | None) -> int:
1318
+ if (err := _require_api_key(config)) is not None:
1319
+ return err
1320
+
1321
+ if computer_id is None:
1322
+ if not _interactive():
1323
+ return report_error("computer id is required")
1324
+ picked = _pick_computer(config, "Pick a computer")
1325
+ if picked is None:
1326
+ return 0
1327
+ computer_id = picked.get("id") or picked.get("slug")
1328
+ if not computer_id:
1329
+ return report_error("computer is missing an id")
1330
+
1331
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1332
+ computer_id = _resolve_computer_ref(client, computer_id)
1333
+ result = client.get_startup_config(computer_id)
1334
+ return report_success(result, lambda p: _render_startup_panel(p, title="startup"))
1335
+
1336
+
1337
+ def handle_startup_set(
1338
+ config: RuntimeConfig,
1339
+ computer_id: str | None,
1340
+ command: str | None,
1341
+ cwd: str | None,
1342
+ port: int | None,
1343
+ ) -> int:
1344
+ if (err := _require_api_key(config)) is not None:
1345
+ return err
1346
+ if not computer_id:
1347
+ return report_error("computer id is required")
1348
+ if not command:
1349
+ return report_error("startup command is required")
1350
+ if port is None:
1351
+ return report_error("port is required")
1352
+ if port <= 0 or port > 65535:
1353
+ return report_error("port must be between 1 and 65535")
1354
+
1355
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1356
+ computer_id = _resolve_computer_ref(client, computer_id)
1357
+ result = _with_spinner(
1358
+ f"saving startup config for {computer_id}…",
1359
+ lambda: client.set_startup_config(computer_id, command=command, cwd=cwd, port=port),
1360
+ )
1361
+ return report_success(result, lambda p: _render_startup_panel(p, title="startup saved"))
1362
+
1363
+
1364
+ def handle_startup_clear(config: RuntimeConfig, computer_id: str | None) -> int:
1365
+ if (err := _require_api_key(config)) is not None:
1366
+ return err
1367
+ if not computer_id:
1368
+ return report_error("computer id is required")
1369
+
1370
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1371
+ computer_id = _resolve_computer_ref(client, computer_id)
1372
+ result = _with_spinner(
1373
+ f"clearing startup config for {computer_id}…",
1374
+ lambda: client.clear_startup_config(computer_id),
1375
+ )
1376
+
1377
+ def render(_: dict[str, Any]) -> None:
1378
+ _UI.console().print(f"[green]✓[/green] cleared durable startup config for [bold]{computer_id}[/bold]")
1379
+
1380
+ return report_success(result or {"configured": False}, render)
1381
+
1382
+
1383
+ def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
1384
+ if (err := _require_api_key(config)) is not None:
1385
+ return err
1386
+
1387
+ if computer_id is None:
1388
+ if not _interactive():
1389
+ return report_error("computer id is required")
1390
+ picked = _pick_computer(config, "Pick a computer")
1391
+ if picked is None:
1392
+ return 0
1393
+ computer_id = picked.get("id") or picked.get("slug")
1394
+ if not computer_id:
1395
+ return report_error("computer is missing an id")
1396
+
1397
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1398
+ computer_id = _resolve_computer_ref(client, computer_id)
1399
+ result = client.get_service_config(computer_id)
1400
+ return report_success(result, lambda p: _render_startup_panel(p, title="service"))
1401
+
1402
+
1403
+ def handle_service_clear(config: RuntimeConfig, computer_id: str | None) -> int:
1404
+ if (err := _require_api_key(config)) is not None:
1405
+ return err
1406
+ if not computer_id:
1407
+ return report_error("computer id is required")
1408
+
1409
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1410
+ computer_id = _resolve_computer_ref(client, computer_id)
1411
+ result = _with_spinner(
1412
+ f"clearing service for {computer_id}…",
1413
+ lambda: client.clear_service_config(computer_id),
1414
+ )
1415
+
1416
+ def render(_: dict[str, Any]) -> None:
1417
+ _UI.console().print(f"[green]✓[/green] cleared durable service for [bold]{computer_id}[/bold]")
1418
+
1419
+ return report_success(result or {"configured": False}, render)
1420
+
1421
+
1263
1422
  def handle_start(config: RuntimeConfig, computer_id: str | None) -> int:
1264
1423
  if (err := _require_api_key(config)) is not None:
1265
1424
  return err
@@ -1433,7 +1592,7 @@ def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | N
1433
1592
 
1434
1593
  def render(payload: dict[str, Any]) -> None:
1435
1594
  _UI.console().print(
1436
- f"[green]✓[/green] public url for [bold]{computer_id}[/bold] pinned to [bold]{payload.get('published_port') or port}[/bold]"
1595
+ f"[green]✓[/green] public url for [bold]{computer_id}[/bold] now points at durable app port [bold]{payload.get('published_port') or port}[/bold]"
1437
1596
  )
1438
1597
  url = payload.get("public_url")
1439
1598
  if url:
@@ -1526,6 +1685,17 @@ def handle_proxy_stop(config: RuntimeConfig, local_ports: list[int] | None, *, s
1526
1685
  return report_success({"proxies": stopped}, render if _interactive() else None)
1527
1686
 
1528
1687
 
1688
+ def _proxy_preflight(base_url: str, api_key: str, computer_id: str, remote_port: int) -> None:
1689
+ client = RuntimeClient(base_url=base_url, api_key=api_key)
1690
+ ticket_payload = client.create_proxy_ticket(computer_id, remote_port)
1691
+ ws_url = _resolve_ws_url(base_url, ticket_payload.get("ws_url"))
1692
+ if not ws_url:
1693
+ raise RuntimeAPIError("proxy endpoint did not return a websocket url")
1694
+ with websocket_connect(ws_url, open_timeout=DEFAULT_WARMUP_TIMEOUT, close_timeout=1, max_size=None):
1695
+ return
1696
+
1697
+
1698
+
1529
1699
  def run_proxy_agent(base_url: str, api_key: str, computer_id: str, display_name: str, local_port: int, remote_port: int) -> int:
1530
1700
  if not api_key:
1531
1701
  print("proxy agent missing api key", file=sys.stderr)
@@ -1541,6 +1711,14 @@ def run_proxy_agent(base_url: str, api_key: str, computer_id: str, display_name:
1541
1711
  print(f"proxy listen failed on localhost:{local_port}: {exc}", file=sys.stderr)
1542
1712
  return 1
1543
1713
 
1714
+ try:
1715
+ _proxy_preflight(base_url, api_key, computer_id, remote_port)
1716
+ except Exception as exc:
1717
+ with contextlib.suppress(OSError):
1718
+ listener.close()
1719
+ print(f"proxy preflight failed for localhost:{local_port} -> {computer_id}:{remote_port}: {exc}", file=sys.stderr)
1720
+ return 1
1721
+
1544
1722
  proxy_dir = _proxy_dir()
1545
1723
  proxy_dir.mkdir(parents=True, exist_ok=True)
1546
1724
  state = {
@@ -82,6 +82,31 @@ class RuntimeClient:
82
82
  def get_computer_network(self, computer_id: str) -> dict[str, Any]:
83
83
  return self._request("GET", f"/api/computers/{computer_id}/network", auth_required=True)
84
84
 
85
+ def get_startup_config(self, computer_id: str) -> dict[str, Any]:
86
+ return self._request("GET", f"/api/computers/{computer_id}/startup", auth_required=True)
87
+
88
+ def get_service_config(self, computer_id: str) -> dict[str, Any]:
89
+ return self.get_startup_config(computer_id)
90
+
91
+ def set_startup_config(
92
+ self,
93
+ computer_id: str,
94
+ *,
95
+ command: str,
96
+ cwd: str | None = None,
97
+ port: int,
98
+ ) -> dict[str, Any]:
99
+ body: dict[str, Any] = {"command": command, "port": port}
100
+ if cwd is not None:
101
+ body["cwd"] = cwd
102
+ return self._request("PUT", f"/api/computers/{computer_id}/startup", json=body, auth_required=True)
103
+
104
+ def clear_startup_config(self, computer_id: str) -> dict[str, Any]:
105
+ return self._request("DELETE", f"/api/computers/{computer_id}/startup", auth_required=True)
106
+
107
+ def clear_service_config(self, computer_id: str) -> dict[str, Any]:
108
+ return self.clear_startup_config(computer_id)
109
+
85
110
  def start_computer(self, computer_id: str) -> dict[str, Any]:
86
111
  return self._request(
87
112
  "POST",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.10
3
+ Version: 0.4.11
4
4
  Summary: Runtime Python SDK and CLI
5
5
  Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
6
6
  Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
@@ -54,16 +54,27 @@ runtime logout
54
54
  runtime login --api-key rt_live_...
55
55
 
56
56
  # Computers
57
- runtime create # in a real TTY, creates then drops you into the console
57
+ runtime create # creates a computer with the starter app already published
58
58
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
59
59
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
60
60
  runtime list
61
61
  runtime info <id>
62
62
  runtime start <id>
63
- runtime run <id> "echo hello"
63
+ runtime run <id> "echo hello" # one-shot foreground command only
64
64
  runtime run <id> "apt install -y nodejs" --uid 0
65
- runtime publish <id> 3000
65
+ runtime startup show <id>
66
+ runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
67
+ runtime startup clear <id> # low-level durable service config
68
+ runtime service show <id> # user-facing alias for the durable published app
69
+ runtime service clear <id>
70
+ runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
66
71
  runtime delete <id>
72
+
73
+ # Inside a running computer, the helper installed by Runtime can manage the
74
+ # durable app without leaving the sandbox:
75
+ # runtime-env publish 3000
76
+ # runtime-env service show
77
+ # runtime-env service clear
67
78
  ```
68
79
 
69
80
  ## Python
@@ -73,12 +84,12 @@ from runtime_sdk import RuntimeClient
73
84
 
74
85
  client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
75
86
 
76
- # Create a computer
87
+ # Create a computer. New computers start with the starter app already published.
77
88
  computer = client.create_computer()
78
89
  print(computer["public_url"]) # https://goldbird.runruntime.dev
79
90
 
80
- # Or create one with a durable startup command.
81
- # That startup config is replayed after cold restore / auto-wake.
91
+ # Or create one with an explicit durable app command.
92
+ # That saved service is replayed after cold restore / start.
82
93
  app = client.create_computer(
83
94
  slug="myapp",
84
95
  command="python3 app.py",
@@ -93,7 +104,8 @@ print(result["stdout"])
93
104
  # Wake a cold computer explicitly
94
105
  client.start_computer(app["id"])
95
106
 
96
- # Pin the public URL to a local app port
107
+ # Promote the running app on a local port to the durable public app.
108
+ # Runtime inspects the listening process and saves its command + cwd when possible.
97
109
  client.publish_port(computer["id"], 3000)
98
110
 
99
111
  # List, info, delete
@@ -120,10 +132,16 @@ make smoke
120
132
  Use `make deploy` instead of `make sync` when migrations, env
121
133
  files, Caddy, or systemd units changed.
122
134
 
123
- Cold restore and public auto-wake replay the saved startup command only for
124
- computers created with `--command` + `--port` (or the SDK `command`/`port`
125
- arguments). If you start an app later via a one-off `runtime run`, the
126
- filesystem is restored after going cold, but that ad-hoc process is not.
135
+ Cold restore and explicit `runtime start` replay the saved published app command.
136
+ New computers seed that durable app from the starter workspace. Later,
137
+ `runtime publish <id> <port>` can promote a running listener into the saved
138
+ public app definition by inspecting the live process. Inside a running computer,
139
+ `runtime-env publish <port>` does the same thing using a computer-scoped token
140
+ installed by Runtime. `runtime service show|clear` and `runtime-env service
141
+ show|clear` expose that same durable app state directly. The low-level
142
+ `runtime startup ...` commands still map to the same durable state. A one-off
143
+ `runtime run` stays one-shot: the filesystem is restored after going cold, but
144
+ that ad-hoc process is not.
127
145
 
128
146
  Run the SDK unit tests through the backend project environment:
129
147
 
File without changes