airbyte-internal-ops 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.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.
Files changed (41) hide show
  1. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/METADATA +19 -3
  2. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/RECORD +41 -41
  3. airbyte_ops_mcp/__init__.py +2 -2
  4. airbyte_ops_mcp/cli/cloud.py +207 -306
  5. airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
  6. airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
  7. airbyte_ops_mcp/constants.py +61 -1
  8. airbyte_ops_mcp/github_actions.py +69 -1
  9. airbyte_ops_mcp/mcp/_http_headers.py +56 -0
  10. airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
  11. airbyte_ops_mcp/mcp/cloud_connector_versions.py +57 -43
  12. airbyte_ops_mcp/mcp/github.py +34 -1
  13. airbyte_ops_mcp/mcp/prerelease.py +3 -3
  14. airbyte_ops_mcp/mcp/prod_db_queries.py +293 -50
  15. airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +158 -176
  16. airbyte_ops_mcp/mcp/server.py +3 -3
  17. airbyte_ops_mcp/prod_db_access/db_engine.py +7 -11
  18. airbyte_ops_mcp/prod_db_access/queries.py +79 -0
  19. airbyte_ops_mcp/prod_db_access/sql.py +86 -0
  20. airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
  21. airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
  22. airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
  23. airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
  24. airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
  25. airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
  26. airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
  27. airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
  28. airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
  29. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/WHEEL +0 -0
  30. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/entry_points.txt +0 -0
  31. /airbyte_ops_mcp/{live_tests → regression_tests}/ci_output.py +0 -0
  32. /airbyte_ops_mcp/{live_tests → regression_tests}/commons/__init__.py +0 -0
  33. /airbyte_ops_mcp/{live_tests → regression_tests}/config.py +0 -0
  34. /airbyte_ops_mcp/{live_tests → regression_tests}/connection_fetcher.py +0 -0
  35. /airbyte_ops_mcp/{live_tests → regression_tests}/evaluation_modes.py +0 -0
  36. /airbyte_ops_mcp/{live_tests → regression_tests}/http_metrics.py +0 -0
  37. /airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/duckdb_cache.py +0 -0
  38. /airbyte_ops_mcp/{live_tests → regression_tests}/models.py +0 -0
  39. /airbyte_ops_mcp/{live_tests → regression_tests}/obfuscation.py +0 -0
  40. /airbyte_ops_mcp/{live_tests → regression_tests}/regression/comparators.py +0 -0
  41. /airbyte_ops_mcp/{live_tests → regression_tests}/validation/catalog_validators.py +0 -0
@@ -48,21 +48,36 @@ class _OriginType(str, Enum):
48
48
 
49
49
 
50
50
  def _get_access_token(
51
- client_id: str,
52
- client_secret: str,
51
+ client_id: str | None = None,
52
+ client_secret: str | None = None,
53
+ bearer_token: str | None = None,
53
54
  ) -> str:
54
55
  """Get an access token for Airbyte Cloud API.
55
56
 
57
+ If a bearer_token is provided, it is returned directly (already an access token).
58
+ Otherwise, exchanges client_id/client_secret for an access token.
59
+
56
60
  Args:
57
- client_id: The Airbyte Cloud client ID
58
- client_secret: The Airbyte Cloud client secret
61
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
62
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
63
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
59
64
 
60
65
  Returns:
61
66
  Access token string
62
67
 
63
68
  Raises:
64
- PyAirbyteInputError: If authentication fails
69
+ PyAirbyteInputError: If authentication fails or no valid credentials provided
65
70
  """
71
+ # If bearer token provided, use it directly
72
+ if bearer_token:
73
+ return bearer_token
74
+
75
+ # Otherwise, exchange client credentials for access token
76
+ if not client_id or not client_secret:
77
+ raise PyAirbyteInputError(
78
+ message="Either bearer_token or both client_id and client_secret must be provided",
79
+ )
80
+
66
81
  # Always authenticate via the public API endpoint
67
82
  auth_url = f"{constants.CLOUD_API_ROOT}/applications/token"
