Skip to content

Commit 0514d14

Browse files
committed
fix: address swarm review findings (C1, C3, C4, R5, R6)
Critical bugs fixed: - C1: Add null guard for response.data in streaming.py SDK can return None when recursing into tool_result fragments - C3: Apply redact_sensitive_text() to extracted context values Prevents credential leakage in error messages - C4: Change log_response_text default to False (secure default) Aligns Python mechanism with YAML policy per Three-Medium Improvements: - R5: Add E2E tool capture happy-path tests (TestE2EToolCaptureHappyPath) Covers complete flow from SDK event to accumulated buffer - R6: Fix membrane import in models.py Use sdk_adapter namespace, not internal model_translation module Test results: 980 passed, 94% coverage (Windows + WSL)
1 parent c7222ae commit 0514d14

10 files changed

Lines changed: 435 additions & 32 deletions

File tree

‎amplifier_module_provider_github_copilot/__init__.py‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,28 @@
5555
# Using importlib.metadata avoids importing the SDK itself at module load time.
5656
# Contract: sdk-boundary.md MUST:5
5757
#
58-
# TESTING: Set SKIP_SDK_CHECK=1 to allow test imports without SDK installed.
59-
# Tests use pytest.importorskip() and skip markers to handle SDK availability.
58+
# P1-6 Security Fix: SDK check bypass only allowed in pytest context.
59+
# The env var alone is not sufficient - pytest must be loaded in sys.modules.
60+
# This prevents production misuse while preserving test functionality.
6061
import asyncio
6162
import os as _os
63+
import sys as _sys
6264
import threading
6365
from importlib.metadata import PackageNotFoundError as _PkgNotFoundError
6466
from importlib.metadata import version as _pkg_version
6567

66-
if not _os.environ.get("SKIP_SDK_CHECK"):
68+
69+
def _is_pytest_running() -> bool:
70+
"""Check if pytest is running (for test-only SDK check bypass)."""
71+
return "pytest" in _sys.modules
72+
73+
74+
# Only skip SDK check if BOTH conditions are met:
75+
# 1. SKIP_SDK_CHECK env var is set
76+
# 2. pytest is actually running (prevents production misuse)
77+
_skip_sdk_check = _os.environ.get("SKIP_SDK_CHECK") and _is_pytest_running()
78+
79+
if not _skip_sdk_check:
6780
try:
6881
_pkg_version("github-copilot-sdk")
6982
except _PkgNotFoundError as _e:
@@ -72,15 +85,17 @@
7285
"Install with: pip install 'github-copilot-sdk>=0.2.0,<0.3.0'"
7386
) from _e
7487

75-
from collections.abc import Awaitable, Callable
76-
from typing import Any
88+
# E402: These imports are intentionally after SDK check - we verify SDK
89+
# installation before importing modules that depend on it (Three-Medium).
90+
from collections.abc import Awaitable, Callable # noqa: E402
91+
from typing import Any # noqa: E402
7792

78-
from amplifier_core import ModelInfo, ModuleCoordinator, ProviderInfo
93+
from amplifier_core import ModelInfo, ModuleCoordinator, ProviderInfo # noqa: E402
7994

80-
from .provider import GitHubCopilotProvider
95+
from .provider import GitHubCopilotProvider # noqa: E402
8196

8297
# Contract: sdk-boundary:Membrane:MUST:1 — import from sdk_adapter package, not submodules
83-
from .sdk_adapter import CopilotClientWrapper
98+
from .sdk_adapter import CopilotClientWrapper # noqa: E402
8499

85100
__version__ = "2.0.0"
86101

