@qingflow-tech/qingflow-cli 1.1.43-beta.5 → 1.1.44-beta.1

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.
@@ -26,6 +26,7 @@ AUTH_CONTEXT_KEY_PREFIX = "mcp:auth:context"
26
26
  AUTH_CONTEXT_LOCK_KEY_PREFIX = "mcp:auth:context:lock"
27
27
  AUTH_CONTEXT_NEGATIVE_KEY_PREFIX = "mcp:auth:context:negative"
28
28
  ACCESS_PLATFORM_KEY_PREFIX = "oauth2:access-platform:"
29
+ ACCESS_CONTEXT_KEY_PREFIX = "auth:access-context:"
29
30
 
30
31
 
31
32
  @dataclass(slots=True)
@@ -108,12 +109,31 @@ class CloudAuthBroker:
108
109
  normalized_token = self._normalize_text(access_token)
109
110
  if normalized_token is None:
110
111
  return ""
112
+ access_context = self.resolve_access_context(normalized_token)
113
+ if access_context is not None:
114
+ source_code = self._normalize_text(access_context.get("source_code"))
115
+ if source_code is not None:
116
+ return source_code
111
117
  try:
112
118
  value = self.cache.get_text(f"{ACCESS_PLATFORM_KEY_PREFIX}{normalized_token}")
113
119
  except QingflowApiError:
114
120
  return ""
115
121
  return self._normalize_text(value) or ""
116
122
 
123
+ def resolve_access_context(self, access_token: str) -> JSONObject | None:
124
+ normalized_token = self._normalize_text(access_token)
125
+ if normalized_token is None:
126
+ return None
127
+ try:
128
+ value = self.cache.get_json(
129
+ f"{ACCESS_CONTEXT_KEY_PREFIX}{sha256(normalized_token.encode('utf-8')).hexdigest()}"
130
+ )
131
+ except QingflowApiError:
132
+ return None
133
+ if not isinstance(value, dict):
134
+ return None
135
+ return value
136
+
117
137
  @staticmethod
118
138
  def credential_hash(credential: str) -> str:
119
139
  return sha256(credential.encode("utf-8")).hexdigest()
@@ -26,6 +26,7 @@ AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"
26
26
  OAUTH_ENDPOINT_PATHS = {
27
27
  "authorization_endpoint": "/oauth/authorize",
28
28
  "token_endpoint": "/oauth/token",
29
+ "revocation_endpoint": "/oauth/revoke",
29
30
  "registration_endpoint": "/oauth/register",
30
31
  }
31
32
  FORWARDED_REQUEST_HEADERS = {"accept", "content-type"}
@@ -12,6 +12,7 @@ DEFAULT_USER_AGENT = "qingflow-mcp/1.0"
12
12
  DEFAULT_RECORD_LIST_TYPE = 8
13
13
  ATTACHMENT_QUESTION_TYPE = 13
14
14
  DEFAULT_BASE_URL = "https://qingflow.com/api"
15
+ DEFAULT_CLI_OAUTH_RESOURCE_URL = "https://mcp.qingflow.com/mcp"
15
16
  DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
16
17
  DEFAULT_FEEDBACK_QSOURCE_TOKEN = "mcp-feedback-7755d14748fc"
17
18
  DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE = "git@hackers.oalite.com:{group}/{repo}.git"
@@ -165,6 +166,17 @@ def get_default_qf_version() -> str | None:
165
166
  return normalized or None
166
167
 
167
168
 
169
+ def get_cli_oauth_resource_url() -> str:
170
+ """获取 CLI OAuth 受保护资源地址。"""
171
+ value = get_config_value(
172
+ "cli.oauth.resource_url",
173
+ env_var="QINGFLOW_MCP_OAUTH_RESOURCE_URL",
174
+ default=DEFAULT_CLI_OAUTH_RESOURCE_URL,
175
+ )
176
+ normalized = normalize_base_url(str(value or "").strip())
177
+ return normalized or DEFAULT_CLI_OAUTH_RESOURCE_URL
178
+
179
+
168
180
  def get_feedback_qsource_token() -> str | None:
169
181
  """获取反馈 q-source 被动入口 token"""
