runtime-sdk 0.4.49__py3-none-win_arm64.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/providers.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import select
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from runtime_sdk import output as cli_output, terminal
|
|
17
|
+
from runtime_sdk.auth import authenticated_client
|
|
18
|
+
from runtime_sdk.client import RuntimeAPIError
|
|
19
|
+
from runtime_sdk.config import RuntimeConfig, RuntimeConfigError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _read_provider_auth_file(path: str | Path) -> str:
|
|
23
|
+
try:
|
|
24
|
+
return Path(path).read_text(encoding="utf-8")
|
|
25
|
+
except OSError as exc:
|
|
26
|
+
raise RuntimeConfigError(f"failed to read auth file: {exc}") from exc
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _codex_auth_path(codex_home: Path) -> Path:
|
|
30
|
+
return codex_home / "auth.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _local_codex_auth_path() -> Path:
|
|
34
|
+
codex_home = os.environ.get("CODEX_HOME", "").strip()
|
|
35
|
+
return _codex_auth_path(
|
|
36
|
+
Path(codex_home).expanduser() if codex_home else Path.home() / ".codex"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _claude_auth_env(home: Path) -> dict[str, str]:
|
|
41
|
+
env = os.environ.copy()
|
|
42
|
+
env["HOME"] = str(home)
|
|
43
|
+
env["USERPROFILE"] = str(home)
|
|
44
|
+
env["XDG_CONFIG_HOME"] = str(home / ".config")
|
|
45
|
+
env.pop("ANTHROPIC_API_KEY", None)
|
|
46
|
+
env.pop("ANTHROPIC_AUTH_TOKEN", None)
|
|
47
|
+
env.pop("CLAUDE_CODE_OAUTH_TOKEN", None)
|
|
48
|
+
return env
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_codex_login(codex_bin: str, codex_home: Path, *, device: bool) -> int:
|
|
52
|
+
cmd = [
|
|
53
|
+
codex_bin,
|
|
54
|
+
"login",
|
|
55
|
+
"-c",
|
|
56
|
+
'cli_auth_credentials_store="file"',
|
|
57
|
+
]
|
|
58
|
+
if device:
|
|
59
|
+
cmd.append("--device-auth")
|
|
60
|
+
env = os.environ.copy()
|
|
61
|
+
env["CODEX_HOME"] = str(codex_home)
|
|
62
|
+
return subprocess.run(cmd, env=env).returncode
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_claude_setup_token(
|
|
66
|
+
claude_bin: str, home: Path, *, timeout_seconds: int = 300
|
|
67
|
+
) -> str:
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
return _run_claude_setup_token_windows(claude_bin, home)
|
|
70
|
+
|
|
71
|
+
import pty
|
|
72
|
+
|
|
73
|
+
sys.stderr.write(
|
|
74
|
+
"Creating Claude Runtime proxy token. Complete the browser flow if prompted.\n"
|
|
75
|
+
)
|
|
76
|
+
sys.stderr.flush()
|
|
77
|
+
master_fd, slave_fd = pty.openpty()
|
|
78
|
+
proc = subprocess.Popen(
|
|
79
|
+
[claude_bin, "setup-token"],
|
|
80
|
+
stdin=slave_fd,
|
|
81
|
+
stdout=slave_fd,
|
|
82
|
+
stderr=slave_fd,
|
|
83
|
+
env=_claude_auth_env(home),
|
|
84
|
+
close_fds=True,
|
|
85
|
+
)
|
|
86
|
+
os.close(slave_fd)
|
|
87
|
+
chunks: list[bytes] = []
|
|
88
|
+
displayed = 0
|
|
89
|
+
token_output_hidden = False
|
|
90
|
+
deadline = time.monotonic() + timeout_seconds
|
|
91
|
+
try:
|
|
92
|
+
while proc.poll() is None:
|
|
93
|
+
remaining = deadline - time.monotonic()
|
|
94
|
+
if remaining <= 0:
|
|
95
|
+
proc.terminate()
|
|
96
|
+
try:
|
|
97
|
+
proc.wait(timeout=5)
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
proc.kill()
|
|
100
|
+
proc.wait()
|
|
101
|
+
raise RuntimeConfigError("claude setup-token timed out")
|
|
102
|
+
readers = [master_fd]
|
|
103
|
+
if cli_output.interactive():
|
|
104
|
+
readers.append(sys.stdin.fileno())
|
|
105
|
+
readable, _, _ = select.select(readers, [], [], min(0.2, remaining))
|
|
106
|
+
for fd in readable:
|
|
107
|
+
if fd == master_fd:
|
|
108
|
+
try:
|
|
109
|
+
data = os.read(master_fd, 4096)
|
|
110
|
+
except OSError:
|
|
111
|
+
data = b""
|
|
112
|
+
if data:
|
|
113
|
+
chunks.append(data)
|
|
114
|
+
displayed, token_output_hidden = _write_claude_setup_output(
|
|
115
|
+
b"".join(chunks).decode("utf-8", errors="ignore"),
|
|
116
|
+
displayed,
|
|
117
|
+
token_output_hidden,
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
data = os.read(fd, 1024)
|
|
121
|
+
if data:
|
|
122
|
+
filtered = _filter_claude_setup_input(data)
|
|
123
|
+
if filtered:
|
|
124
|
+
os.write(master_fd, filtered)
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
data = os.read(master_fd, 4096)
|
|
128
|
+
except OSError:
|
|
129
|
+
break
|
|
130
|
+
if not data:
|
|
131
|
+
break
|
|
132
|
+
chunks.append(data)
|
|
133
|
+
displayed, token_output_hidden = _write_claude_setup_output(
|
|
134
|
+
b"".join(chunks).decode("utf-8", errors="ignore"),
|
|
135
|
+
displayed,
|
|
136
|
+
token_output_hidden,
|
|
137
|
+
)
|
|
138
|
+
finally:
|
|
139
|
+
with contextlib.suppress(OSError):
|
|
140
|
+
os.close(master_fd)
|
|
141
|
+
status = proc.wait()
|
|
142
|
+
if status != 0:
|
|
143
|
+
raise RuntimeConfigError(f"claude setup-token failed with exit code {status}")
|
|
144
|
+
token = _extract_claude_oauth_token(
|
|
145
|
+
b"".join(chunks).decode("utf-8", errors="ignore")
|
|
146
|
+
)
|
|
147
|
+
if not token:
|
|
148
|
+
raise RuntimeConfigError(
|
|
149
|
+
"claude setup-token completed but did not print an OAuth token"
|
|
150
|
+
)
|
|
151
|
+
return token
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _run_claude_setup_token_windows(claude_bin: str, home: Path) -> str:
|
|
155
|
+
sys.stderr.write(
|
|
156
|
+
"Creating Claude Runtime proxy token. Complete the browser flow, then copy the token shown by Claude.\n"
|
|
157
|
+
)
|
|
158
|
+
sys.stderr.flush()
|
|
159
|
+
status = subprocess.run(
|
|
160
|
+
[claude_bin, "setup-token"], env=_claude_auth_env(home)
|
|
161
|
+
).returncode
|
|
162
|
+
if status != 0:
|
|
163
|
+
raise RuntimeConfigError(f"claude setup-token failed with exit code {status}")
|
|
164
|
+
token = cli_output.prompt_password("Paste Claude OAuth token") or ""
|
|
165
|
+
if not re.fullmatch(r"sk-ant-[A-Za-z0-9_-]{20,}", token):
|
|
166
|
+
raise RuntimeConfigError("Claude did not provide a valid OAuth token")
|
|
167
|
+
return token
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _write_claude_setup_output(
|
|
171
|
+
output: str, displayed: int, token_output_hidden: bool
|
|
172
|
+
) -> tuple[int, bool]:
|
|
173
|
+
marker = re.search(r"Your OAuth token", output, flags=re.I)
|
|
174
|
+
if marker:
|
|
175
|
+
if marker.start() > displayed:
|
|
176
|
+
sys.stderr.write(
|
|
177
|
+
terminal.strip_controls(output[displayed : marker.start()])
|
|
178
|
+
)
|
|
179
|
+
sys.stderr.flush()
|
|
180
|
+
if not token_output_hidden:
|
|
181
|
+
sys.stderr.write("\nClaude OAuth token captured and hidden.\n")
|
|
182
|
+
sys.stderr.flush()
|
|
183
|
+
return len(output), True
|
|
184
|
+
if len(output) > displayed:
|
|
185
|
+
chunk = terminal.strip_controls(output[displayed:])
|
|
186
|
+
chunk = re.sub(r"sk-ant-[A-Za-z0-9_-]{12,}", "[hidden]", chunk)
|
|
187
|
+
sys.stderr.write(chunk)
|
|
188
|
+
sys.stderr.flush()
|
|
189
|
+
return len(output), token_output_hidden
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _filter_claude_setup_input(data: bytes) -> bytes:
|
|
193
|
+
cleaned = terminal.strip_controls(data.decode("utf-8", errors="ignore"))
|
|
194
|
+
return cleaned.encode()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extract_claude_oauth_token(output: str) -> str:
|
|
198
|
+
output = terminal.strip_controls(output)
|
|
199
|
+
section = output
|
|
200
|
+
marker = re.search(r"Your OAuth token.*?:", output, flags=re.I | re.S)
|
|
201
|
+
if marker:
|
|
202
|
+
section = output[marker.end() :]
|
|
203
|
+
section = re.split(r"Store this token", section, maxsplit=1, flags=re.I)[0]
|
|
204
|
+
compact = re.sub(r"\s+", "", section)
|
|
205
|
+
match = re.search(r"sk-ant-[A-Za-z0-9_-]{20,}", compact)
|
|
206
|
+
return match.group(0) if match else ""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _claude_oauth_token_json(token: str) -> str:
|
|
210
|
+
return json.dumps(
|
|
211
|
+
{"type": "claude-code-oauth-token", "token": token}, separators=(",", ":")
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _grok_auth_path() -> Path:
|
|
216
|
+
grok_home = os.environ.get("GROK_HOME", "").strip()
|
|
217
|
+
return (
|
|
218
|
+
Path(grok_home).expanduser() / "auth.json"
|
|
219
|
+
if grok_home
|
|
220
|
+
else Path.home() / ".grok" / "auth.json"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _grok_oauth_token_json(auth_json: str) -> str:
|
|
225
|
+
try:
|
|
226
|
+
payload = json.loads(auth_json)
|
|
227
|
+
except json.JSONDecodeError as exc:
|
|
228
|
+
raise RuntimeConfigError("Grok auth.json is not valid JSON") from exc
|
|
229
|
+
if not isinstance(payload, dict):
|
|
230
|
+
raise RuntimeConfigError("Grok auth.json must contain an account login")
|
|
231
|
+
|
|
232
|
+
candidates: list[tuple[str, str]] = []
|
|
233
|
+
for value in payload.values():
|
|
234
|
+
if not isinstance(value, dict):
|
|
235
|
+
continue
|
|
236
|
+
token = str(value.get("key") or "").strip()
|
|
237
|
+
if token:
|
|
238
|
+
candidates.append((str(value.get("expires_at") or ""), token))
|
|
239
|
+
if not candidates:
|
|
240
|
+
raise RuntimeConfigError("Grok auth.json does not contain a login token")
|
|
241
|
+
|
|
242
|
+
token = max(candidates, key=lambda candidate: candidate[0])[1]
|
|
243
|
+
return json.dumps(
|
|
244
|
+
{"type": "grok-oauth-token", "token": token}, separators=(",", ":")
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _xai_api_key_json(api_key: str) -> str:
|
|
249
|
+
api_key = api_key.strip()
|
|
250
|
+
if not api_key:
|
|
251
|
+
raise RuntimeConfigError("xAI API key is required")
|
|
252
|
+
return json.dumps(
|
|
253
|
+
{"type": "xai-api-key", "api_key": api_key}, separators=(",", ":")
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _xai_api_key(api_key: str | None) -> str | None:
|
|
258
|
+
resolved = (api_key or "").strip()
|
|
259
|
+
if resolved:
|
|
260
|
+
return resolved
|
|
261
|
+
resolved = os.environ.get("XAI_API_KEY", "").strip()
|
|
262
|
+
if resolved:
|
|
263
|
+
return resolved
|
|
264
|
+
if not cli_output.interactive():
|
|
265
|
+
return None
|
|
266
|
+
return cli_output.prompt_password("xAI API key")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _find_provider(credentials: dict[str, Any], provider: str) -> dict[str, Any] | None:
|
|
270
|
+
for credential in credentials.get("provider_credentials") or []:
|
|
271
|
+
if credential.get("provider") == provider:
|
|
272
|
+
return credential
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def handle_provider_status(config: RuntimeConfig, provider: str) -> int:
|
|
277
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
278
|
+
return err
|
|
279
|
+
client = authenticated_client(config)
|
|
280
|
+
credential = _find_provider(client.list_provider_credentials(), provider)
|
|
281
|
+
return cli_output.report_success(
|
|
282
|
+
{
|
|
283
|
+
"provider": provider,
|
|
284
|
+
"saved": credential is not None,
|
|
285
|
+
"provider_credential": credential,
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def handle_provider_logout(config: RuntimeConfig, provider: str) -> int:
|
|
291
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
292
|
+
return err
|
|
293
|
+
client = authenticated_client(config)
|
|
294
|
+
try:
|
|
295
|
+
client.delete_provider_credential(provider)
|
|
296
|
+
except RuntimeAPIError as exc:
|
|
297
|
+
if exc.status_code != 404:
|
|
298
|
+
raise
|
|
299
|
+
return cli_output.report_success({"provider": provider, "saved": False})
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def handle_codex_status(config: RuntimeConfig) -> int:
|
|
303
|
+
return handle_provider_status(config, "codex")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def handle_codex_login(config: RuntimeConfig, *, device: bool = False) -> int:
|
|
307
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
308
|
+
return err
|
|
309
|
+
local_auth_path = _local_codex_auth_path()
|
|
310
|
+
if local_auth_path.exists():
|
|
311
|
+
auth_json = _read_provider_auth_file(local_auth_path)
|
|
312
|
+
else:
|
|
313
|
+
codex_bin = shutil.which("codex")
|
|
314
|
+
if not codex_bin:
|
|
315
|
+
return cli_output.report_error("codex CLI is not installed")
|
|
316
|
+
|
|
317
|
+
with tempfile.TemporaryDirectory(prefix="runtime-codex-") as codex_home:
|
|
318
|
+
home = Path(codex_home)
|
|
319
|
+
status = _run_codex_login(codex_bin, home, device=device)
|
|
320
|
+
if status != 0:
|
|
321
|
+
return cli_output.report_error(
|
|
322
|
+
f"codex login failed with exit code {status}"
|
|
323
|
+
)
|
|
324
|
+
auth_path = _codex_auth_path(home)
|
|
325
|
+
if not auth_path.exists():
|
|
326
|
+
return cli_output.report_error(
|
|
327
|
+
"codex login completed but did not write auth.json"
|
|
328
|
+
)
|
|
329
|
+
auth_json = _read_provider_auth_file(auth_path)
|
|
330
|
+
|
|
331
|
+
client = authenticated_client(config)
|
|
332
|
+
credential = client.put_provider_credential("codex", auth_json)
|
|
333
|
+
return cli_output.report_success(
|
|
334
|
+
{"provider": "codex", "saved": True, "provider_credential": credential}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def handle_codex_logout(config: RuntimeConfig) -> int:
|
|
339
|
+
return handle_provider_logout(config, "codex")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def handle_claude_status(config: RuntimeConfig) -> int:
|
|
343
|
+
return handle_provider_status(config, "claude")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def handle_claude_login(config: RuntimeConfig) -> int:
|
|
347
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
348
|
+
return err
|
|
349
|
+
claude_bin = shutil.which("claude")
|
|
350
|
+
if not claude_bin:
|
|
351
|
+
return cli_output.report_error("claude CLI is not installed")
|
|
352
|
+
|
|
353
|
+
with tempfile.TemporaryDirectory(prefix="runtime-claude-") as claude_home:
|
|
354
|
+
home = Path(claude_home)
|
|
355
|
+
token = _run_claude_setup_token(claude_bin, home)
|
|
356
|
+
auth_json = _claude_oauth_token_json(token)
|
|
357
|
+
|
|
358
|
+
client = authenticated_client(config)
|
|
359
|
+
credential = client.put_provider_credential("claude", auth_json)
|
|
360
|
+
return cli_output.report_success(
|
|
361
|
+
{"provider": "claude", "saved": True, "provider_credential": credential}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def handle_claude_logout(config: RuntimeConfig) -> int:
|
|
366
|
+
return handle_provider_logout(config, "claude")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def handle_grok_status(config: RuntimeConfig) -> int:
|
|
370
|
+
return handle_provider_status(config, "grok")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def handle_grok_login(config: RuntimeConfig) -> int:
|
|
374
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
375
|
+
return err
|
|
376
|
+
auth_path = _grok_auth_path()
|
|
377
|
+
if not auth_path.exists():
|
|
378
|
+
grok_bin = shutil.which("grok")
|
|
379
|
+
if not grok_bin:
|
|
380
|
+
return cli_output.report_error("grok CLI is not installed")
|
|
381
|
+
status = subprocess.run([grok_bin, "login"]).returncode
|
|
382
|
+
if status != 0:
|
|
383
|
+
return cli_output.report_error(f"grok login failed with exit code {status}")
|
|
384
|
+
if not auth_path.exists():
|
|
385
|
+
return cli_output.report_error(
|
|
386
|
+
"grok login completed but did not write auth.json"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
auth_json = _grok_oauth_token_json(_read_provider_auth_file(auth_path))
|
|
390
|
+
client = authenticated_client(config)
|
|
391
|
+
credential = client.put_provider_credential("grok", auth_json)
|
|
392
|
+
return cli_output.report_success(
|
|
393
|
+
{"provider": "grok", "saved": True, "provider_credential": credential}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def handle_grok_logout(config: RuntimeConfig) -> int:
|
|
398
|
+
return handle_provider_logout(config, "grok")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def handle_xai_status(config: RuntimeConfig) -> int:
|
|
402
|
+
return handle_provider_status(config, "xai")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def handle_xai_login(config: RuntimeConfig, *, api_key: str | None = None) -> int:
|
|
406
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
407
|
+
return err
|
|
408
|
+
resolved_key = _xai_api_key(api_key)
|
|
409
|
+
if not resolved_key:
|
|
410
|
+
return cli_output.report_error(
|
|
411
|
+
"xAI API key is required; pass --api-key or set XAI_API_KEY"
|
|
412
|
+
)
|
|
413
|
+
auth_json = _xai_api_key_json(resolved_key)
|
|
414
|
+
client = authenticated_client(config)
|
|
415
|
+
credential = client.put_provider_credential("xai", auth_json)
|
|
416
|
+
return cli_output.report_success(
|
|
417
|
+
{"provider": "xai", "saved": True, "provider_credential": credential}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def handle_xai_logout(config: RuntimeConfig) -> int:
|
|
422
|
+
return handle_provider_logout(config, "xai")
|