‎amplifier_module_provider_github_copilot/error_translation.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ def translate_sdk_error(
454454

455455
# Extract context from message (use original for pattern matching)
456456
context = _extract_context(original_message, mapping.context_extraction)
457+
# P0 Fix (C3): Redact extracted context values - they come from the
458+
# original (unredacted) message and may contain secrets like tokens.
459+
context = {k: redact_sensitive_text(v) for k, v in context.items()}
457460
# Use redacted message for output, append context
458461
message = safe_message + _format_context_suffix(context)
459462

‎amplifier_module_provider_github_copilot/fake_tool_detection.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ class LoggingConfig:
3333
"""Logging configuration for fake tool call detection."""
3434

3535
log_matched_pattern: bool = True
36-
log_response_text: bool = True
36+
# P1 Fix (C4): Secure default matches YAML (false). If YAML fails to load,
37+
# we fail closed (don't log potentially sensitive LLM response text).
38+
log_response_text: bool = False
3739
log_response_text_limit: int = 500
3840
log_tool_calls: bool = True
3941
log_correction_message: bool = True

‎amplifier_module_provider_github_copilot/models.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ def __init__(self, message: str, *, provider: str = "github-copilot") -> None:
6666
)
6767

6868
# =============================================================================
69-
# Re-export from sdk_adapter for backward compatibility
70-
# The actual implementation is now in sdk_adapter/model_translation.py
69+
# Re-export from sdk_adapter membrane (NOT direct module import)
70+
# R6 Fix: Import via membrane __init__.py, not bypassing to model_translation.py
7171
# Contract: sdk-boundary:ModelDiscovery:MUST:2
7272
# =============================================================================
73-
from .sdk_adapter.model_translation import ( # noqa: E402
73+
from .sdk_adapter import ( # noqa: E402
7474
CopilotModelInfo,
7575
sdk_model_to_copilot_model,
7676
)

‎amplifier_module_provider_github_copilot/sdk_adapter/_imports.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from __future__ import annotations
1515

1616
import os
17+
import sys
1718
from typing import Any
1819

1920
# Re-export SDK-independent utilities for backward compatibility.
@@ -24,10 +25,20 @@
2425
# SDK imports - THE ONLY PLACE IN THE CODEBASE where SDK is imported
2526
# =============================================================================
2627

27-
# TESTING: Set SKIP_SDK_CHECK=1 to allow imports without SDK installed.
28-
# Tests use pytest.importorskip() and skip markers to handle SDK availability.
29-
# This matches the pattern in __init__.py for consistent test behavior.
30-
_SKIP_SDK_CHECK = os.environ.get("SKIP_SDK_CHECK")
28+
29+
def _is_pytest_running() -> bool:
30+
"""Check if pytest is running (for test-only SDK bypass).
31+
32+
P1-6 Security Fix: SDK bypass only allowed when pytest is actually running.
33+
This prevents production misuse while preserving test functionality.
34+
"""
35+
return "pytest" in sys.modules
36+
37+
38+
# Only skip SDK imports if BOTH conditions are met:
39+
# 1. SKIP_SDK_CHECK env var is set
40+
# 2. pytest is actually running (prevents production misuse)
41+
_SKIP_SDK_CHECK = os.environ.get("SKIP_SDK_CHECK") and _is_pytest_running()
3142

3243
# Guard against import failures - fail fast with clear error
3344
# Unless SKIP_SDK_CHECK is set (for testing without SDK)

‎amplifier_module_provider_github_copilot/streaming.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,13 @@ def extract_response_content(response: Any, _depth: int = 0) -> str:
596596
if _depth > _MAX_EXTRACTION_DEPTH:
597597
return ""
598598

599-
# Check for .data wrapper first (response.data -> Data object)
600-
if hasattr(response, "data"):
599+
# P0 Fix (C1): Added null guard to .data check. Previous code recursed into
600+
# .data even when None, returning "" silently when .content was valid.
601+
# SDK design: .data wraps a nested Data object with .content - unwrap first.
602+
if hasattr(response, "data") and response.data is not None:
601603
return extract_response_content(response.data, _depth + 1)
602604

603-
# Check for Data object with .content attribute
605+
# Fall back to direct .content if .data doesn't exist or is None
604606
if hasattr(response, "content"):
605607
content = response.content # type: ignore[union-attr]
606608
return str(content) if content is not None else ""

‎contracts/deny-destroy.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Contract: Deny + Destroy Pattern
22

33
## Version
4-
- **Current:** 1.2 (ToolSuppression-Corrected)
4+
- **Current:** 1.3 (NoTools-Clarified)
55
- **Module Reference:** amplifier_module_provider_github_copilot/sdk_adapter/client.py
66
- **Status:** Non-Negotiable Constraint
77
- **History:**
8-
- **1.2** — Fixed ToolSuppression: removed incorrect `available_tools=[]` claim, documented `overrides_built_in_tool=True` approach
8+
- **1.3** — Clarified ToolSuppression: `available_tools=[]` required when NO tools provided, allowlist when tools provided
9+
- **1.2** — Fixed ToolSuppression for WITH-tools case: documented `overrides_built_in_tool=True` approach
910
- **1.1** — Fixed path from `modules/provider-core/session_factory.py` to actual location
1011

1112
---
@@ -123,8 +124,9 @@ From GOLDEN_VISION_V2.md:
123124

124125
| Anchor | Clause |
125126
|--------|--------|
126-
| `deny-destroy:ToolSuppression:MUST:1` | available_tools set to Amplifier tool names (allowlist) |
127+
| `deny-destroy:ToolSuppression:MUST:1` | available_tools set to Amplifier tool names (allowlist) when tools provided |
127128
| `deny-destroy:ToolSuppression:MUST:2` | overrides_built_in_tool=True (see sdk-boundary:ToolForwarding:MUST:2) |
129+
| `deny-destroy:ToolSuppression:MUST:3` | available_tools=[] when NO Amplifier tools provided (blocks SDK built-ins) |
128130

129131
### Allowlist
130132

‎tests/conftest.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@
44
55
All fixtures are typed for pyright strict mode compliance.
66
Windows event loop policy configured for asyncio subprocess compatibility.
7+
8+
P1-6 Security Fix: SKIP_SDK_CHECK is now guarded by _is_pytest_running() in
9+
__init__.py and _imports.py, making it safe to use in tests while preventing
10+
production misuse.
711
"""
812

913
from __future__ import annotations
1014

1115
import os
1216
import sys
17+
from collections.abc import Generator
1318
from typing import TYPE_CHECKING, Any
1419

1520
import pytest
1621

22+
# P1-6 Security: SKIP_SDK_CHECK is guarded by _is_pytest_running() in production code.
23+
# This allows tests to run without the SDK while preventing production misuse.
24+
os.environ["SKIP_SDK_CHECK"] = "1"
25+
1726
# Store original function before any test patches run
18-
import amplifier_module_provider_github_copilot.models as _models_module
27+
import amplifier_module_provider_github_copilot.models as _models_module # noqa: E402
1928

2029
_original_fetch_and_map_models = _models_module.fetch_and_map_models
2130

@@ -27,9 +36,6 @@
2736

2837
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
2938

30-
# Allow test imports without SDK installed - tests use skip markers for SDK tests
31-
os.environ["SKIP_SDK_CHECK"] = "1"
32-
3339
# P1-6 Security Fix: Clear GitHub token env vars for non-live tests.
3440
# This prevents the fail-closed security behavior from triggering when
3541
# SubprocessConfig is None (due to SKIP_SDK_CHECK=1) and a token exists.
@@ -219,9 +225,6 @@ def sdk_module() -> Any:
219225
return pytest.importorskip("copilot", reason="github-copilot-sdk not installed")
220226

221227

222-
from collections.abc import Generator
223-
224-
225228
@pytest.fixture
226229
def restore_github_tokens() -> Generator[None, None, None]:
227230
"""Restore saved GitHub tokens for live tests.

0 commit comments

Comments
 (0)