airbyte-internal-ops 0.1.11__py3-none-any.whl → 0.2.1__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.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/METADATA +2 -2
  2. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/RECORD +41 -40
  3. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/entry_points.txt +1 -0
  4. airbyte_ops_mcp/__init__.py +2 -2
  5. airbyte_ops_mcp/cli/cloud.py +264 -301
  6. airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
  7. airbyte_ops_mcp/cloud_admin/auth.py +32 -0
  8. airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
  9. airbyte_ops_mcp/constants.py +18 -0
  10. airbyte_ops_mcp/github_actions.py +94 -5
  11. airbyte_ops_mcp/mcp/_http_headers.py +254 -0
  12. airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
  13. airbyte_ops_mcp/mcp/cloud_connector_versions.py +162 -52
  14. airbyte_ops_mcp/mcp/github.py +34 -1
  15. airbyte_ops_mcp/mcp/prod_db_queries.py +67 -24
  16. airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +165 -152
  17. airbyte_ops_mcp/mcp/server.py +84 -11
  18. airbyte_ops_mcp/prod_db_access/db_engine.py +15 -11
  19. airbyte_ops_mcp/prod_db_access/queries.py +27 -15
  20. airbyte_ops_mcp/prod_db_access/sql.py +17 -16
  21. airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
  22. airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
  23. airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
  24. airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
  25. airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
  26. airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
  27. airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
  28. airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
  29. airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
  30. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/WHEEL +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
@@ -5,20 +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
- from airbyte.cloud.auth import (
19
- resolve_cloud_client_id,
20
- resolve_cloud_client_secret,
21
- )
22
17
  from airbyte.exceptions import PyAirbyteInputError
23
18
  from fastmcp import FastMCP
24
19
  from pydantic import Field
@@ -26,39 +21,64 @@ from pydantic import Field
26
21
  from airbyte_ops_mcp.cloud_admin import api_client
27
22
  from airbyte_ops_mcp.cloud_admin.auth import (
28
23
  CloudAuthError,
29
- get_admin_user_email,
30
- require_internal_admin,
24
+ require_internal_admin_flag_only,
31
25
  )
32
26
  from airbyte_ops_mcp.cloud_admin.models import (
33
27
  ConnectorVersionInfo,
34
28
  VersionOverrideOperationResult,
35
29
  )
30
+ from airbyte_ops_mcp.mcp._http_headers import (
31
+ resolve_bearer_token,
32
+ resolve_client_id,
33
+ resolve_client_secret,
34
+ )
36
35
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
37
36
 
38
37
 
39
- def _get_workspace(workspace_id: str) -> CloudWorkspace:
40
- """Get a CloudWorkspace instance for the specified workspace.
38
+ @dataclass(frozen=True)
39
+ class _ResolvedCloudAuth:
40
+ """Resolved authentication for Airbyte Cloud API calls.
41
41
 
42
- Args:
43
- workspace_id: The Airbyte Cloud workspace ID (required).
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
48
+
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)
44
56
 
45
57
  Returns:
46
- CloudWorkspace instance configured for the specified workspace.
58
+ _ResolvedCloudAuth with either bearer_token or client credentials set.
47
59
 
48
60
  Raises:
49
- CloudAuthError: If required environment variables are not set.
61
+ CloudAuthError: If credentials cannot be resolved from headers or env vars.
50
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
51
69
  try:
52
- return CloudWorkspace(
53
- workspace_id=workspace_id,
54
- client_id=resolve_cloud_client_id(),
55
- client_secret=resolve_cloud_client_secret(),
56
- 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),
57
75
  )
58
76
  except Exception as e:
59
77
  raise CloudAuthError(
60
- f"Failed to initialize CloudWorkspace. Ensure AIRBYTE_CLIENT_ID "
61
- f"and AIRBYTE_CLIENT_SECRET are set. 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}"
62
82
  ) from e
63
83
 
64
84
 
