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.
@@ -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")