fix: report TLS trust source in apm doctor - #2602
fix: report TLS trust source in apm doctor#2602Aryan Singh K. (aryansk) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new “TLS trust” informational check to apm marketplace doctor and stabilizes test isolation around process-global truststore injection.
Changes:
- Introduced
describe_tls_trust()to generate user-facing TLS trust-source and precedence text. - Added a new “TLS trust” informational doctor check and a corresponding unit test assertion.
- Added an autouse pytest fixture to reset truststore/SSL-related global state between tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| tests/unit/commands/test_marketplace_doctor.py | Adds a unit test ensuring the doctor output includes the TLS trust section/source. |
| tests/conftest.py | Adds an autouse fixture to reset truststore injection and clear TLS trust configuration cache between tests. |
| src/apm_cli/core/tls_trust.py | Adds describe_tls_trust() to report TLS trust source and precedence. |
| src/apm_cli/commands/marketplace/doctor.py | Adds a new informational “TLS trust” check to doctor output. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def describe_tls_trust(env: Mapping[str, str] | None = None) -> tuple[str, str]: | ||
| """Return a user-facing trust source and precedence description.""" | ||
| if _env_flag(_DISABLE_ENV_VAR, env): | ||
| source = f"bundled CA (certifi); {_DISABLE_ENV_VAR}=1 disables OS trust-store injection" | ||
| elif has_explicit_ca_override(env): | ||
| source = f"explicit CA bundle: {_explicit_ca_path(env)}" |
| precedence = ( | ||
| "APM_DISABLE_TRUSTSTORE > REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE > " | ||
| "APM_EXTRA_CA_BUNDLE (when supported) > OS trust store > certifi fallback" | ||
| ) |
| ) | ||
| ) | ||
|
|
||
| # Check 4: TLS trust source (informational) |
| ) | ||
| ) | ||
|
|
||
| # Check 4: marketplace config presence + parsability |
| except Exception as exc: | ||
| tls_detail = f"Unable to determine TLS trust source: {str(exc)[:60]}" |
|
|
||
|
|
||
| def describe_tls_trust(env: Mapping[str, str] | None = None) -> tuple[str, str]: | ||
| """Return a user-facing trust source and precedence description.""" |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/apm_cli/commands/marketplace/doctor.py:198
- This introduces a second "Check 4" header; it makes the check numbering confusing when scanning the function. Consider removing the numeric label here (or renumbering all subsequent checks).
# Check 4: TLS trust source (informational)
src/apm_cli/commands/marketplace/doctor.py:205
- TLS trust diagnostic detail can include non-ASCII text (explicit CA bundle paths, truststore error messages). That can trigger UnicodeEncodeError on Windows terminals and violates the repo's printable-ASCII output constraint. Sanitize the rendered strings before adding them to the doctor table (including the exception path).
try:
from ...core.tls_trust import describe_tls_trust
tls_source, tls_precedence = describe_tls_trust()
tls_detail = f"{tls_source}; precedence: {tls_precedence}"
tests/conftest.py:149
- The new autouse SSL isolation fixture calls truststore.extract_from_ssl(), but it does not snapshot/restore ssl.SSLContext itself. Some tests (e.g. TLS integration suites) directly assign ssl.SSLContext, and without restoring it here a future test could leak a patched SSLContext into subsequent tests.
@pytest.fixture(autouse=True)
def _isolate_ssl_state():
"""Restore process-global truststore injection between tests."""
import contextlib
src/apm_cli/core/tls_trust.py:115
- describe_tls_trust() is documented as "user-facing", but it can embed non-ASCII bytes from REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE values or from exception messages recorded in _LAST_TLS_STATUS. If callers print this directly (as apm doctor does), it can violate the repo's printable-ASCII output constraint on Windows.
def describe_tls_trust(env: Mapping[str, str] | None = None) -> tuple[str, str]:
"""Return a user-facing trust source and precedence description."""
if _env_flag(_DISABLE_ENV_VAR, env):
source = f"bundled CA (certifi); {_DISABLE_ENV_VAR}=1 disables OS trust-store injection"
elif has_explicit_ca_override(env):
tests/conftest.py:129
- The linked issue acceptance criteria calls out removing the per-suite SSL/truststore isolation guards once a shared autouse fixture exists, but those guards still remain (e.g. tests/integration/test_tls_custom_ca.py:_isolate_trust and tests/integration/test_tls_frozen_hook.py:_isolate_trust). This PR adds the shared fixture but does not remove the redundant per-suite ones.
@pytest.fixture(autouse=True)
def _isolate_ssl_state():
"""Restore process-global truststore injection between tests."""
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 1 | Solid placement of describe_tls_trust in the canonical TLS module. The module-global _LAST_TLS_STATUS read is acceptable for an informational diagnostic but has a stale-read edge case worth documenting. No blocking issues. |
| CLI Logging Expert | 0 | 1 | 2 | The TLS doctor check is well-structured but the precedence chain is too verbose for a table cell -- it belongs behind --verbose. The rest is clean. |
| DevX UX Expert | 0 | 2 | 2 | The TLS trust row is a good diagnostic addition but the inline precedence string is too dense for a table cell, the 'not selected yet' message is confusing, and the error fallback lacks actionable guidance. |
| Supply Chain Security Expert | 0 | 2 | 2 | No security bypasses or credential leakage. Minor concerns around path disclosure and misleading pass status, but nothing blocking. |
| OSS Growth Hacker | 0 | 0 | 2 | Strong adoption move. TLS diagnostics in apm doctor is exactly the kind of self-service debugging that unblocks enterprise users behind corporate proxies -- the #1 silent churn vector for CLI tools in regulated environments. |
| Doc Writer | 0 | 2 | 1 | The PR adds a new TLS trust row to apm doctor output. The CHANGELOG [Unreleased] section has no entry. The docs reference page Checks table does not list TLS trust. Two targeted updates needed. |
| Test Coverage Expert | 0 | 2 | 1 | New describe_tls_trust() has zero direct tests covering its 4 branches; doctor error path untested; autouse fixture leaks _LAST_TLS_STATUS across tests. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] Add parametrized unit tests for describe_tls_trust's 4 branches -- Missing regression trap on a user-promise surface (devx principle). No automated guardrail currently proves the precedence logic is correct.
- [Test Coverage Expert] Add test for doctor TLS error-degradation path -- The except block that renders 'Unable to determine' has zero coverage -- if it regresses, doctor crashes instead of degrading gracefully.
- [CLI Logging Expert] Move precedence chain behind --verbose; show only active source in default table -- Three panelists independently flagged the 120+ char detail string as too dense for 80-col terminals. Progressive disclosure is APM's output contract.
- [Python Architect] Eagerly call configure_process_tls_trust() inside describe_tls_trust to eliminate stale-read / 'not selected yet' UX -- Eliminates confusing cold-state message and satisfies devx-ux's reword request in one line.
- [Doc Writer] Add CHANGELOG entry and update doctor.md Checks table with TLS trust row -- User-visible behavior change with no changelog or docs update -- violates 'ship fast, communicate clearly' principle.
Architecture
classDiagram
direction LR
class tls_trust {
<<Module>>
+configure_process_tls_trust() None
+describe_tls_trust(env) tuple
+has_explicit_ca_override(env) bool
-_LAST_TLS_STATUS tuple|None
-_env_flag(name, env) bool
-_explicit_ca_path(env) str
}
class doctor {
<<Module>>
+run_doctor() list~DoctorCheck~
}
class _DoctorCheck {
<<Dataclass>>
+name str
+passed bool
+detail str
+informational bool
}
doctor ..> tls_trust : imports describe_tls_trust
doctor *-- _DoctorCheck : builds
class tls_trust:::touched
class doctor:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm doctor command"] --> B{"configure_process_tls_trust\ncalled earlier?"}
B -->|Yes| C["_LAST_TLS_STATUS populated"]
B -->|No| D["_LAST_TLS_STATUS is None"]
C --> E["describe_tls_trust()\nreads _LAST_TLS_STATUS"]
D --> F["describe_tls_trust()\nreturns fallback string"]
E --> G["Renders TLS source + precedence"]
F --> G
G --> H["_DoctorCheck\ninformational=True"]
H --> I["Print doctor table"]
Recommendation
Ship now with the understanding that the test-coverage and --verbose follow-ups land before the next release cut. The core diagnostic is correct, non-blocking, and immediately valuable to the enterprise adoption surface. The missing tests are not blocking because the feature is informational-only (passed=True, informational=True) -- a regression here cannot break installs or corrupt state. Recommend the author addresses the top 3 follow-ups in a stacked PR or amends this one before merge.
Full per-persona findings
Python Architect
- [recommended] Stale-read of _LAST_TLS_STATUS produces misleading 'not selected yet' message if doctor runs before configure_process_tls_trust fires at
src/apm_cli/core/tls_trust.py
If apm doctor runs in a fresh process where configure_process_tls_trust() has not been called, _LAST_TLS_STATUS is None and the user sees 'not selected yet; OS trust store preferred with certifi fallback'. The actual trust source has not been determined yet. describe_tls_trust should eagerly call configure_process_tls_trust() (cached, so idempotent) to ensure the global is populated before reading it.
Suggested: At the top of describe_tls_trust, call configure_process_tls_trust() to ensure _LAST_TLS_STATUS is populated. - [nit] Stale Check 4 comment numbering after insertion -- two checks labeled Check 4 at
src/apm_cli/commands/marketplace/doctor.py
Purely cosmetic but makes grep-by-number unreliable.
CLI Logging Expert
- [recommended] Precedence chain concatenated into detail string is too verbose for default table output -- wraps badly at 80-col terminals at
src/apm_cli/commands/marketplace/doctor.py
The combined tls_detail string can exceed 120 chars (source + '; precedence: APM_DISABLE_TRUSTSTORE > REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE > APM_EXTRA_CA_BUNDLE (when supported) > OS trust store > certifi fallback'). In a table cell at 80-col terminal this wraps badly and buries the actionable source info. Show only tls_source in default mode; emit the full precedence chain only under --verbose. This follows APM's progressive-disclosure contract. - [nit] Precedence string uses '(when supported)' hedging language -- specify or drop at
src/apm_cli/core/tls_trust.py
Either state the condition precisely or omit the qualifier for cleaner output. - [nit] Error fallback truncation at 60 chars arbitrary -- may cut mid-word at
src/apm_cli/commands/marketplace/doctor.py
The [:60] slice could cut mid-word or mid-path. Consider truncating to last space or using a utility that appends '...' when truncated.
DevX UX Expert
- [recommended] Inline precedence chain is reference documentation, not diagnostic output -- belongs behind --verbose at
src/apm_cli/commands/marketplace/doctor.py
Show only the active source in the default view; move the precedence explanation behind --verbose or a apm doctor --explain flag. Compare: pip config debug prints the active config file, not the full resolution order inline. - [recommended] 'not selected yet' message reads as broken state to users who haven't run network commands at
src/apm_cli/core/tls_trust.py
Reword to 'OS trust store (default)' or similar positive statement -- the user doesn't need to know about lazy initialization. - [nit] Error fallback lacks actionable guidance for next steps at
src/apm_cli/commands/marketplace/doctor.py
Append a hint: 'run with --verbose or check REQUESTS_CA_BUNDLE'. Every doctor error should be one copy-paste away from recovery. - [nit] Consider surfacing REQUESTS_CA_BUNDLE hint for corporate-proxy users when no custom bundle detected at
src/apm_cli/core/tls_trust.py
Surface 'set REQUESTS_CA_BUNDLE to your corporate CA path' when no custom bundle detected and OS trust store in use -- the primary audience for this check.
Supply Chain Security Expert
- [recommended] str(exc)[:60] in error fallback may leak filesystem paths from unexpected exceptions at
src/apm_cli/commands/marketplace/doctor.py
Consider sanitizing to just the exception type name when the message contains path separators or known sensitive patterns.
Suggested: Use type(exc).name as the safe fallback, or filter the message through a path-redaction helper before truncating. - [recommended] _LAST_TLS_STATUS is module-global mutable state read without synchronization -- may be stale at
src/apm_cli/core/tls_trust.py
In long-lived processes or concurrent test runs it could reflect a stale or different trust configuration than what is currently active, making the doctor output misleading rather than informative. Document that this is a snapshot.
Suggested: Document clearly that this is a snapshot, or query ssl.SSLContext default_verify_paths() at call time. - [nit] CA bundle path exposed in diagnostic output -- low-risk but reveals filesystem layout in CI logs at
src/apm_cli/core/tls_trust.py
Exposing the full CA bundle path via _explicit_ca_path(env) is low-risk (diagnostic tool user runs locally) but in CI logs it reveals internal filesystem layout. - [nit] passed=True unconditionally may green-light a potentially insecure config (certifi fallback in corp env) at
src/apm_cli/commands/marketplace/doctor.py
Consider making it informational=True with passed=None or a tri-state so it does not green-light a potentially insecure config.
OSS Growth Hacker
- [nit] Consider linking to a docs troubleshooting anchor in the CHANGELOG entry so the surface compounds at
src/apm_cli/commands/marketplace/doctor.py
The detail string showing env-var precedence is excellent -- users paste this into Slack. Link to docs anchor to make it compound. - [nit] CHANGELOG opportunity: frame as 'apm doctor now tells you why TLS fails in corporate networks'
That framing is a repostable one-liner for enterprise DevRel channels and HN threads about proxy pain.
Auth Expert -- inactive
PR only adds a read-only TLS diagnostic reporter; no auth.py, token_manager.py, AuthResolver, or HTTP auth surfaces touched.
Doc Writer
- [recommended] Missing CHANGELOG entry for new TLS trust row in apm doctor at
CHANGELOG.md
The [Unreleased] section has no entry for this behavior change. The PR closes Addapm doctor tlstrust-source diagnostic + test-suite ssl-isolation fixture #2035 and adds a new informational TLS trust row -- this is user-visible and belongs in the changelog.
Suggested: Add under ### Fixed:apm doctornow reports the active TLS trust source as an informational row. (closes Addapm doctor tlstrust-source diagnostic + test-suite ssl-isolation fixture #2035) - [recommended] docs/src/content/docs/reference/cli/doctor.md Checks table missing TLS trust row at
docs/src/content/docs/reference/cli/doctor.md
The reference page lists every apm doctor check row but omits TLS trust. Users running apm doctor will see the row and find no explanation in the docs. - [nit] packages/apm-guide commands.md does not mention TLS trust in apm doctor description at
packages/apm-guide/.apm/skills/apm-usage/commands.md
Secondary agent-usage reference omits the new check. Lower priority.
Test Coverage Expert
- [recommended] No unit tests for describe_tls_trust() branch logic -- 4 branches uncovered at
src/apm_cli/core/tls_trust.py
describe_tls_trust has 4 distinct branches (disable env, explicit CA, cached status, cold state). grep of tests/ shows only one hit: a mock in the doctor test that never exercises the real function. No tests in tests/unit/core/test_tls_trust.py cover it.
Suggested: Add parametrized unit tests in tests/unit/core/test_tls_trust.py exercising each branch.
Proof (missing):tests/unit/core/test_tls_trust.py::test_describe_tls_trust_branches-- proves: describe_tls_trust returns correct source string for each precedence branch [devx]
assert source == 'bundled CA (certifi); APM_DISABLE_TRUSTSTORE=1 disables OS trust-store injection' - [recommended] Doctor TLS error path (except Exception block) has no test at
tests/unit/commands/test_marketplace_doctor.py
The only doctor TLS test patches describe_tls_trust to return a tuple (happy path). No test verifies the error rendering or that the check degrades gracefully.
Suggested: Add a test that patches describe_tls_trust to raise RuntimeError and asserts exit_code == 0 and 'Unable to determine' in output.
Proof (missing):tests/unit/commands/test_marketplace_doctor.py::test_tls_trust_error_degrades_gracefully-- proves: doctor does not crash when TLS introspection fails; user sees informational degradation [devx]
assert 'Unable to determine TLS trust source' in result.output - [nit] Autouse fixture _isolate_ssl_state does not reset _LAST_TLS_STATUS global -- state leaks between tests at
tests/conftest.py
The fixture clears the lru_cache and calls truststore.extract_from_ssl() but never resets the module-level _LAST_TLS_STATUS. A test calling configure_tls_trust pollutes describe_tls_trust's third branch for all subsequent tests.
Suggested: Addimport apm_cli.core.tls_trust as _tls; _tls._LAST_TLS_STATUS = Nonein the fixture teardown.
Performance Expert -- inactive
PR adds a diagnostic-only describe_tls_trust() function with no loops, no I/O, no subprocess calls; no hot-path (install/resolve/cache/transport) surfaces touched.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Closes #2035
Summary
apm doctorValidation
main