@@ -85,11 +105,13 @@ def get_cloud_connector_version(
85
105
  Returns version details including the current version string and whether
86
106
  an override (pin) is applied.
87
107
 
88
- The `AIRBYTE_CLIENT_ID`, `AIRBYTE_CLIENT_SECRET`, and `AIRBYTE_API_ROOT`
89
- environment variables will be used to authenticate with the Airbyte Cloud API.
108
+ Authentication credentials are resolved in priority order:
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
90
112
  """
91
113
  try:
92
- workspace = _get_workspace(workspace_id)
114
+ auth = _resolve_cloud_auth()
93
115
 
94
116
  # Use vendored API client instead of connector.get_connector_version()
95
117
  # Use Config API root for version management operations
@@ -97,8 +119,9 @@ def get_cloud_connector_version(
97
119
  connector_id=actor_id,
98
120
  connector_type=actor_type,
99
121
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API, not public API
100
- client_id=workspace.client_id,
101
- client_secret=workspace.client_secret,
122
+ client_id=auth.client_id,
123
+ client_secret=auth.client_secret,
124
+ bearer_token=auth.bearer_token,
102
125
  )
103
126
 
104
127
  return ConnectorVersionInfo(
@@ -163,11 +186,47 @@ def set_cloud_connector_version_override(
163
186
  default=None,
164
187
  ),
165
188
  ],
189
+ admin_user_email: Annotated[
190
+ str | None,
191
+ Field(
192
+ description="Email of the admin user authorizing this operation. "
193
+ "Must be an @airbyte.io email address. Required for authorization.",
194
+ default=None,
195
+ ),
196
+ ],
197
+ issue_url: Annotated[
198
+ str | None,
199
+ Field(
200
+ description="URL to the GitHub issue providing context for this operation. "
201
+ "Must be a valid GitHub URL (https://github.com/...). Required for authorization.",
202
+ default=None,
203
+ ),
204
+ ],
205
+ approval_comment_url: Annotated[
206
+ str | None,
207
+ Field(
208
+ description="URL to a GitHub comment where the admin has explicitly "
209
+ "requested or authorized this deployment. Must be a valid GitHub comment URL. "
210
+ "Required for authorization.",
211
+ default=None,
212
+ ),
213
+ ],
214
+ ai_agent_session_url: Annotated[
215
+ str | None,
216
+ Field(
217
+ description="URL to the AI agent session driving this operation, if applicable. "
218
+ "Provides additional auditability for AI-driven operations.",
219
+ default=None,
220
+ ),
221
+ ],
166
222
  ) -> VersionOverrideOperationResult:
167
223
  """Set or clear a version override for a deployed connector.
168
224
 
169
- **Admin-only operation** - Requires AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io
170
- and AIRBYTE_INTERNAL_ADMIN_USER environment variables.
225
+ **Admin-only operation** - Requires:
226
+ - AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io environment variable
227
+ - admin_user_email parameter (must be @airbyte.io email)
228
+ - issue_url parameter (GitHub issue URL for context)
229
+ - approval_comment_url parameter (GitHub comment URL with approval)
171
230
 
172
231
  You must specify EXACTLY ONE of `version` OR `unset=True`, but not both.
173
232
  When setting a version, `override_reason` is required.
@@ -177,13 +236,14 @@ def set_cloud_connector_version_override(
177
236
  - Production versions: Require strong justification mentioning customer/support/investigation
178
237
  - Release candidates (-rc): Any admin can pin/unpin RC versions
179
238
 
180
- The `AIRBYTE_CLIENT_ID`, `AIRBYTE_CLIENT_SECRET`, and `AIRBYTE_API_ROOT`
181
- environment variables will be used to authenticate with the Airbyte Cloud API.
239
+ Authentication credentials are resolved in priority order:
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
182
243
  """
183
- # Validate admin access
244
+ # Validate admin access (check env var flag)
184
245
  try:
185
- require_internal_admin()
186
- user_email = get_admin_user_email()
246
+ require_internal_admin_flag_only()
187
247
  except CloudAuthError as e:
188
248
  return VersionOverrideOperationResult(
189
249
  success=False,
@@ -192,17 +252,72 @@ def set_cloud_connector_version_override(
192
252
  connector_type=actor_type,
193
253
  )
194
254
 
195
- # Get workspace and current version info
255
+ # Validate new authorization parameters
256
+ validation_errors: list[str] = []
257
+
258
+ if not admin_user_email:
259
+ validation_errors.append("admin_user_email is required for authorization")
260
+ elif "@airbyte.io" not in admin_user_email:
261
+ validation_errors.append(
262
+ f"admin_user_email must be an @airbyte.io email address, got: {admin_user_email}"
263
+ )
264
+
265
+ if not issue_url:
266
+ validation_errors.append(
267
+ "issue_url is required for authorization (GitHub issue URL)"
268
+ )
269
+ elif not issue_url.startswith("https://github.com/"):
270
+ validation_errors.append(
271
+ f"issue_url must be a valid GitHub URL (https://github.com/...), got: {issue_url}"
272
+ )
273
+
274
+ if not approval_comment_url:
275
+ validation_errors.append(
276
+ "approval_comment_url is required for authorization (GitHub comment URL)"
277
+ )
278
+ elif not approval_comment_url.startswith("https://github.com/"):
279
+ validation_errors.append(
280
+ f"approval_comment_url must be a valid GitHub URL, got: {approval_comment_url}"
281
+ )
282
+ elif (
283
+ "#issuecomment-" not in approval_comment_url
284
+ and "#discussion_r" not in approval_comment_url
285
+ ):
286
+ validation_errors.append(
287
+ "approval_comment_url must be a GitHub comment URL "
288
+ "(containing #issuecomment- or #discussion_r)"
289
+ )
290
+
291
+ if validation_errors:
292
+ return VersionOverrideOperationResult(
293
+ success=False,
294
+ message="Authorization validation failed: " + "; ".join(validation_errors),
295
+ connector_id=actor_id,
296
+ connector_type=actor_type,
297
+ )
298
+
299
+ # Build enhanced override reason with audit fields (only for 'set' operations)
300
+ enhanced_override_reason = override_reason
301
+ if not unset and override_reason:
302
+ audit_parts = [override_reason]
303
+ audit_parts.append(f"Issue: {issue_url}")
304
+ audit_parts.append(f"Approval: {approval_comment_url}")
305
+ if ai_agent_session_url:
306
+ audit_parts.append(f"AI Session: {ai_agent_session_url}")
307
+ enhanced_override_reason = " | ".join(audit_parts)
308
+
309
+ # Resolve auth and get current version info
196
310
  try:
197
- workspace = _get_workspace(workspace_id)
311
+ auth = _resolve_cloud_auth()
198
312
 
199
313
  # Get current version info before the operation
200
314
  current_version_data = api_client.get_connector_version(
201
315
  connector_id=actor_id,
202
316
  connector_type=actor_type,
203
317
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
204
- client_id=workspace.client_id,
205
- client_secret=workspace.client_secret,
318
+ client_id=auth.client_id,
319
+ client_secret=auth.client_secret,
320
+ bearer_token=auth.bearer_token,
206
321
  )
207
322
  current_version = current_version_data["dockerImageTag"]
208
323
  was_pinned_before = current_version_data["isVersionOverrideApplied"]
@@ -210,14 +325,7 @@ def set_cloud_connector_version_override(
210
325
  except CloudAuthError as e:
211
326
  return VersionOverrideOperationResult(
212
327
  success=False,
213
- message=f"Failed to initialize workspace or connector: {e}",
214
- connector_id=actor_id,
215
- connector_type=actor_type,
216
- )
217
- except Exception as e:
218
- return VersionOverrideOperationResult(
219
- success=False,
220
- message=f"Failed to get connector: {e}",
328
+ message=f"Failed to resolve credentials or get connector: {e}",
221
329
  connector_id=actor_id,
222
330
  connector_type=actor_type,
223
331
  )
@@ -228,14 +336,15 @@ def set_cloud_connector_version_override(
228
336
  connector_id=actor_id,
229
337
  connector_type=actor_type,
230
338
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
231
- client_id=workspace.client_id,
232
- client_secret=workspace.client_secret,
339
+ client_id=auth.client_id,
340
+ client_secret=auth.client_secret,
233
341
  workspace_id=workspace_id,
234
342
  version=version,
235
343
  unset=unset,
236
- override_reason=override_reason,
344
+ override_reason=enhanced_override_reason,
237
345
  override_reason_reference_url=override_reason_reference_url,
238
- user_email=user_email,
346
+ user_email=admin_user_email,
347
+ bearer_token=auth.bearer_token,
239
348
  )
240
349
 
241
350
  # Get updated version info after the operation
@@ -243,8 +352,9 @@ def set_cloud_connector_version_override(
243
352
  connector_id=actor_id,
244
353
  connector_type=actor_type,
245
354
  api_root=constants.CLOUD_CONFIG_API_ROOT, # Use Config API
246
- client_id=workspace.client_id,
247
- client_secret=workspace.client_secret,
355
+ client_id=auth.client_id,
356
+ client_secret=auth.client_secret,
357
+ bearer_token=auth.bearer_token,
248
358
  )
249
359
  new_version = updated_version_data["dockerImageTag"] if not unset else None
250
360
  is_pinned_after = updated_version_data["isVersionOverrideApplied"]
@@ -14,12 +14,27 @@ import requests
14
14
  from fastmcp import FastMCP
15
15
  from pydantic import BaseModel, Field
16
16
 
17
- from airbyte_ops_mcp.github_actions import GITHUB_API_BASE, resolve_github_token
17
+ from airbyte_ops_mcp.github_actions import (
18
+ GITHUB_API_BASE,
19
+ get_workflow_jobs,
20
+ resolve_github_token,
21
+ )
18
22
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
19
23
 
20
24
  DOCKERHUB_API_BASE = "https://hub.docker.com/v2"
21
25
 
22
26
 
27
+ class JobInfo(BaseModel):
28
+ """Information about a single job in a workflow run."""
29
+
30
+ job_id: int
31
+ name: str
32
+ status: str
33
+ conclusion: str | None = None
34
+ started_at: str | None = None
35
+ completed_at: str | None = None
36
+
37
+
23
38
  class WorkflowRunStatus(BaseModel):
24
39
  """Response model for check_workflow_status MCP tool."""
25
40
 
@@ -34,6 +49,7 @@ class WorkflowRunStatus(BaseModel):
34
49
  updated_at: str
35
50
  run_started_at: str | None = None
36
51
  jobs_url: str
52
+ jobs: list[JobInfo] = []
37
53
 
38
54
 
39
55
  def _parse_workflow_url(url: str) -> tuple[str, str, int]:
@@ -148,6 +164,22 @@ def check_workflow_status(
148
164
  # Get workflow run details
149
165
  run_data = _get_workflow_run(owner, repo, run_id, token)
150
166
 
167
+ # Get jobs for the workflow run (uses upstream function that resolves its own token)
168
+ workflow_jobs = get_workflow_jobs(owner, repo, run_id)
169
+
170
+ # Convert dataclass objects to Pydantic models for the response
171
+ jobs = [
172
+ JobInfo(
173
+ job_id=job.job_id,
174
+ name=job.name,
175
+ status=job.status,
176
+ conclusion=job.conclusion,
177
+ started_at=job.started_at,
178
+ completed_at=job.completed_at,
179
+ )
180
+ for job in workflow_jobs
181
+ ]
182
+
151
183
  return WorkflowRunStatus(
152
184
  run_id=run_data["id"],
153
185
  status=run_data["status"],
@@ -160,6 +192,7 @@ def check_workflow_status(
160
192
  updated_at=run_data["updated_at"],
161
193
  run_started_at=run_data.get("run_started_at"),
162
194
  jobs_url=run_data["jobs_url"],
195
+ jobs=jobs,
163
196
  )
164
197
 
165
198
 
@@ -20,7 +20,7 @@ from airbyte_ops_mcp.prod_db_access.queries import (
20
20
  query_connections_by_connector,
21
21
  query_connector_versions,
22
22
  query_dataplanes_list,
23
- query_failed_sync_attempts_for_version,
23
+ query_failed_sync_attempts_for_connector,
24
24
  query_new_connector_releases,
25
25
  query_sync_results_for_version,
26
26
  query_workspace_info,
@@ -249,12 +249,41 @@ def query_prod_connector_version_sync_results(
249
249
  @mcp_tool(
250
250
  read_only=True,
251
251
  idempotent=True,
252
+ open_world=True,
252
253
  )
253
- def query_prod_failed_sync_attempts_for_version(
254
- connector_version_id: Annotated[
255
- str,
256
- Field(description="Connector version UUID to find failed sync attempts for"),
257
- ],
254
+ def query_prod_failed_sync_attempts_for_connector(
255
+ source_definition_id: Annotated[
256
+ str | None,
257
+ Field(
258
+ description=(
259
+ "Source connector definition ID (UUID) to search for. "
260
+ "Exactly one of this or source_canonical_name is required. "
261
+ "Example: 'afa734e4-3571-11ec-991a-1e0031268139' for YouTube Analytics."
262
+ ),
263
+ default=None,
264
+ ),
265
+ ] = None,
266
+ source_canonical_name: Annotated[
267
+ str | None,
268
+ Field(
269
+ description=(
270
+ "Canonical source connector name to search for. "
271
+ "Exactly one of this or source_definition_id is required. "
272
+ "Examples: 'source-youtube-analytics', 'YouTube Analytics'."
273
+ ),
274
+ default=None,
275
+ ),
276
+ ] = None,
277
+ organization_id: Annotated[
278
+ str | None,
279
+ Field(
280
+ description=(
281
+ "Optional organization ID (UUID) to filter results. "
282
+ "If provided, only failed attempts from this organization will be returned."
283
+ ),
284
+ default=None,
285
+ ),
286
+ ] = None,
258
287
  days: Annotated[
259
288
  int,
260
289
  Field(description="Number of days to look back (default: 7)", default=7),
@@ -264,29 +293,43 @@ def query_prod_failed_sync_attempts_for_version(
264
293
  Field(description="Maximum number of results (default: 100)", default=100),
265
294
  ] = 100,
266
295
  ) -> list[dict[str, Any]]:
267
- """List failed sync attempts with failure details for actors pinned to a connector version.
296
+ """List failed sync attempts for ALL actors using a source connector type.
268
297
 
269
- Returns failed attempt records for connections using actors pinned to the specified
270
- version. Includes failure_summary from the attempts table for debugging.
298
+ This tool finds all actors with the given connector definition and returns their
299
+ failed sync attempts, regardless of whether they have explicit version pins.
271
300
 
272
- Key fields:
273
- - latest_job_attempt_status: Final job status after all retries ('succeeded' means
274
- the job eventually succeeded despite this failed attempt)
275
- - failed_attempt_number: Which attempt this was (0-indexed)
276
- - failure_summary: JSON containing failure details including failureType and messages
301
+ This is useful for investigating connector issues across all users. Use this when
302
+ you want to find failures for a connector type regardless of which version users
303
+ are on.
277
304
 
278
- Note: May return multiple rows per job (one per failed attempt). Results ordered by
279
- job_updated_at DESC, then failed_attempt_number DESC.
305
+ Note: This tool only supports SOURCE connectors. For destination connectors,
306
+ a separate tool would be needed.
280
307
 
281
- Returns list of dicts with keys: job_id, connection_id, latest_job_attempt_status,
282
- job_started_at, job_updated_at, connection_name, actor_id, actor_name,
283
- actor_definition_id, pin_origin_type, pin_origin, workspace_id, workspace_name,
284
- organization_id, dataplane_group_id, dataplane_name, failed_attempt_id,
285
- failed_attempt_number, failed_attempt_status, failed_attempt_created_at,
286
- failed_attempt_ended_at, failure_summary, processing_task_queue
308
+ Key fields in results:
309
+ - failure_summary: JSON containing failure details including failureType and messages
310
+ - pin_origin_type, pin_origin, pinned_version_id: Version pin context (NULL if not pinned)
287
311
  """
288
- return query_failed_sync_attempts_for_version(
289
- connector_version_id,
312
+ # Validate that exactly one of the two parameters is provided
313
+ if (source_definition_id is None) == (source_canonical_name is None):
314
+ raise PyAirbyteInputError(
315
+ message=(
316
+ "Exactly one of source_definition_id or source_canonical_name "
317
+ "must be provided, but not both."
318
+ ),
319
+ )
320
+
321
+ # Resolve canonical name to definition ID if needed
322
+ resolved_definition_id: str
323
+ if source_canonical_name:
324
+ resolved_definition_id = _resolve_canonical_name_to_definition_id(
325
+ canonical_name=source_canonical_name,
326
+ )
327
+ else:
328
+ resolved_definition_id = source_definition_id # type: ignore[assignment]
329
+
330
+ return query_failed_sync_attempts_for_connector(
331
+ connector_definition_id=resolved_definition_id,
332
+ organization_id=organization_id,
290
333
  days=days,
291
334
  limit=limit,
292
335
  )