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.
@@ -0,0 +1,18 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from runtime_sdk.client import RuntimeAPIError, RuntimeClient
4
+ from runtime_sdk.config import RuntimeConfig, RuntimeConfigError, WorkOSSession
5
+
6
+ try:
7
+ __version__ = version("runtime-sdk")
8
+ except PackageNotFoundError: # pragma: no cover - local source tree fallback
9
+ __version__ = "0.0.0"
10
+
11
+ __all__ = [
12
+ "RuntimeAPIError",
13
+ "RuntimeClient",
14
+ "RuntimeConfig",
15
+ "RuntimeConfigError",
16
+ "WorkOSSession",
17
+ "__version__",
18
+ ]
runtime_sdk/account.py ADDED
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ import time
7
+ import webbrowser
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from runtime_sdk import output as cli_output
12
+ from runtime_sdk.auth import (
13
+ authenticated_client,
14
+ clear_credentials,
15
+ save_api_key,
16
+ save_session,
17
+ session_from_payload,
18
+ )
19
+ from runtime_sdk.client import RuntimeAPIError, RuntimeClient
20
+ from runtime_sdk.config import RuntimeConfig
21
+
22
+
23
+ DEVICE_STATE_ENV = "RUNTIME_DEVICE_STATE_FILE"
24
+ SLOW_DOWN_SECONDS = 5
25
+
26
+
27
+ def handle_login(
28
+ config: RuntimeConfig,
29
+ *,
30
+ api_key: str | None,
31
+ no_open: bool,
32
+ ) -> int:
33
+ if os.environ.get("RUNTIME_API_KEY", "").strip():
34
+ return cli_output.report_error(
35
+ "RUNTIME_API_KEY is set; unset it before saving another credential"
36
+ )
37
+ if api_key is not None:
38
+ return _login_with_api_key(config, api_key)
39
+ return _login_with_device(config, no_open=no_open)
40
+
41
+
42
+ def _login_with_api_key(config: RuntimeConfig, api_key: str) -> int:
43
+ if not api_key:
44
+ if not cli_output.interactive():
45
+ return cli_output.report_error("api key is required")
46
+ api_key = cli_output.prompt_password("API key") or ""
47
+ if not api_key:
48
+ return cli_output.report_error("api key is required")
49
+
50
+ identity = RuntimeClient(base_url=config.base_url, credential=api_key).whoami()
51
+ save_api_key(config, api_key)
52
+ return cli_output.report_success({"message": "logged in with API key", **identity})
53
+
54
+
55
+ def _login_with_device(config: RuntimeConfig, *, no_open: bool) -> int:
56
+ client = RuntimeClient(base_url=config.base_url)
57
+ device = _device_response(client.start_device_login())
58
+ _publish_device_state(device)
59
+
60
+ verification_uri = str(device["verification_uri"])
61
+ complete_uri = str(device.get("verification_uri_complete") or verification_uri)
62
+ print(f"Open {verification_uri}", file=os.sys.stderr)
63
+ print(f"Enter code: {device['user_code']}", file=os.sys.stderr)
64
+ if not no_open:
65
+ webbrowser.open(complete_uri)
66
+
67
+ interval = int(device["interval"])
68
+ deadline = time.monotonic() + int(device["expires_in"])
69
+ while time.monotonic() < deadline:
70
+ time.sleep(interval)
71
+ try:
72
+ payload = client.poll_device_login(str(device["device_code"]))
73
+ except RuntimeAPIError as exc:
74
+ if exc.status_code == 202:
75
+ continue
76
+ if exc.status_code == 429:
77
+ interval += SLOW_DOWN_SECONDS
78
+ continue
79
+ if exc.status_code == 403:
80
+ raise RuntimeAPIError("WorkOS sign-in was denied", status_code=403) from exc
81
+ if exc.status_code == 410:
82
+ raise RuntimeAPIError("WorkOS sign-in expired", status_code=410) from exc
83
+ raise
84
+
85
+ session = session_from_payload(payload)
86
+ save_session(config, session)
87
+ return cli_output.report_success(
88
+ {
89
+ "message": "logged in",
90
+ "organization_id": session.organization_id,
91
+ "session_id": session.session_id,
92
+ }
93
+ )
94
+
95
+ raise RuntimeAPIError("WorkOS sign-in expired", status_code=410)
96
+
97
+
98
+ def _device_response(payload: dict[str, Any]) -> dict[str, Any]:
99
+ required_strings = (
100
+ "device_code",
101
+ "user_code",
102
+ "verification_uri",
103
+ )
104
+ required_integers = ("expires_in", "interval")
105
+ for field in required_strings:
106
+ if not isinstance(payload.get(field), str) or not str(payload[field]).strip():
107
+ raise RuntimeAPIError("server returned an invalid device login")
108
+ complete = payload.get("verification_uri_complete")
109
+ if complete is not None and (not isinstance(complete, str) or not complete.strip()):
110
+ raise RuntimeAPIError("server returned an invalid device login")
111
+ for field in required_integers:
112
+ if not isinstance(payload.get(field), int) or int(payload[field]) <= 0:
113
+ raise RuntimeAPIError("server returned an invalid device login")
114
+ return payload
115
+
116
+
117
+ def _publish_device_state(device: dict[str, Any]) -> None:
118
+ raw_path = os.environ.get(DEVICE_STATE_ENV)
119
+ if not raw_path:
120
+ return
121
+ path = Path(raw_path)
122
+ public_state = {
123
+ key: device.get(key)
124
+ for key in (
125
+ "user_code",
126
+ "verification_uri",
127
+ "verification_uri_complete",
128
+ "expires_in",
129
+ "interval",
130
+ )
131
+ }
132
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
133
+ fd, temporary_path = tempfile.mkstemp(prefix=".device-", dir=path.parent)
134
+ try:
135
+ os.fchmod(fd, 0o600)
136
+ with os.fdopen(fd, "w", encoding="utf-8") as file:
137
+ json.dump(public_state, file)
138
+ file.write("\n")
139
+ file.flush()
140
+ os.fsync(file.fileno())
141
+ os.replace(temporary_path, path)
142
+ finally:
143
+ try:
144
+ os.unlink(temporary_path)
145
+ except FileNotFoundError:
146
+ pass
147
+
148
+
149
+ def handle_whoami(config: RuntimeConfig) -> int:
150
+ if (err := cli_output.require_auth(config)) is not None:
151
+ return err
152
+ return cli_output.report_success(authenticated_client(config).whoami())
153
+
154
+
155
+ def handle_tui_state(config: RuntimeConfig) -> int:
156
+ if (err := cli_output.require_auth(config)) is not None:
157
+ return err
158
+
159
+ client = authenticated_client(config)
160
+ identity = client.whoami()
161
+ computers = client.list_computers().get("computers") or []
162
+ provider_credentials = (
163
+ client.list_provider_credentials().get("provider_credentials") or []
164
+ )
165
+ github_installations = client.list_github_installations().get("installations") or []
166
+ default_network_policy = client.get_default_network()
167
+ return cli_output.report_success(
168
+ {
169
+ "identity": identity,
170
+ "computers": computers,
171
+ "provider_credentials": provider_credentials,
172
+ "github_installations": github_installations,
173
+ "default_network_policy": default_network_policy,
174
+ }
175
+ )
176
+
177
+
178
+ def handle_logout(config: RuntimeConfig) -> int:
179
+ environment_key = os.environ.get("RUNTIME_API_KEY", "").strip()
180
+ if environment_key:
181
+ try:
182
+ RuntimeClient(base_url=config.base_url, credential=environment_key).logout()
183
+ except RuntimeAPIError as exc:
184
+ if exc.status_code != 401:
185
+ raise
186
+ return cli_output.report_success(
187
+ {
188
+ "message": "environment API key logged out; unset RUNTIME_API_KEY to remove it",
189
+ }
190
+ )
191
+
192
+ if config.api_key or config.session:
193
+ try:
194
+ authenticated_client(config).logout()
195
+ except RuntimeAPIError as exc:
196
+ if exc.status_code != 401:
197
+ raise
198
+
199
+ clear_credentials(config)
200
+ message = "logged out"
201
+ return cli_output.report_success({"message": message})
202
+
203
+
204
+ def handle_api_keys_list(config: RuntimeConfig) -> int:
205
+ if (err := cli_output.require_interactive_session(config)) is not None:
206
+ return err
207
+ return cli_output.report_success(
208
+ authenticated_client(config, interactive=True).list_api_keys()
209
+ )
210
+
211
+
212
+ def handle_api_keys_create(config: RuntimeConfig, name: str | None) -> int:
213
+ if (err := cli_output.require_interactive_session(config)) is not None:
214
+ return err
215
+ if name is None:
216
+ if not cli_output.interactive():
217
+ return cli_output.report_error("name is required")
218
+ name = cli_output.prompt_text("API key name")
219
+ if not name:
220
+ return cli_output.report_error("name is required")
221
+
222
+ return cli_output.report_success(
223
+ authenticated_client(config, interactive=True).create_api_key(name)
224
+ )
225
+
226
+
227
+ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
228
+ if (err := cli_output.require_interactive_session(config)) is not None:
229
+ return err
230
+ if key_id is None:
231
+ if not cli_output.interactive():
232
+ return cli_output.report_error("api key id is required")
233
+ key_id = cli_output.prompt_text("API key id")
234
+ if not key_id:
235
+ return cli_output.report_error("api key id is required")
236
+
237
+ resolved_key_id = key_id.strip()
238
+ if not resolved_key_id:
239
+ return cli_output.report_error("api key id is required")
240
+ authenticated_client(config, interactive=True).revoke_api_key(resolved_key_id)
241
+ return cli_output.report_success(
242
+ {"id": resolved_key_id, "message": f"api key {resolved_key_id} revoked"}
243
+ )
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+
6
+ def command_from_remainder(parts: list[str] | None) -> str | None:
7
+ values = list(parts or [])
8
+ if values and values[0] == "--":
9
+ values = values[1:]
10
+ return shlex.join(values) if values else None
runtime_sdk/auth.py ADDED
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ import time
7
+ from typing import Any
8
+
9
+ from runtime_sdk.client import RuntimeAPIError, RuntimeClient
10
+ from runtime_sdk.config import (
11
+ RuntimeConfig,
12
+ RuntimeConfigError,
13
+ WorkOSSession,
14
+ _save_config_unlocked,
15
+ config_lock,
16
+ load_config,
17
+ save_config,
18
+ )
19
+
20
+
21
+ REFRESH_SKEW_SECONDS = 30
22
+
23
+
24
+ def has_credentials(config: RuntimeConfig, *, interactive: bool = False) -> bool:
25
+ if os.environ.get("RUNTIME_API_KEY", "").strip() and not interactive:
26
+ return True
27
+ _reload_credentials(config)
28
+ return config.session is not None if interactive else bool(config.api_key or config.session)
29
+
30
+
31
+ def authenticated_client(
32
+ config: RuntimeConfig, *, interactive: bool = False
33
+ ) -> RuntimeClient:
34
+ return RuntimeClient(
35
+ base_url=config.base_url,
36
+ credential=lambda: bearer_token(config, interactive=interactive),
37
+ )
38
+
39
+
40
+ def bearer_token(config: RuntimeConfig, *, interactive: bool = False) -> str:
41
+ environment_key = os.environ.get("RUNTIME_API_KEY", "").strip()
42
+ if environment_key and not interactive:
43
+ return environment_key
44
+
45
+ _reload_credentials(config)
46
+ if interactive and config.session is None:
47
+ raise RuntimeAPIError("interactive WorkOS login required", status_code=401)
48
+ if config.api_key:
49
+ return config.api_key
50
+ if config.session:
51
+ return _session_access_token(config)
52
+ raise RuntimeAPIError("not logged in; run runtime login", status_code=401)
53
+
54
+
55
+ def _reload_credentials(config: RuntimeConfig) -> None:
56
+ if not config._tracks_saved_credentials:
57
+ return
58
+ latest = load_config()
59
+ config.api_key = latest.api_key
60
+ config.session = latest.session
61
+
62
+
63
+ def session_from_payload(payload: dict[str, Any]) -> WorkOSSession:
64
+ fields = (
65
+ "access_token",
66
+ "refresh_token",
67
+ "organization_id",
68
+ "session_id",
69
+ )
70
+ if any(
71
+ not isinstance(payload.get(field), str) or not payload[field].strip()
72
+ for field in fields
73
+ ):
74
+ raise RuntimeAPIError("server returned an invalid WorkOS session")
75
+ try:
76
+ session = WorkOSSession(
77
+ access_token=payload["access_token"],
78
+ refresh_token=payload["refresh_token"],
79
+ organization_id=payload["organization_id"],
80
+ session_id=payload["session_id"],
81
+ )
82
+ except (KeyError, TypeError, ValueError) as exc:
83
+ raise RuntimeAPIError("server returned an invalid WorkOS session") from exc
84
+ try:
85
+ fresh = _token_is_fresh(session.access_token)
86
+ except RuntimeConfigError as exc:
87
+ raise RuntimeAPIError("server returned an invalid WorkOS session") from exc
88
+ if not fresh:
89
+ raise RuntimeAPIError("server returned an expired WorkOS session")
90
+ return session
91
+
92
+
93
+ def save_session(config: RuntimeConfig, session: WorkOSSession) -> None:
94
+ config.session = session
95
+ config.api_key = None
96
+ save_config(config)
97
+
98
+
99
+ def save_api_key(config: RuntimeConfig, api_key: str) -> None:
100
+ config.api_key = api_key
101
+ config.session = None
102
+ save_config(config)
103
+
104
+
105
+ def clear_credentials(config: RuntimeConfig) -> None:
106
+ config.session = None
107
+ config.api_key = None
108
+ save_config(config)
109
+
110
+
111
+ def _session_access_token(config: RuntimeConfig) -> str:
112
+ if not config._tracks_saved_credentials:
113
+ if config.session is None:
114
+ raise RuntimeAPIError("not logged in; run runtime login", status_code=401)
115
+ if _stored_token_is_fresh(config.session.access_token):
116
+ return config.session.access_token
117
+ config.session = _refresh_workos_session(config.base_url, config.session)
118
+ config.api_key = None
119
+ return config.session.access_token
120
+
121
+ latest = load_config()
122
+ if latest.session is None:
123
+ raise RuntimeAPIError("not logged in; run runtime login", status_code=401)
124
+ config.session = latest.session
125
+ config.api_key = None
126
+ if _stored_token_is_fresh(latest.session.access_token):
127
+ return latest.session.access_token
128
+
129
+ with config_lock():
130
+ latest = load_config()
131
+ if latest.session is None:
132
+ raise RuntimeAPIError("WorkOS session is no longer available", status_code=401)
133
+ if _stored_token_is_fresh(latest.session.access_token):
134
+ config.session = latest.session
135
+ config.api_key = None
136
+ return latest.session.access_token
137
+
138
+ refreshed = _refresh_workos_session(config.base_url, latest.session)
139
+ latest.session = refreshed
140
+ latest.api_key = None
141
+ _save_config_unlocked(latest)
142
+ config.session = refreshed
143
+ config.api_key = None
144
+ return refreshed.access_token
145
+
146
+
147
+ def _refresh_workos_session(base_url: str, session: WorkOSSession) -> WorkOSSession:
148
+ payload = RuntimeClient(base_url=base_url).refresh_session(
149
+ session.refresh_token,
150
+ session.organization_id,
151
+ )
152
+ refreshed = session_from_payload(payload)
153
+ if refreshed.organization_id != session.organization_id:
154
+ raise RuntimeAPIError("WorkOS refresh changed organizations", status_code=401)
155
+ return refreshed
156
+
157
+
158
+ def _token_is_fresh(token: str) -> bool:
159
+ try:
160
+ encoded_payload = token.split(".")[1]
161
+ encoded_payload += "=" * (-len(encoded_payload) % 4)
162
+ payload = json.loads(base64.urlsafe_b64decode(encoded_payload))
163
+ expires_at = payload["exp"]
164
+ except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
165
+ raise RuntimeConfigError("stored WorkOS access token is invalid") from exc
166
+ if not isinstance(expires_at, (int, float)):
167
+ raise RuntimeConfigError("stored WorkOS access token has an invalid expiry")
168
+ return float(expires_at) > time.time() + REFRESH_SKEW_SECONDS
169
+
170
+
171
+ def _stored_token_is_fresh(token: str) -> bool:
172
+ try:
173
+ return _token_is_fresh(token)
174
+ except RuntimeConfigError:
175
+ return False