From c5be5ea1eb855c54ca2d1cd1dd4ec937bcab9091 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:55:12 +0000 Subject: [PATCH 1/2] fix(mcp): reject path-like `manifest_yaml` when trusted execution is off Co-Authored-By: AJ Steers --- airbyte/mcp/_arg_resolvers.py | 18 +++++++++++ .../unit_tests/test_mcp_trusted_execution.py | 31 ++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/airbyte/mcp/_arg_resolvers.py b/airbyte/mcp/_arg_resolvers.py index 52f600937..85a2a597c 100644 --- a/airbyte/mcp/_arg_resolvers.py +++ b/airbyte/mcp/_arg_resolvers.py @@ -113,9 +113,27 @@ def resolve_manifest_yaml(manifest_yaml: str | Path | None) -> str | Path | None if is_trusted_execution_enabled(): return Path(manifest_yaml) + if _looks_like_file_path(manifest_yaml): + raise AirbyteLibInputError( + message="`manifest_yaml` looks like a file path, which this server cannot read.", + guidance=( + "This MCP server does not run on your machine and cannot read local files. " + "Pass the manifest content inline as the `manifest_yaml` value." + ), + context={"manifest_yaml": manifest_yaml}, + ) + return manifest_yaml +def _looks_like_file_path(value: str) -> bool: + """Return True for a single-line value shaped like a manifest file path.""" + stripped = value.strip() + return stripped.lower().endswith((".yaml", ".yml", ".json")) or stripped.startswith( + ("/", "~", "./", "../") + ) + + def resolve_connector_config( # noqa: PLR0912 config: dict | str | None = None, config_file: str | Path | None = None, diff --git a/tests/unit_tests/test_mcp_trusted_execution.py b/tests/unit_tests/test_mcp_trusted_execution.py index 75ea06a5d..1ff1fc401 100644 --- a/tests/unit_tests/test_mcp_trusted_execution.py +++ b/tests/unit_tests/test_mcp_trusted_execution.py @@ -178,7 +178,7 @@ def test_resolve_manifest_yaml_rejects_path_when_untrusted( @pytest.mark.parametrize( "manifest_yaml", [ - pytest.param("manifest.yaml", id="single-line-text"), + pytest.param("not-a-manifest", id="single-line-text"), pytest.param( '{"version": "0.1.0", "type": "DeclarativeSource"}', id="inline-json" ), @@ -196,6 +196,27 @@ def test_resolve_manifest_yaml_preserves_inline_values_when_untrusted( assert resolved == manifest_yaml +@pytest.mark.parametrize( + "manifest_yaml", + [ + pytest.param("manifest.yaml", id="relative-file"), + pytest.param("/private/tmp/session/bill_manifest.yaml", id="absolute-yaml"), + pytest.param("~/connectors/manifest.YML", id="home-yml"), + pytest.param("./manifest", id="dot-relative"), + ], +) +def test_resolve_manifest_yaml_rejects_path_like_text_when_untrusted( + monkeypatch: MonkeyPatch, + manifest_yaml: str, +) -> None: + """Untrusted execution rejects single-line path-like text and asks for inline content.""" + _set_trusted(monkeypatch, enabled=False) + with pytest.raises(AirbyteLibInputError) as exc_info: + resolve_manifest_yaml(manifest_yaml) + assert exc_info.value.guidance is not None + assert "inline" in exc_info.value.guidance + + def test_resolve_manifest_yaml_preserves_existing_path_when_trusted( monkeypatch: MonkeyPatch, tmp_path: Path, @@ -228,13 +249,13 @@ def test_resolve_manifest_yaml_does_not_read_file_when_untrusted( monkeypatch: MonkeyPatch, tmp_path: Path, ) -> None: - """A path string remains inline YAML when trusted execution is disabled.""" + """A path string is rejected, not read, when trusted execution is disabled.""" _set_trusted(monkeypatch, enabled=False) manifest_path = tmp_path / "manifest.yaml" manifest_path.write_text("version: 0.1.0\ntype: DeclarativeSource\n") - resolved = resolve_manifest_yaml(str(manifest_path)) - assert isinstance(resolved, str) - assert resolved == str(manifest_path) + with pytest.raises(AirbyteLibInputError) as exc_info: + resolve_manifest_yaml(str(manifest_path)) + assert "version: 0.1.0" not in str(exc_info.value) _UNTRUSTED_LOCAL_HELPERS: list[Callable[[], object]] = [ From 8df6d694a7b8e1af1eeb1e91c4cf4b16accf1764 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:00:44 +0000 Subject: [PATCH 2/2] docs(mcp): describe path-like `manifest_yaml` rejection in tool schemas Co-Authored-By: AJ Steers --- airbyte/mcp/cloud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/airbyte/mcp/cloud.py b/airbyte/mcp/cloud.py index e9bfb4ffd..bc1fb73d3 100644 --- a/airbyte/mcp/cloud.py +++ b/airbyte/mcp/cloud.py @@ -2991,8 +2991,8 @@ def publish_custom_source_definition( Field( description=( "The Low-code CDK manifest as inline YAML or a file path. File paths are " - "honored only when trusted execution is enabled; otherwise, the value is " - "treated as inline YAML. Required for YAML connectors." + "honored only when trusted execution is enabled; otherwise, pass the manifest " + "content inline (path-like values are rejected). Required for YAML connectors." ), default=None, ), @@ -3234,7 +3234,8 @@ def update_custom_source_definition( Field( description=( "New manifest as inline YAML or a file path. File paths are honored only when " - "trusted execution is enabled; otherwise, the value is treated as inline YAML. " + "trusted execution is enabled; otherwise, pass the manifest content inline " + "(path-like values are rejected). " "Optional; omit to update only testing values." ), default=None,