170
182
  value = get_config_value(
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from contextlib import contextmanager
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Iterator, Literal
11
+ from uuid import uuid4
12
+
13
+ from .config import get_mcp_home, normalize_base_url
14
+ from .errors import QingflowApiError
15
+
16
+
17
+ OAUTH_PROFILE_SCHEMA_VERSION = 1
18
+ OAuthStateValue = Literal["oauth", "none"]
19
+
20
+
21
+ def _utcnow() -> str:
22
+ return datetime.now(timezone.utc).isoformat()
23
+
24
+
25
+ def profile_storage_key(profile: str) -> str:
26
+ return hashlib.sha256(profile.encode("utf-8")).hexdigest()
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class OAuthProfileState:
31
+ active_auth_type: OAuthStateValue
32
+ profile: str
33
+ base_url: str | None = None
34
+ qf_version: str | None = None
35
+ qf_version_source: str | None = None
36
+ uid: int | None = None
37
+ email: str | None = None
38
+ nick_name: str | None = None
39
+ selected_ws_id: int | None = None
40
+ selected_ws_name: str | None = None
41
+ persisted: bool = True
42
+ secret_storage: str | None = None
43
+ issuer: str | None = None
44
+ client_id: str | None = None
45
+ resource: str | None = None
46
+ source_code: str | None = None
47
+ access_expires_at: str | None = None
48
+ created_at: str = ""
49
+ updated_at: str = ""
50
+
51
+ @classmethod
52
+ def from_dict(cls, profile: str, value: object) -> "OAuthProfileState":
53
+ if not isinstance(value, dict):
54
+ raise _config_error(profile, "profile entry must be an object")
55
+ active_auth_type = value.get("active_auth_type")
56
+ if active_auth_type not in {"oauth", "none"}:
57
+ raise _config_error(profile, "active_auth_type must be 'oauth' or 'none'")
58
+ stored_profile = value.get("profile")
59
+ if stored_profile != profile:
60
+ raise _config_error(profile, "stored profile name does not match the requested profile")
61
+ secret_storage = _normalized_text(value.get("secret_storage"))
62
+ if active_auth_type == "oauth":
63
+ required_text = ("base_url", "issuer", "client_id", "resource", "source_code", "access_expires_at")
64
+ missing = [name for name in required_text if not _normalized_text(value.get(name))]
65
+ if secret_storage not in {"keyring", "file"}:
66
+ missing.append("secret_storage")
67
+ uid = _positive_int(value.get("uid"))
68
+ if uid is None:
69
+ missing.append("uid")
70
+ if missing:
71
+ raise _config_error(profile, "missing OAuth fields: " + ", ".join(missing))
72
+ created_at = _normalized_text(value.get("created_at")) or _utcnow()
73
+ updated_at = _normalized_text(value.get("updated_at")) or created_at
74
+ return cls(
75
+ active_auth_type=active_auth_type,
76
+ profile=profile,
77
+ base_url=_normalized_text(value.get("base_url")),
78
+ qf_version=_normalized_text(value.get("qf_version")),
79
+ qf_version_source=_normalized_text(value.get("qf_version_source")),
80
+ uid=_positive_int(value.get("uid")),
81
+ email=_normalized_text(value.get("email")),
82
+ nick_name=_normalized_text(value.get("nick_name")),
83
+ selected_ws_id=_positive_int(value.get("selected_ws_id")),
84
+ selected_ws_name=_normalized_text(value.get("selected_ws_name")),
85
+ persisted=bool(value.get("persisted", True)),
86
+ secret_storage=secret_storage,
87
+ issuer=_normalized_text(value.get("issuer")),
88
+ client_id=_normalized_text(value.get("client_id")),
89
+ resource=_normalized_text(value.get("resource")),
90
+ source_code=_normalized_text(value.get("source_code")),
91
+ access_expires_at=_normalized_text(value.get("access_expires_at")),
92
+ created_at=created_at,
93
+ updated_at=updated_at,
94
+ )
95
+
96
+ def to_dict(self) -> dict[str, object]:
97
+ return asdict(self)
98
+
99
+
100
+ class OAuthProfileStore:
101
+ def __init__(self, base_dir: Path | None = None) -> None:
102
+ self.base_dir = Path(base_dir) if base_dir is not None else get_mcp_home()
103
+ self._profiles_dir = self.base_dir / "oauth-profiles"
104
+ self._locks_dir = self.base_dir / "locks"
105
+
106
+ def get(self, profile: str) -> OAuthProfileState | None:
107
+ path = self.profile_path(profile)
108
+ if not path.exists():
109
+ return None
110
+ try:
111
+ with path.open("r", encoding="utf-8") as handle:
112
+ payload = json.load(handle)
113
+ except (OSError, json.JSONDecodeError) as exc:
114
+ raise _config_error(profile, f"state file cannot be read: {exc}") from exc
115
+ if not isinstance(payload, dict) or payload.get("schema_version") != OAUTH_PROFILE_SCHEMA_VERSION:
116
+ raise _config_error(profile, "unsupported or missing schema_version")
117
+ profiles = payload.get("profiles")
118
+ if not isinstance(profiles, dict) or profile not in profiles:
119
+ raise _config_error(profile, "state file does not contain the requested profile")
120
+ return OAuthProfileState.from_dict(profile, profiles[profile])
121
+
122
+ def save(self, state: OAuthProfileState, *, locked: bool = False) -> None:
123
+ if locked:
124
+ self._save_locked(state)
125
+ return
126
+ with self.lock(state.profile):
127
+ self._save_locked(state)
128
+
129
+ def set_none(self, profile: str, *, locked: bool = False) -> OAuthProfileState:
130
+ now = _utcnow()
131
+ try:
132
+ previous = self.get(profile)
133
+ except QingflowApiError:
134
+ previous = None
135
+ state = OAuthProfileState(
136
+ active_auth_type="none",
137
+ profile=profile,
138
+ persisted=True,
139
+ created_at=previous.created_at if previous is not None else now,
140
+ updated_at=now,
141
+ )
142
+ self.save(state, locked=locked)
143
+ return state
144
+
145
+ def delete(self, profile: str, *, locked: bool = False) -> None:
146
+ if not locked:
147
+ with self.lock(profile):
148
+ self.delete(profile, locked=True)
149
+ return
150
+ try:
151
+ self.profile_path(profile).unlink()
152
+ except FileNotFoundError:
153
+ return
154
+ except OSError as exc:
155
+ raise _config_error(profile, f"state file cannot be removed: {exc}") from exc
156
+
157
+ @contextmanager
158
+ def lock(self, profile: str) -> Iterator[None]:
159
+ lock_path = self._locks_dir / f"{profile_storage_key(profile)}.oauth.lock"
160
+ with _FileLock(lock_path):
161
+ yield
162
+
163
+ def profile_path(self, profile: str) -> Path:
164
+ return self._profiles_dir / f"{profile_storage_key(profile)}.json"
165
+
166
+ def _save_locked(self, state: OAuthProfileState) -> None:
167
+ normalized_base_url = normalize_base_url(state.base_url) if state.base_url else None
168
+ if state.active_auth_type == "oauth" and not normalized_base_url:
169
+ raise _config_error(state.profile, "base_url is invalid")
170
+ state.base_url = normalized_base_url
171
+ state.updated_at = _utcnow()
172
+ if not state.created_at:
173
+ state.created_at = state.updated_at
174
+ payload = {
175
+ "schema_version": OAUTH_PROFILE_SCHEMA_VERSION,
176
+ "profiles": {state.profile: state.to_dict()},
177
+ }
178
+ _atomic_json_write(self.profile_path(state.profile), payload)
179
+
180
+
181
+ class _FileLock:
182
+ def __init__(self, path: Path) -> None:
183
+ self.path = path
184
+ self._handle = None
185
+
186
+ def __enter__(self) -> "_FileLock":
187
+ self.path.parent.mkdir(parents=True, exist_ok=True)
188
+ self._handle = self.path.open("a+b")
189
+ self._handle.seek(0, os.SEEK_END)
190
+ if self._handle.tell() == 0:
191
+ self._handle.write(b"0")
192
+ self._handle.flush()
193
+ self._handle.seek(0)
194
+ if os.name == "nt":
195
+ import msvcrt
196
+
197
+ msvcrt.locking(self._handle.fileno(), msvcrt.LK_LOCK, 1)
198
+ else:
199
+ import fcntl
200
+
201
+ fcntl.flock(self._handle.fileno(), fcntl.LOCK_EX)
202
+ return self
203
+
204
+ def __exit__(self, _exc_type, _exc, _traceback) -> None:
205
+ if self._handle is None:
206
+ return
207
+ try:
208
+ self._handle.seek(0)
209
+ if os.name == "nt":
210
+ import msvcrt
211
+
212
+ msvcrt.locking(self._handle.fileno(), msvcrt.LK_UNLCK, 1)
213
+ else:
214
+ import fcntl
215
+
216
+ fcntl.flock(self._handle.fileno(), fcntl.LOCK_UN)
217
+ finally:
218
+ self._handle.close()
219
+ self._handle = None
220
+
221
+
222
+ def _atomic_json_write(path: Path, payload: object) -> None:
223
+ path.parent.mkdir(parents=True, exist_ok=True)
224
+ temporary = path.parent / f".{path.name}.{os.getpid()}.{uuid4().hex}.tmp"
225
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
226
+ try:
227
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
228
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
229
+ handle.flush()
230
+ os.fsync(handle.fileno())
231
+ os.replace(temporary, path)
232
+ try:
233
+ os.chmod(path, 0o600)
234
+ except OSError:
235
+ pass
236
+ finally:
237
+ try:
238
+ temporary.unlink()
239
+ except FileNotFoundError:
240
+ pass
241
+
242
+
243
+ def _normalized_text(value: object) -> str | None:
244
+ if value is None:
245
+ return None
246
+ normalized = str(value).strip()
247
+ return normalized or None
248
+
249
+
250
+ def _positive_int(value: object) -> int | None:
251
+ if value is None or isinstance(value, bool):
252
+ return None
253
+ try:
254
+ parsed = int(value)
255
+ except (TypeError, ValueError):
256
+ return None
257
+ return parsed if parsed > 0 else None
258
+
259
+
260
+ def _config_error(profile: str, reason: str) -> QingflowApiError:
261
+ return QingflowApiError.config_error(
262
+ f"OAuth state for profile '{profile}' is invalid: {reason}.",
263
+ details={"reason": "oauth_state_invalid", "profile": profile},
264
+ )
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+
7
+ try:
8
+ import keyring
9
+ except ImportError:
10
+ keyring = None
11
+
12
+ from .errors import QingflowApiError
13
+ from .json_types import KeyringBackend
14
+ from .oauth_profile_store import OAuthProfileStore, _atomic_json_write, profile_storage_key
15
+ from .session_store import KEYRING_SERVICE_NAME
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class OAuthTokenBundle:
20
+ access_token: str
21
+ refresh_token: str
22
+ access_expires_at: str
23
+ generation: int
24
+
25
+ @classmethod
26
+ def from_json(cls, profile: str, value: object) -> "OAuthTokenBundle":
27
+ if not isinstance(value, dict):
28
+ raise _bundle_error(profile, "bundle must be an object")
29
+ access_token = _text(value.get("access_token"))
30
+ refresh_token = _text(value.get("refresh_token"))
31
+ access_expires_at = _text(value.get("access_expires_at"))
32
+ generation_value = value.get("generation")
33
+ if isinstance(generation_value, bool):
34
+ generation = -1
35
+ else:
36
+ try:
37
+ generation = int(generation_value)
38
+ except (TypeError, ValueError):
39
+ generation = -1
40
+ missing = []
41
+ if not access_token:
42
+ missing.append("access_token")
43
+ if not refresh_token:
44
+ missing.append("refresh_token")
45
+ if not access_expires_at:
46
+ missing.append("access_expires_at")
47
+ if generation < 0:
48
+ missing.append("generation")
49
+ if missing:
50
+ raise _bundle_error(profile, "missing or invalid fields: " + ", ".join(missing))
51
+ return cls(
52
+ access_token=access_token,
53
+ refresh_token=refresh_token,
54
+ access_expires_at=access_expires_at,
55
+ generation=generation,
56
+ )
57
+
58
+ def to_json(self) -> dict[str, object]:
59
+ return asdict(self)
60
+
61
+
62
+ class OAuthTokenStore:
63
+ def __init__(
64
+ self,
65
+ profile_store: OAuthProfileStore,
66
+ *,
67
+ keyring_backend: KeyringBackend | None = None,
68
+ ) -> None:
69
+ self.profile_store = profile_store
70
+ self._keyring = keyring_backend if keyring_backend is not None else keyring
71
+ self._secrets_dir = profile_store.base_dir / "oauth-secrets"
72
+
73
+ def load(self, profile: str) -> OAuthTokenBundle | None:
74
+ state = self.profile_store.get(profile)
75
+ if state is not None and state.active_auth_type == "none":
76
+ return None
77
+ if state is not None and state.secret_storage == "file":
78
+ return self._load_file(profile)
79
+ if state is not None and state.secret_storage == "keyring":
80
+ keyring_value = self._get_keyring(profile)
81
+ return self._decode(profile, keyring_value, storage="keyring") if keyring_value is not None else None
82
+ keyring_value = self._get_keyring(profile)
83
+ if keyring_value is not None:
84
+ return self._decode(profile, keyring_value, storage="keyring")
85
+ return self._load_file(profile)
86
+
87
+ def _load_file(self, profile: str) -> OAuthTokenBundle | None:
88
+ path = self.secret_path(profile)
89
+ if not path.exists():
90
+ return None
91
+ try:
92
+ with path.open("r", encoding="utf-8") as handle:
93
+ payload = json.load(handle)
94
+ except (OSError, json.JSONDecodeError) as exc:
95
+ raise _bundle_error(profile, f"file bundle cannot be read: {exc}") from exc
96
+ return OAuthTokenBundle.from_json(profile, payload)
97
+
98
+ def save(
99
+ self,
100
+ profile: str,
101
+ bundle: OAuthTokenBundle,
102
+ *,
103
+ storage: str | None = None,
104
+ locked: bool = False,
105
+ ) -> str:
106
+ if not locked:
107
+ with self.profile_store.lock(profile):
108
+ state = self.profile_store.get(profile)
109
+ selected_storage = storage
110
+ if selected_storage is None and state is not None and state.active_auth_type == "oauth":
111
+ selected_storage = state.secret_storage
112
+ saved_storage = self.save(profile, bundle, storage=selected_storage, locked=True)
113
+ if state is not None and state.active_auth_type == "oauth":
114
+ state.secret_storage = saved_storage
115
+ state.access_expires_at = bundle.access_expires_at
116
+ self.profile_store.save(state, locked=True)
117
+ return saved_storage
118
+ if storage not in {None, "keyring", "file"}:
119
+ raise _bundle_error(profile, f"unsupported secret storage: {storage}")
120
+ encoded = json.dumps(bundle.to_json(), ensure_ascii=False, separators=(",", ":"))
121
+ if storage == "keyring":
122
+ if not self._set_keyring(profile, encoded):
123
+ raise _bundle_error(profile, "keyring write failed")
124
+ self._delete_file(profile)
125
+ return "keyring"
126
+ if storage == "file":
127
+ self._save_file(profile, bundle)
128
+ self._delete_keyring(profile)
129
+ return "file"
130
+ if self._set_keyring(profile, encoded):
131
+ self._delete_file(profile)
132
+ return "keyring"
133
+ self._save_file(profile, bundle)
134
+ self._delete_keyring(profile)
135
+ return "file"
136
+
137
+ def delete(self, profile: str, *, locked: bool = False) -> None:
138
+ if not locked:
139
+ with self.profile_store.lock(profile):
140
+ self.delete(profile, locked=True)
141
+ return
142
+ self._delete_keyring(profile)
143
+ self._delete_file(profile)
144
+
145
+ def secret_path(self, profile: str) -> Path:
146
+ return self._secrets_dir / f"{profile_storage_key(profile)}.json"
147
+
148
+ def _decode(self, profile: str, encoded: str, *, storage: str) -> OAuthTokenBundle:
149
+ try:
150
+ value = json.loads(encoded)
151
+ except json.JSONDecodeError as exc:
152
+ raise _bundle_error(profile, f"{storage} bundle is not valid JSON") from exc
153
+ return OAuthTokenBundle.from_json(profile, value)
154
+
155
+ def _key(self, profile: str) -> str:
156
+ return f"{profile}:oauth-token-set"
157
+
158
+ def _get_keyring(self, profile: str) -> str | None:
159
+ if self._keyring is None:
160
+ return None
161
+ try:
162
+ value = self._keyring.get_password(KEYRING_SERVICE_NAME, self._key(profile))
163
+ except Exception:
164
+ return None
165
+ return value if value else None
166
+
167
+ def _set_keyring(self, profile: str, value: str) -> bool:
168
+ if self._keyring is None:
169
+ return False
170
+ try:
171
+ self._keyring.set_password(KEYRING_SERVICE_NAME, self._key(profile), value)
172
+ except Exception:
173
+ return False
174
+ return True
175
+
176
+ def _delete_keyring(self, profile: str) -> None:
177
+ if self._keyring is None:
178
+ return
179
+ try:
180
+ self._keyring.delete_password(KEYRING_SERVICE_NAME, self._key(profile))
181
+ except Exception:
182
+ pass
183
+
184
+ def _save_file(self, profile: str, bundle: OAuthTokenBundle) -> None:
185
+ try:
186
+ _atomic_json_write(self.secret_path(profile), bundle.to_json())
187
+ except OSError as exc:
188
+ raise _bundle_error(profile, f"file bundle cannot be written: {exc}") from exc
189
+
190
+ def _delete_file(self, profile: str) -> None:
191
+ try:
192
+ self.secret_path(profile).unlink()
193
+ except FileNotFoundError:
194
+ return
195
+ except OSError as exc:
196
+ raise _bundle_error(profile, f"file bundle cannot be removed: {exc}") from exc
197
+
198
+
199
+ def _text(value: object) -> str | None:
200
+ if value is None:
201
+ return None
202
+ normalized = str(value).strip()
203
+ return normalized or None
204
+
205
+
206
+ def _bundle_error(profile: str, reason: str) -> QingflowApiError:
207
+ return QingflowApiError.config_error(
208
+ f"OAuth token bundle for profile '{profile}' is invalid: {reason}.",
209
+ details={"reason": "oauth_token_bundle_invalid", "profile": profile},
210
+ )
@@ -109,9 +109,22 @@ class CloudMcpAuthApp:
109
109
  and self._oauth_token_resolver is not None
110
110
  and not auth_value.startswith("mcp_")
111
111
  )