68
83
  response = requests.post(
@@ -90,16 +105,18 @@ def _get_access_token(
90
105
  def get_user_id_by_email(
91
106
  email: str,
92
107
  api_root: str,
93
- client_id: str,
94
- client_secret: str,
108
+ client_id: str | None = None,
109
+ client_secret: str | None = None,
110
+ bearer_token: str | None = None,
95
111
  ) -> str:
96
112
  """Get user ID from email address.
97
113
 
98
114
  Args:
99
115
  email: The user's email address
100
116
  api_root: The API root URL
101
- client_id: The Airbyte Cloud client ID
102
- client_secret: The Airbyte Cloud client secret
117
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
118
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
119
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
103
120
 
104
121
  Returns:
105
122
  User ID (UUID string)
@@ -107,7 +124,7 @@ def get_user_id_by_email(
107
124
  Raises:
108
125
  PyAirbyteInputError: If user not found or API request fails
109
126
  """
110
- access_token = _get_access_token(client_id, client_secret)
127
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
111
128
 
112
129
  endpoint = f"{api_root}/users/list_instance_admin"
113
130
  response = requests.post(
@@ -152,8 +169,9 @@ def resolve_connector_version_id(
152
169
  connector_type: Literal["source", "destination"],
153
170
  version: str,
154
171
  api_root: str,
155
- client_id: str,
156
- client_secret: str,
172
+ client_id: str | None = None,
173
+ client_secret: str | None = None,
174
+ bearer_token: str | None = None,
157
175
  ) -> str:
158
176
  """Resolve a version string to an actor_definition_version_id.
159
177
 
@@ -162,8 +180,9 @@ def resolve_connector_version_id(
162
180
  connector_type: Either "source" or "destination"
163
181
  version: The version string (e.g., "0.1.47-preview.abe7cb4")
164
182
  api_root: The API root URL
165
- client_id: The Airbyte Cloud client ID
166
- client_secret: The Airbyte Cloud client secret
183
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
184
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
185
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
167
186
 
168
187
  Returns:
169
188
  Version ID (UUID string)
@@ -171,7 +190,7 @@ def resolve_connector_version_id(
171
190
  Raises:
172
191
  PyAirbyteInputError: If version cannot be resolved or API request fails
173
192
  """
174
- access_token = _get_access_token(client_id, client_secret)
193
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
175
194
 
176
195
  endpoint = f"{api_root}/actor_definition_versions/resolve"
177
196
  payload = {
@@ -223,8 +242,9 @@ def get_connector_version(
223
242
  connector_id: str,
224
243
  connector_type: Literal["source", "destination"],
225
244
  api_root: str,
226
- client_id: str,
227
- client_secret: str,
245
+ client_id: str | None = None,
246
+ client_secret: str | None = None,
247
+ bearer_token: str | None = None,
228
248
  ) -> dict[str, Any]:
229
249
  """Get version information for a deployed connector.
230
250
 
@@ -232,8 +252,9 @@ def get_connector_version(
232
252
  connector_id: The ID of the deployed connector (source or destination)
233
253
  connector_type: Either "source" or "destination"
234
254
  api_root: The API root URL
235
- client_id: The Airbyte Cloud client ID
236
- client_secret: The Airbyte Cloud client secret
255
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
256
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
257
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
237
258
 
238
259
  Returns:
239
260
  Dictionary containing:
@@ -243,7 +264,7 @@ def get_connector_version(
243
264
  Raises:
244
265
  PyAirbyteInputError: If the API request fails
245
266
  """
246
- access_token = _get_access_token(client_id, client_secret)
267
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
247
268
 
248
269
  # Determine endpoint based on connector type
249
270
  # api_root already includes /v1
@@ -294,14 +315,15 @@ def set_connector_version_override(
294
315
  connector_id: str,
295
316
  connector_type: Literal["source", "destination"],
296
317
  api_root: str,
297
- client_id: str,
298
- client_secret: str,
299
- workspace_id: str,
318
+ client_id: str | None = None,
319
+ client_secret: str | None = None,
320
+ workspace_id: str | None = None,
300
321
  version: str | None = None,
301
322
  unset: bool = False,
302
323
  override_reason: str | None = None,
303
324
  override_reason_reference_url: str | None = None,
304
325
  user_email: str | None = None,
326
+ bearer_token: str | None = None,
305
327
  ) -> bool:
306
328
  """Set or clear a version override for a deployed connector.
307
329
 
@@ -309,14 +331,15 @@ def set_connector_version_override(
309
331
  connector_id: The ID of the deployed connector
310
332
  connector_type: Either "source" or "destination"
311
333
  api_root: The API root URL
312
- client_id: The Airbyte Cloud client ID
313
- client_secret: The Airbyte Cloud client secret
334
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
335
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
314
336
  workspace_id: The workspace ID
315
337
  version: The version to pin to (e.g., "0.1.0"), or None to unset
316
338
  unset: If True, removes any existing override
317
339
  override_reason: Required when setting. Explanation for the override
318
340
  override_reason_reference_url: Optional URL with more context
319
341
  user_email: Email of user creating the override
342
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
320
343
 
321
344
  Returns:
322
345
  True if operation succeeded, False if no override existed (unset only)
@@ -335,7 +358,7 @@ def set_connector_version_override(
335
358
  message="override_reason is required when setting a version and must be at least 10 characters",
336
359
  )
337
360
 
338
- access_token = _get_access_token(client_id, client_secret)
361
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
339
362
 
340
363
  # Build the scoped configuration
341
364
  scope_type = _ScopeType.ACTOR
@@ -479,6 +502,7 @@ def set_connector_version_override(
479
502
  api_root=api_root,
480
503
  client_id=client_id,
481
504
  client_secret=client_secret,
505
+ bearer_token=bearer_token,
482
506
  )
483
507
 
484
508
  # Get user ID from email if provided
@@ -489,6 +513,7 @@ def set_connector_version_override(
489
513
  api_root=api_root,
490
514
  client_id=client_id,
491
515
  client_secret=client_secret,
516
+ bearer_token=bearer_token,
492
517
  )
493
518
 
494
519
  # Create the override with correct schema
@@ -13,8 +13,8 @@ from pathlib import Path
13
13
 
14
14
  from pydantic import BaseModel, Field
15
15
 
16
- from airbyte_ops_mcp.live_tests.connection_fetcher import fetch_connection_data
17
- from airbyte_ops_mcp.live_tests.connection_secret_retriever import (
16
+ from airbyte_ops_mcp.regression_tests.connection_fetcher import fetch_connection_data
17
+ from airbyte_ops_mcp.regression_tests.connection_secret_retriever import (
18
18
  retrieve_unmasked_config,
19
19
  )
20
20
 
@@ -3,7 +3,9 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from enum import Enum
6
+ from enum import Enum, StrEnum
7
+
8
+ from airbyte.exceptions import PyAirbyteInputError
7
9
 
8
10
  MCP_SERVER_NAME = "airbyte-internal-ops"
9
11
  """The name of the MCP server."""
@@ -59,6 +61,64 @@ CLOUD_REGISTRY_URL = (
59
61
  )
60
62
  """URL for the Airbyte Cloud connector registry."""
61
63
 
64
+ # =============================================================================
65
+ # Organization ID Aliases
66
+ # =============================================================================
67
+
68
+
69
+ class OrganizationAliasEnum(StrEnum):
70
+ """Organization ID aliases that can be used in place of UUIDs.
71
+
72
+ Each member's name is the alias (e.g., "@airbyte-internal") and its value
73
+ is the actual organization UUID. Use `OrganizationAliasEnum.resolve()` to
74
+ resolve aliases to actual IDs.
75
+ """
76
+
77
+ AIRBYTE_INTERNAL = "664c690e-5263-49ba-b01f-4a6759b3330a"
78
+ """The Airbyte internal organization for testing and internal operations.
79
+
80
+ Alias: @airbyte-internal
81
+ """
82
+
83
+ @classmethod
84
+ def resolve(cls, org_id: str | None) -> str | None:
85
+ """Resolve an organization ID alias to its actual UUID.
86
+
87
+ Accepts either an alias string (e.g., "@airbyte-internal") or an
88
+ OrganizationAliasEnum enum member, and returns the actual UUID.
89
+
90
+ Returns:
91
+ The resolved organization ID (UUID), or None if input is None.
92
+ If the input doesn't start with "@", it is returned unchanged.
93
+
94
+ Raises:
95
+ PyAirbyteInputError: If the input starts with "@" but is not a recognized alias.
96
+ """
97
+ if org_id is None:
98
+ return None
99
+
100
+ # Handle OrganizationAliasEnum enum members directly
101
+ if isinstance(org_id, cls):
102
+ return org_id.value
103
+
104
+ # If it doesn't look like an alias, return as-is (assume it's a UUID)
105
+ if not org_id.startswith("@"):
106
+ return org_id
107
+
108
+ # Handle alias strings or raise an error if invalid
109
+ alias_mapping = {
110
+ "@airbyte-internal": cls.AIRBYTE_INTERNAL.value,
111
+ }
112
+ if org_id not in alias_mapping:
113
+ raise PyAirbyteInputError(
114
+ message=f"Unknown organization alias: {org_id}",
115
+ context={
116
+ "valid_aliases": list(alias_mapping.keys()),
117
+ },
118
+ )
119
+ return alias_mapping[org_id]
120
+
121
+
62
122
  CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS_SECRET_ID = (
63
123
  "projects/587336813068/secrets/CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS"
64
124
  )
@@ -83,6 +83,74 @@ class WorkflowDispatchResult:
83
83
  """Direct URL to the workflow run, if discovered"""
84
84
 
85
85
 
86
+ @dataclass
87
+ class WorkflowJobInfo:
88
+ """Information about a single job in a workflow run."""
89
+
90
+ job_id: int
91
+ """GitHub job ID (use with git_ci_job_logs to retrieve logs)"""
92
+
93
+ name: str
94
+ """Job name as defined in the workflow"""
95
+
96
+ status: str
97
+ """Job status: queued, in_progress, completed"""
98
+
99
+ conclusion: str | None = None
100
+ """Job conclusion: success, failure, cancelled, skipped (only set when completed)"""
101
+
102
+ started_at: str | None = None
103
+ """ISO 8601 timestamp when the job started"""
104
+
105
+ completed_at: str | None = None
106
+ """ISO 8601 timestamp when the job completed"""
107
+
108
+
109
+ def get_workflow_jobs(
110
+ owner: str,
111
+ repo: str,
112
+ run_id: int,
113
+ ) -> list[WorkflowJobInfo]:
114
+ """Get jobs for a workflow run from GitHub API.
115
+
116
+ Args:
117
+ owner: Repository owner (e.g., "airbytehq")
118
+ repo: Repository name (e.g., "airbyte")
119
+ run_id: Workflow run ID
120
+
121
+ Returns:
122
+ List of WorkflowJobInfo objects for each job in the workflow run.
123
+
124
+ Raises:
125
+ ValueError: If no GitHub token is found.
126
+ requests.HTTPError: If API request fails.
127
+ """
128
+ token = resolve_github_token()
129
+
130
+ url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/runs/{run_id}/jobs"
131
+ headers = {
132
+ "Authorization": f"Bearer {token}",
133
+ "Accept": "application/vnd.github+json",
134
+ "X-GitHub-Api-Version": "2022-11-28",
135
+ }
136
+
137
+ response = requests.get(url, headers=headers, timeout=30)
138
+ response.raise_for_status()
139
+
140
+ jobs_data = response.json()
141
+ return [
142
+ WorkflowJobInfo(
143
+ job_id=job["id"],
144
+ name=job["name"],
145
+ status=job["status"],
146
+ conclusion=job.get("conclusion"),
147
+ started_at=job.get("started_at"),
148
+ completed_at=job.get("completed_at"),
149
+ )
150
+ for job in jobs_data.get("jobs", [])
151
+ ]
152
+
153
+
86
154
  def find_workflow_run(
87
155
  owner: str,
88
156
  repo: str,
@@ -167,7 +235,7 @@ def trigger_workflow_dispatch(
167
235
  Args:
168
236
  owner: Repository owner (e.g., "airbytehq")
169
237
  repo: Repository name (e.g., "airbyte-ops-mcp")
170
- workflow_file: Workflow file name (e.g., "connector-live-test.yml")
238
+ workflow_file: Workflow file name (e.g., "connector-regression-test.yml")
171
239
  ref: Git ref to run the workflow on (branch name)
172
240
  inputs: Workflow inputs dictionary
173
241
  token: GitHub API token
@@ -15,6 +15,8 @@ for the MCP module and should not be imported directly by external code.
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import os
19
+
18
20
  from airbyte.cloud.auth import (
19
21
  resolve_cloud_api_url,
20
22
  resolve_cloud_client_id,
@@ -49,6 +51,33 @@ def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
49
51
  return None
50
52
 
51
53
 
54
+ def get_bearer_token_from_headers() -> SecretString | None:
55
+ """Extract bearer token from HTTP Authorization header.
56
+
57
+ This function extracts the bearer token from the standard HTTP
58
+ `Authorization: Bearer <token>` header when running as an MCP HTTP server.
59
+
60
+ Returns:
61
+ The bearer token as a SecretString, or None if not found or not in HTTP context.
62
+ """
63
+ headers = get_http_headers()
64
+ if not headers:
65
+ return None
66
+
67
+ auth_header = _get_header_value(headers, "Authorization")
68
+ if not auth_header:
69
+ return None
70
+
71
+ # Parse "Bearer <token>" format (case-insensitive prefix check)
72
+ bearer_prefix = "bearer "
73
+ if auth_header.lower().startswith(bearer_prefix):
74
+ token = auth_header[len(bearer_prefix) :].strip()
75
+ if token:
76
+ return SecretString(token)
77
+
78
+ return None
79
+
80
+
52
81
  def get_client_id_from_headers() -> SecretString | None:
53
82
  """Extract client ID from HTTP headers.
54
83
 
@@ -196,3 +225,30 @@ def resolve_api_url(api_url: str | None = None) -> str:
196
225
  return header_value
197
226
 
198
227
  return resolve_cloud_api_url()
228
+
229
+
230
+ def resolve_bearer_token() -> SecretString | None:
231
+ """Resolve bearer token from HTTP headers or environment variables.
232
+
233
+ Resolution order:
234
+ 1. HTTP Authorization header (Bearer <token>)
235
+ 2. Environment variable AIRBYTE_CLOUD_BEARER_TOKEN
236
+
237
+ Returns:
238
+ The resolved bearer token as a SecretString, or None if not found.
239
+
240
+ Note:
241
+ Unlike resolve_client_id/resolve_client_secret, this function returns
242
+ None instead of raising an exception if no bearer token is found,
243
+ since bearer token auth is optional (can fall back to client credentials).
244
+ """
245
+ header_value = get_bearer_token_from_headers()
246
+ if header_value:
247
+ return header_value
248
+
249
+ # Try env var directly
250
+ env_value = os.environ.get("AIRBYTE_CLOUD_BEARER_TOKEN")
251
+ if env_value:
252
+ return SecretString(env_value)
253
+
254
+ return None
@@ -76,8 +76,8 @@ class ToolDomain(str, Enum):
76
76
  PROMPTS = "prompts"
77
77
  """Prompt templates for common workflows"""
78
78
 
79
- LIVE_TESTS = "live_tests"
80
- """Live tests for connector validation and regression testing"""
79
+ REGRESSION_TESTS = "regression_tests"
80
+ """Regression tests for connector validation and comparison testing"""
81
81
 
82
82
 
83
83
  _REGISTERED_TOOLS: list[tuple[Callable[..., Any], dict[str, Any]]] = []
@@ -5,16 +5,15 @@ This module provides MCP tools for viewing and managing connector version
5
5
  overrides (pins) in Airbyte Cloud. These tools enable admins to pin connectors
6
6
  to specific versions for troubleshooting or stability purposes.
7
7
 
8
- Uses PyAirbyte's CloudWorkspace, CloudSource, and CloudDestination classes
9
- for all cloud operations.
8
+ Uses direct API client calls with either bearer token or client credentials auth.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
13
12
 
13
+ from dataclasses import dataclass
14
14
  from typing import Annotated, Literal
15
15
 
16
16
  from airbyte import constants
17
- from airbyte.cloud import CloudWorkspace
18
17
  from airbyte.exceptions import PyAirbyteInputError
19
18
  from fastmcp import FastMCP
20
19
  from pydantic import Field
@@ -29,41 +28,57 @@ from airbyte_ops_mcp.cloud_admin.models import (
29
28
  VersionOverrideOperationResult,
30
29
  )
31
30
  from airbyte_ops_mcp.mcp._http_headers import (
31
+ resolve_bearer_token,
32
32
  resolve_client_id,
33
33
  resolve_client_secret,
34
34
  )
35
35
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
36
36
 
37
37
 
38
- def _get_workspace(workspace_id: str) -> CloudWorkspace:
39
- """Get a CloudWorkspace instance for the specified workspace.
38
+ @dataclass(frozen=True)
39
+ class _ResolvedCloudAuth:
40
+ """Resolved authentication for Airbyte Cloud API calls.
40
41
 
41
- Credentials are resolved in priority order:
42
- 1. HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret)
43
- 2. Environment variables (AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET)
42
+ Either bearer_token OR (client_id AND client_secret) will be set, not both.
43
+ """
44
+
45
+ bearer_token: str | None = None
46
+ client_id: str | None = None
47
+ client_secret: str | None = None
44
48
 
45
- Args:
46
- workspace_id: The Airbyte Cloud workspace ID (required).
49
+
50
+ def _resolve_cloud_auth() -> _ResolvedCloudAuth:
51
+ """Resolve authentication credentials for Airbyte Cloud API.
52
+
53
+ Credentials are resolved in priority order:
54
+ 1. Bearer token (Authorization header or AIRBYTE_CLOUD_BEARER_TOKEN env var)
55
+ 2. Client credentials (X-Airbyte-Cloud-Client-Id/Secret headers or env vars)
47
56
 
48
57
  Returns:
49
- CloudWorkspace instance configured for the specified workspace.
58
+ _ResolvedCloudAuth with either bearer_token or client credentials set.
50
59
 
51
60
  Raises:
52
61
  CloudAuthError: If credentials cannot be resolved from headers or env vars.
53
62
  """
63
+ # Try bearer token first (preferred)
64
+ bearer_token = resolve_bearer_token()
65
+ if bearer_token:
66
+ return _ResolvedCloudAuth(bearer_token=str(bearer_token))
67
+
68
+ # Fall back to client credentials
54
69
  try:
55
- return CloudWorkspace(
56
- workspace_id=workspace_id,
57
- client_id=resolve_client_id(),
58
- client_secret=resolve_client_secret(),
59
- api_root=constants.CLOUD_API_ROOT, # Used for workspace operations
70
+ client_id = resolve_client_id()
71
+ client_secret = resolve_client_secret()
72
+ return _ResolvedCloudAuth(
73
+ client_id=str(client_id),
74
+ client_secret=str(client_secret),
60
75
  )
61
76
  except Exception as e:
62
77
  raise CloudAuthError(
63
- f"Failed to initialize CloudWorkspace. Ensure credentials are provided "
64
- f"via HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret) "
65
- f"or environment variables (AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET). "
66
- f"Error: {e}"
78
+ f"Failed to resolve credentials. Ensure credentials are provided "
79
+ f"via Authorization header (Bearer token), "
80
+ f"HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret), "
81
+ f"or environment variables. Error: {e}"
67
82
  ) from e
68
83
 
69
84
 
@@ -91,11 +106,12 @@ def get_cloud_connector_version(
91
106
  an override (pin) is applied.
92
107
 
93
108
  Authentication credentials are resolved in priority order:
94
- 1. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
95
- 2. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
109
+ 1. Bearer token (Authorization header or AIRBYTE_CLOUD_BEARER_TOKEN env var)
110
+ 2. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
111
+ 3. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
96
112
  """
97
113
  try:
98
- workspace = _get_workspace(workspace_id)
114
+ auth = _resolve_cloud_auth()
99
115
 
100
116
  # Use vendored API client instead of connector.get_connector_version()
101
117
  # Use Config API root for version management operations
@@ -103,8 +119,9 @@ def get_cloud_connector_version(
103
119
  connector_id=actor_id,
104
120
  connector_type=actor_type,
105
121
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API, not public API
106
- client_id=workspace.client_id,
107
- client_secret=workspace.client_secret,
122
+ client_id=auth.client_id,
123
+ client_secret=auth.client_secret,
124
+ bearer_token=auth.bearer_token,
108
125
  )
109
126
 
110
127
  return ConnectorVersionInfo(
@@ -220,8 +237,9 @@ def set_cloud_connector_version_override(
220
237
  - Release candidates (-rc): Any admin can pin/unpin RC versions
221
238
 
222
239
  Authentication credentials are resolved in priority order:
223
- 1. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
224
- 2. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
240
+ 1. Bearer token (Authorization header or AIRBYTE_CLOUD_BEARER_TOKEN env var)
241
+ 2. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
242
+ 3. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
225
243
  """
226
244
  # Validate admin access (check env var flag)
227
245
  try:
@@ -288,17 +306,18 @@ def set_cloud_connector_version_override(
288
306
  audit_parts.append(f"AI Session: {ai_agent_session_url}")
289
307
  enhanced_override_reason = " | ".join(audit_parts)
290
308
 
291
- # Get workspace and current version info
309
+ # Resolve auth and get current version info
292
310
  try:
293
- workspace = _get_workspace(workspace_id)
311
+ auth = _resolve_cloud_auth()
294
312
 
295
313
  # Get current version info before the operation
296
314
  current_version_data = api_client.get_connector_version(
297
315
  connector_id=actor_id,
298
316
  connector_type=actor_type,
299
317
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
300
- client_id=workspace.client_id,
301
- client_secret=workspace.client_secret,
318
+ client_id=auth.client_id,
319
+ client_secret=auth.client_secret,
320
+ bearer_token=auth.bearer_token,
302
321
  )
303
322
  current_version = current_version_data["dockerImageTag"]
304
323
  was_pinned_before = current_version_data["isVersionOverrideApplied"]
@@ -306,14 +325,7 @@ def set_cloud_connector_version_override(
306
325
  except CloudAuthError as e:
307
326
  return VersionOverrideOperationResult(
308
327
  success=False,
309
- message=f"Failed to initialize workspace or connector: {e}",
310
- connector_id=actor_id,
311
- connector_type=actor_type,
312
- )
313
- except Exception as e:
314
- return VersionOverrideOperationResult(
315
- success=False,
316
- message=f"Failed to get connector: {e}",
328
+ message=f"Failed to resolve credentials or get connector: {e}",
317
329
  connector_id=actor_id,
318
330
  connector_type=actor_type,
319
331
  )
@@ -324,14 +336,15 @@ def set_cloud_connector_version_override(
324
336
  connector_id=actor_id,
325
337
  connector_type=actor_type,
326
338
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
327
- client_id=workspace.client_id,
328
- client_secret=workspace.client_secret,
339
+ client_id=auth.client_id,
340
+ client_secret=auth.client_secret,
329
341
  workspace_id=workspace_id,
330
342
  version=version,
331
343
  unset=unset,
332
344
  override_reason=enhanced_override_reason,
333
345
  override_reason_reference_url=override_reason_reference_url,
334
346
  user_email=admin_user_email,
347
+ bearer_token=auth.bearer_token,
335
348
  )
336
349
 
337
350
  # Get updated version info after the operation
@@ -339,8 +352,9 @@ def set_cloud_connector_version_override(
339
352
  connector_id=actor_id,
340
353
  connector_type=actor_type,
341
354
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
342
- client_id=workspace.client_id,
343
- client_secret=workspace.client_secret,
355
+ client_id=auth.client_id,
356
+ client_secret=auth.client_secret,
357
+ bearer_token=auth.bearer_token,
344
358
  )
345
359
  new_version = updated_version_data["dockerImageTag"] if not unset else None
346
360
  is_pinned_after = updated_version_data["isVersionOverrideApplied"]