112
+ access_context: dict[str, Any] | None = None
112
113
  try:
113
114
  if use_oauth:
114
115
  assert self._oauth_token_resolver is not None
116
+ access_context = await anyio.to_thread.run_sync(
117
+ self._broker.resolve_access_context,
118
+ auth_value,
119
+ )
120
+ if access_context is not None:
121
+ assert self._oauth_proxy is not None
122
+ token_resource = str(access_context.get("resource") or "").strip()
123
+ if token_resource != self._oauth_proxy.config.resource_url:
124
+ raise QingflowApiError(
125
+ category="auth",
126
+ message="OAuth access token was issued for a different protected resource",
127
+ )
115
128
  cloud_context = await anyio.to_thread.run_sync(
116
129
  self._resolve_oauth_or_legacy_credential,
117
130
  auth_value,
@@ -138,7 +151,14 @@ class CloudMcpAuthApp:
138
151
  )
139
152
  return
140
153
 
141
- if not call_platform:
154
+ trusted_source = (
155
+ str(access_context.get("source_code") or "").strip()
156
+ if access_context is not None
157
+ else ""
158
+ )
159
+ if trusted_source:
160
+ call_platform = trusted_source
161
+ elif not call_platform:
142
162
  call_platform = await anyio.to_thread.run_sync(
143
163
  self._broker.resolve_call_platform,
144
164
  cloud_context.token,