Skip to content

fix(audit): drift check skip-with-info on cache miss (closes #1200)#1289

Merged
danielmeppiel merged 6 commits into
mainfrom
fix/1200-audit-drift-skip-cache-miss
May 14, 2026
Merged

fix(audit): drift check skip-with-info on cache miss (closes #1200)#1289
danielmeppiel merged 6 commits into
mainfrom
fix/1200-audit-drift-skip-cache-miss

Conversation

@sergio-sisternes-epam

@sergio-sisternes-epam sergio-sisternes-epam commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Description

Treat cache miss during apm audit drift as skip-with-info instead of a check failure. When the install cache is not populated, the drift check now passes with an informational skip message rather than failing CI.

Fixes #1200

Changes

  • ci_checks.py: _check_drift() CacheMissError handler now returns passed=True with skip message instead of passed=False
  • audit.py: New elif branch echoes skip message to stderr for UX visibility
  • baseline-checks.md: Drift check entry updated with "Skips when" bullet for cache-miss case
  • drift-detection.md: Added cache-miss skip behaviour explanation
  • CHANGELOG.md: Fixed entry under [Unreleased]

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass (8299 passed)
  • Added tests for new functionality
    • TestCheckDriftCacheMiss: 3 unit tests covering passed=True, skip message, non-blocking behaviour
    • Updated integration test test_e10_bare_audit_surfaces_cache_miss_on_stderr

Sergio Sisternes and others added 2 commits May 12, 2026 13:33
@sergio-sisternes-epam sergio-sisternes-epam marked this pull request as ready for review May 12, 2026 12:56
Copilot AI review requested due to automatic review settings May 12, 2026 12:56
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label May 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts apm audit drift behavior so a cold install cache (CacheMissError during replay) is treated as a non-blocking skip-with-info rather than a failing check, aligning CI ergonomics for fresh checkouts.

Changes:

  • Update _check_drift to return passed=True with a clear "drift skipped: install cache not populated ..." message on cache miss.
  • Update apm audit stderr UX so bare audit surfaces the drift-skip reason (not just drift failures) when no drift findings are produced.
  • Add/adjust unit + integration tests and update docs + changelog to reflect the new skip semantics.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/apm_cli/policy/ci_checks.py Make CacheMissError in drift replay skip-with-info (passed=True) with a new informational message.
src/apm_cli/commands/audit.py Emit a stderr warning when drift was skipped (cache cold) during bare apm audit.
tests/unit/policy/test_ci_checks.py Add unit tests around cache-miss drift behavior (one test currently doesn’t exercise drift).
tests/integration/test_drift_check.py Update integration expectations for the new "drift skipped" stderr contract.
docs/src/content/docs/reference/baseline-checks.md Document drift check as failing on diffs but skipping on cold cache.
docs/src/content/docs/enterprise/drift-detection.md Document cache-cold behavior as skip-with-info and how to enable drift by warming cache.
CHANGELOG.md Add Unreleased Fixed entry describing the new drift skip-with-info behavior.

Comment thread tests/unit/policy/test_ci_checks.py
Comment thread src/apm_cli/commands/audit.py
- Add DRIFT_SKIP_PREFIX constant to ci_checks.py; use it in the
  skip message and import it in audit.py to replace the brittle
  hardcoded string literal in the startswith() check.
- Rewrite test_cache_miss_does_not_block_other_checks to call
  _check_drift directly (monkeypatch was previously a no-op via
  run_baseline_checks which never invokes _check_drift); now
  builds a CIAuditResult directly and asserts the aggregate passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 12, 2026
@github-actions

Copy link
Copy Markdown

APM Review Panel: ship_with_followups

PR #1289 converts cold-cache CI failures into informational skips, unblocking the most common first-run friction point for new APM adopters -- but ships without an integration test defending the --ci exit-code promise and with a symbol/string coupling that four panelists flagged independently.

cc @sergio-sisternes-epam @danielmeppiel -- a fresh advisory pass is ready for your review.

The correctness call is unambiguous: treating CacheMissError as passed=True is the right behavior and every active panelist agrees. The CHANGELOG and docstring both document the intent clearly. The growth signal is real -- cold-cache failures are the highest-friction first-run CI event for new adopters, and removing them without user action is a meaningful onboarding win.

Two findings demand immediate follow-up before this becomes load-bearing in production pipelines. First, the test-coverage-expert identified a missing integration test for the --ci + CacheMissError path (outcome: missing, principle: governed-by-policy). The docstring in _check_drift explicitly promises "CI does not red-mark a fresh checkout" but no automated guardrail defends this promise -- the next refactor of the CI gate logic could silently re-break issue #1200 with no test catching it. Second, supply-chain-security and devx-ux-expert converge on the same structural risk: an adversary (or misconfigured pipeline) that cold-caches the CI artifact store will cause apm audit --ci to exit 0 with drift silently unchecked. This is not a CVE -- drift is advisory by design -- but it needs a documentation commit to enterprise/security.md and a --require-drift escape hatch tracked in a follow-up issue.

The remaining convergent signal across python-architect, cli-logging-expert, devx-ux-expert, and test-coverage-expert on the CheckResult.skipped field is consistent enough to treat as a single technical-debt follow-up rather than a blocker. The [!] vs [i] symbol issue from cli-logging-expert is a real UX contract violation (passed=True should not emit a warning symbol) and is a one-line fix worth landing in this PR or as an immediate patch.

Dissent. No panelist disagreed on the core correctness of the change. The only tension is between devx-ux-expert (exit 2 on skipped checks in --ci mode) and the current advisory-only design intent. Changing the default exit code would itself be a breaking change -- the correct resolution is a future --require-drift flag, not a default behavior change in this PR.

Aligned with: Secure by default -- cold-cache bypass is a real (if low-severity) weakening of the drift gate for hardened pipelines; production CI must document the apm install prerequisite. Community-over-feature-count -- fixing #1200 directly addresses the highest-friction first-run contributor pain point. Ship fast, communicate clearly -- the behavior change is minimal, correct, and documented; the missing --ci integration test is the only gap.

Growth signal. PR #1289 removes the most common first-run CI failure for new APM adopters. Worth a dedicated bullet in the next release post under "CI onboarding improvements": "Fresh checkouts no longer fail apm audit on a cold cache -- the drift check skips with a clear message guiding you to run apm install first." Risk to monitor: if drift skips too frequently in pipelines that never call apm install, the check loses signal value and becomes background noise.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 1 Routine correctness fix, correct and minimal; elif guard couples to message-string prefix instead of first-class skipped state on CheckResult.
CLI Logging Expert 0 2 1 Cache-miss branch routes to stderr correctly; wrong STATUS_SYMBOLS key ([!] not [i]) and startswith() coupling both need attention.
DevX UX Expert 0 2 1 Directionally correct for bare audit; --ci silently exits 0 on cache miss with no distinguishable signal from a clean drift check.
Supply Chain Security Expert 0 1 1 Not a clean bypass, but cold-cache path removes the hard gate; document CI prerequisite and track --require-drift.
OSS Growth Hacker 0 2 1 Solid onboarding friction-reducer; CHANGELOG leads with impl detail and --no-drift in skip message needs qualification.
Doc Writer 0 1 2 Both changed doc files accurate; exit-code table missing the drift-skipped=exit-0 row.
Test Coverage Expert 0 1 1 Three unit tests correct; integration test e10 covers bare audit; missing --ci + CacheMissError integration test.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] Add integration test: apm audit --ci + CacheMissError asserts exit_code == 0 and passed=true in JSON output -- outcome: missing on a governed-by-policy promise; the docstring documents the intent but no automated guardrail defends it. Suggested: test_e11_ci_audit_exits_zero_on_cache_miss in tests/integration/test_drift_check.py.
  2. [CLI Logging Expert] Change STATUS_SYMBOLS['warning'] to STATUS_SYMBOLS['info'] in the passed=True skip branch (audit.py:729) -- [!] on a passed=True result is a UX contract violation; one-line fix suitable for this PR or immediate patch.
  3. [Supply Chain Security Expert] Document cold-cache bypass in enterprise/security.md and open a follow-up issue for a --require-drift flag -- convergent signal from supply-chain and devx-ux: pipelines that skip the cache-warm step will silently skip drift.
  4. [Python Architect / CLI Logging / DevX UX / Test Coverage] Add skipped: bool = False to CheckResult dataclass; replace startswith('drift skipped') guard with drift_check.skipped -- four panelists flagged this stringly-typed skip detection independently; clear technical-debt follow-up.
  5. [OSS Growth Hacker] Rewrite CHANGELOG entry to lead with user benefit, not implementation detail; qualify --no-drift mention in skip message -- low-cost copy edit before release tagging.

Architecture

classDiagram
    direction LR
    class CheckResult {
        <<ValueObject>>
        +name: str
        +passed: bool
        +message: str
        +details: list[str]
    }
    class CIAuditResult {
        <<ValueObject>>
        +checks: list[CheckResult]
        +passed: bool
    }
    class _check_drift {
        <<Pure>>
        +__call__(config, logger) tuple[CheckResult, list]
    }
    class CacheMissError {
        <<Exception>>
    }
    class _audit_content_scan {
        <<IOBoundary>>
        +__call__(cfg, ...) None
    }
    _check_drift ..> CheckResult : returns
    _check_drift ..> CacheMissError : catches
    _audit_content_scan ..> _check_drift : calls
    _audit_content_scan ..> CheckResult : reads passed, message
    CIAuditResult *-- CheckResult : aggregates
    class _check_drift:::touched
    class _audit_content_scan:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm audit / apm audit --ci"]) --> B["_audit_content_scan\naudit.py:710"]
    B --> C["_check_drift\nci_checks.py:421"]
    C --> D["run_replay\ndrift.py"]
    D -->|success| E["CheckResult\npassed=True, findings=list"]
    D -->|CacheMissError| F["CheckResult\npassed=True\nmessage='drift skipped: ...'\n[BEFORE: passed=False]"]
    D -->|other error| G["CheckResult\npassed=False"]
    E --> H{"drift_failed?"}
    F --> H
    G --> H
    H -->|"passed=False and no findings"| I["stderr: [!] drift check could not run"]
    H -->|"passed=True, no findings, startswith('drift skipped')"| J["stderr: [!] drift skipped...\n[NEW elif branch]"]
    H -->|"passed=True with findings"| K["render drift findings"]
Loading
sequenceDiagram
    participant CI as CI runner
    participant audit as audit.py
    participant check as ci_checks.py
    participant cache as install cache
    CI->>audit: apm audit --ci (fresh checkout)
    audit->>check: _check_drift(config, logger)
    check->>cache: run_replay(config, logger)
    cache-->>check: CacheMissError
    note over check: BEFORE: passed=False, AFTER: passed=True + skip message
    check-->>audit: CheckResult(passed=True, message='drift skipped...')
    audit->>CI: stderr: [!] drift skipped: install cache not populated
    note over CI: BEFORE: exit 1, AFTER: exit 0 (non-blocking)
Loading

Recommendation

Ship PR #1289. The correctness fix is sound, the growth case is concrete, and every active panelist agrees on direction. Two items should be addressed before this is cited in release notes as a CI reliability guarantee: (1) the missing --ci integration test (follow-up 1 above -- the docstring promise needs an automated guardrail or it will silently regress), and (2) the [!] vs [i] symbol fix (follow-up 2 -- a one-line change). Items 3-5 are clean follow-up issues, not blockers. The cold-cache bypass (follow-up 3) is a real supply-chain consideration for hardened enterprise pipelines and requires a documentation commit, but it does not block the merge: the behavior is correct for the documented advisory-mode design.


Full per-persona findings

Python Architect

  • [nit] audit.py elif couples to message string prefix instead of a first-class skipped state
    The new elif in audit.py disambiguates a cache-miss skip via drift_check.message.startswith("drift skipped"). This makes the caller depend on the exact wording of a message produced in ci_checks.py. A skipped: bool = False field on CheckResult would let the caller test drift_check.skipped instead.

CLI Logging Expert

  • [recommended] Wrong STATUS_SYMBOLS key for a passed=True skip at src/apm_cli/commands/audit.py:729
    STATUS_SYMBOLS['warning'] ([!]) means "yellow -- should act". But drift_check.passed is True; the check did not fail. The correct symbol is STATUS_SYMBOLS['info'] ([i]).
    Suggested: Change STATUS_SYMBOLS['warning'] to STATUS_SYMBOLS['info'] in the elif drift_check.passed branch only.

  • [recommended] startswith('drift skipped') couples routing logic to message text at src/apm_cli/commands/audit.py:726
    A refactor that rephrases the message prefix will silently regress the UX. A dedicated skipped: bool field on CheckResult makes the contract explicit.
    Suggested: Add skipped: bool = False to CheckResult dataclass. Set skipped=True in the CacheMissError handler. Change guard to elif drift_check.passed and drift_check.skipped and not drift_findings.

  • [nit] Message string embeds the discriminator prefix
    Once a skipped field exists, the 'drift skipped: ' prefix can be dropped from the message.

DevX UX Expert

  • [recommended] apm audit --ci exits 0 on cache miss with no distinguishable signal from a clean drift check at src/apm_cli/policy/ci_checks.py:450
    CI pipelines relying on apm audit --ci as a drift gate will silently regress to zero drift coverage on any workflow that does not warm the cache first. Consider a --require-drift flag or a structured skipped: true field in JSON output.

  • [recommended] Skip detection relies on brittle startswith('drift skipped') string match at src/apm_cli/commands/audit.py:726
    A skipped: bool field or CheckStatus enum would make this explicit and refactor-safe.

  • [nit] No structured skipped field in JSON/SARIF output
    passed=True with message='drift skipped...' is indistinguishable from a clean pass to machine consumers.

Supply Chain Security Expert

  • [recommended] Cold-cache bypass path: attacker who invalidates CI cache skips drift entirely at src/apm_cli/policy/ci_checks.py:450
    An adversary who can invalidate the CI artifact cache will cause apm audit --ci to exit 0 with drift silently skipped. All other ci_checks.py checks still run, so this is a partial bypass, but supply chain tampering manifesting only as a working-tree delta would go undetected.
    Suggested: (1) Document in enterprise/security.md that production CI must run apm install before apm audit --ci. (2) Track a --require-drift flag for hardened pipelines.

  • [nit] CacheMissError exception variable exc dropped -- lose diagnostic detail in structured logs at src/apm_cli/policy/ci_checks.py:450
    Dropping as exc loses the specific cache miss reason in CheckResult.message; forensic SARIF/JSON output loses the root cause string.

OSS Growth Hacker

  • [recommended] CHANGELOG entry leads with implementation detail rather than user benefit
    The entry front-loads passed=True, stderr before the user outcome. Reorder to lead with: "Fresh checkouts no longer fail apm audit due to a cold cache -- the drift check now skips with a clear message guiding you to run apm install first."

  • [recommended] Skip message offers --no-drift as escape hatch without sufficient context
    For a new user, "or pass --no-drift" implies "skip drift forever" -- the wrong lesson. Qualify: "to permanently disable the check in performance-constrained CI, pass --no-drift".

  • [nit] drift-detection.md: consider "automatically enables" for zero-config recovery feel

Auth Expert -- inactive

PR only touches audit drift-check cache-miss handling (audit.py, ci_checks.py, tests, docs/changelog). No auth.py, token_manager.py, credential resolution, host classification, or HTTP authorization surfaces are modified.

Doc Writer

  • [recommended] Exit-code table in baseline-checks.md missing the drift-skipped=exit-0 case at docs/src/content/docs/reference/baseline-checks.md
    After this PR a third observable outcome exists: drift is skipped and job exits 0 with informational message. A reader scanning the exit-code table won't find this case.
    Suggested: Add row: | Drift check skipped (cache not warmed) | 0 (informational message on stderr) |.

  • [nit] Skips when bullet contains redundant passive clause "returns a pass"
    Suggested: "Skips when. The install cache has not been warmed (the replay is cache-only by design so audit stays deterministic); an informational message advises running apm install first."

  • [nit] drift-detection.md new paragraph has run-on construction
    Suggested: "When the install cache is cold (fresh checkout before the first apm install), the drift check is skipped with an informational message; run apm install to warm the cache before the next apm audit --ci run."

Test Coverage Expert

  • [recommended] No test asserts --ci exit code is 0 on CacheMissError
    The three new unit tests confirm the CheckResult shape, and test_e10 covers bare audit stderr. But no test exercises the full --ci command path with a CacheMissError and asserts exit_code == 0.
    Proof (missing at integration-with-fixtures): tests/integration/test_drift_check.py::test_e11_ci_audit_exits_zero_on_cache_miss -- proves: apm audit --ci exits 0 (non-blocking) when drift cache is cold [devx, governed-by-policy]

  • [nit] startswith guard has no boundary test for non-skip passed=True messages
    No unit test asserts that a CheckResult with passed=True but a message not starting with 'drift skipped' does not accidentally trigger the new elif branch.
    Proof (missing at unit): tests/unit/policy/test_ci_checks.py::test_non_skip_message_does_not_trigger_skip_branch

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

Generated by PR Review Panel for issue #1289 · ● 3.1M ·

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread docs/src/content/docs/enterprise/drift-detection.md
When the install cache has not been warmed the drift check now skips
with an informational message rather than failing.  Add a sentence to
the apm-usage commands reference so the guide stays in sync with the
CLI behaviour documented in baseline-checks.md and drift-detection.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergio-sisternes-epam sergio-sisternes-epam added this pull request to the merge queue May 14, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch May 14, 2026
danielmeppiel and others added 2 commits May 14, 2026 07:12
Resolve conflicts in CHANGELOG.md (keep both entries) and tests/unit/policy/test_ci_checks.py (keep both test classes).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel merged commit 5a72809 into main May 14, 2026
11 checks passed
@danielmeppiel danielmeppiel deleted the fix/1200-audit-drift-skip-cache-miss branch May 14, 2026 05:31
danielmeppiel pushed a commit that referenced this pull request May 15, 2026
Triage of 7 Copilot comments:

LEGIT and applied:
- Replace non-ASCII em/en dashes and right-arrows in mcp_integrator.py
  and test_mcp_integrator.py with ASCII equivalents (encoding rule).
- Update explicit_target type hint from `str | None` to
  `str | list[str] | None`; document CSV-string acceptance via the
  _wire_bundle_mcp_servers caller.
- Normalize CSV `explicit_target` ("claude,copilot") to a list
  before calling active_targets(), which would otherwise treat the CSV
  as one unknown token and drop every runtime. Adds
  test_explicit_target_csv_string_normalized as regression guard.

Already addressed in 82fe862 (no-op):
- Broad `except Exception` was already narrowed to
  (ConflictingTargetsError, EmptyTargetsListError) with fail-closed
  semantics in the prior follow-up commit; comment was stale.

Rejected:
- Suggestion to reference PR # in CHANGELOG: declined. Every recent
  CHANGELOG entry references the issue # (#1313, #1289, #1222, #1299,
  etc.) -- this is the established repo convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request May 15, 2026
…1336)

* fix: respect targets: whitelist for all runtimes during MCP install (#1335)

Two defects caused the targets: field in apm.yml to be silently ignored
during MCP server installation:

1. _gate_project_scoped_runtimes read apm_config.get('target') (singular)
   instead of using parse_targets_field() which handles both target: and
   targets: keys.

2. Only codex and claude were in _PROJECT_SCOPED_RUNTIMES; all other
   runtimes (copilot, vscode, cursor, opencode, gemini, windsurf) were
   never filtered against the user's whitelist.

Fix: when an explicit targets field is present (or --target CLI flag),
gate ALL runtimes against the active target set.  When no targets field
exists, preserve backward-compat by gating only project-scoped runtimes.

Adds 10 unit tests covering both singular/plural keys, user_scope bypass,
explicit_target override, backward-compat auto-detect, and edge cases.

Closes #1335

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: act on panel review for #1336 -- fail-closed + visible whitelist gating

Implements all five panel follow-ups in-PR rather than deferring:

1. Supply-chain + py-architect: narrow except to (ConflictingTargetsError,
   EmptyTargetsListError) and fail closed with _rich_warning + return [].
   Malformed apm.yml targets no longer silently widen the MCP write surface
   via auto-detect fallback.

2. Test-coverage: two new tests cover the fail-closed path
   (test_conflicting_targets_field_fails_closed, test_empty_targets_list_fails_closed).
   12/12 TestGateProjectScopedRuntimes pass.

3. CLI-logging + DevX UX: promote whitelist gating from _log.debug to
   _rich_info so users see confirmation that their declared targets:
   filter took effect ("Targets whitelist active: skipped MCP config for X").
   The auto-detect debug log on the backward-compat path is unchanged.

4. Doc-writer: docs/consumer/install-mcp-servers.md now documents the
   targets: whitelist semantics and fail-closed behavior in the
   canonical 'What apm install writes to disk' section.

5. OSS growth: CHANGELOG entry rewritten to lead with the user promise
   ('MCP server installation now respects the explicit targets: whitelist
   for all runtimes'), name the user-facing symptom, and call out the
   backward-compat split + fail-closed semantics.

Plus the supply-chain user_scope security comment and the py-architect
falsy-list rationale comment on config_target.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address Copilot PR review on #1336

Triage of 7 Copilot comments:

LEGIT and applied:
- Replace non-ASCII em/en dashes and right-arrows in mcp_integrator.py
  and test_mcp_integrator.py with ASCII equivalents (encoding rule).
- Update explicit_target type hint from `str | None` to
  `str | list[str] | None`; document CSV-string acceptance via the
  _wire_bundle_mcp_servers caller.
- Normalize CSV `explicit_target` ("claude,copilot") to a list
  before calling active_targets(), which would otherwise treat the CSV
  as one unknown token and drop every runtime. Adds
  test_explicit_target_csv_string_normalized as regression guard.

Already addressed in 82fe862 (no-op):
- Broad `except Exception` was already narrowed to
  (ConflictingTargetsError, EmptyTargetsListError) with fail-closed
  semantics in the prior follow-up commit; comment was stale.

Rejected:
- Suggestion to reference PR # in CHANGELOG: declined. Every recent
  CHANGELOG entry references the issue # (#1313, #1289, #1222, #1299,
  etc.) -- this is the established repo convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: align MCP gate with apm-install targets engine UX

Drop the codex/claude backward-compat special case. The gate now
mirrors active_targets() resolution (explicit > directory detection >
[copilot] fallback) for every runtime, exactly the same model
'apm install' uses for skills, agents, and instructions. A user
running 'apm install' on a project with no .codex/ directory no
longer leaks an MCP write to ~/.codex/config.toml -- no matter
whether 'targets:' is declared.

Drops _PROJECT_SCOPED_RUNTIMES constant. Aliases the 'vscode'
runtime to the 'copilot' canonical target inside the gate (mirrors
the alias active_targets honors for explicit_target). Updates docs,
CHANGELOG, and the two backward-compat-specific tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: honor APM-install UX in MCP gate (audit-caught call-site bypass)

Paired CLI-logging + DevX-UX advisory review caught five UX
divergences between MCP install gating and the canonical
'apm install' targets engine. The most critical was a hidden
call-site bypass that silently broke the PR's own promise.

Audit-caught fix (B1, devx-ux blocking)
  commands/install.py:1795 built mcp_apm_config from APMPackage.target
  (singular legacy field) only. APMPackage.from_apm_yml never read
  'targets:' plural, so for any user on the canonical syntax
  apm_package.target = None, the gate saw an empty config dict, and
  fell back to permissive directory detection. The whitelist was
  silently bypassed.

  - APMPackage now exposes a 'targets: list[str] | None' field and
    parses it in from_apm_yml.
  - The call site builds mcp_apm_config conditionally, forwarding only
    the key the user actually declared (avoids tripping the gate's
    target/targets mutex check with a None placeholder).
  - Regression test test_apm_package_targets_plural_forwards_through_call_site
    locks in the full apm.yml -> APMPackage -> mcp_apm_config path.

CLI-logging UX parity (B1+B2+B3)
  - Malformed-targets branch now emits two _rich_error lines (red [x])
    instead of a single _rich_warning that glued the multi-line
    EmptyTargetsListError body mid-sentence into a yellow [!]. Mirrors
    install/phases/targets.py:213's voice. No SystemExit because the
    gate runs mid-bundle in local_bundle_handler callers.
  - Drop branch now passes symbol='info' explicitly and includes
    '(active targets: ...)' in the same '  ' double-space shape as
    format_provenance's canonical 'Targets: X  (source: Y)' line.

DevX-UX parity (B3+B4)
  - apm install --mcp NAME help text mentions the gate.
  - docs/consumer/install-mcp-servers.md cross-links to install-packages
    'Where files land' instead of duplicating the rule.

Closes the PR's own promise (#1335) end-to-end. The gate was correct;
the call site dropped 'targets:' plural before reaching it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: extend MCP-gate strictness to greenfield + apply panel docs/UX polish

Closes the last asymmetry vs 'apm install': greenfield projects
(no targets:, no --target, no detected signals) now fail closed
with the same NoHarnessError voice as the canonical install path,
instead of silently falling back to [copilot] for the MCP write.

Apply apm-review-panel recommendations:
- scsec R2: catch UnknownTargetError in the gate so malformed
  --target value renders an [x] error instead of a Python traceback.
- scsec N1: local_bundle_handler builds plural 'targets:' list shape
  (was a CSV string -- pre-empted dependency-shape drift).
- cli-log N1, N2: drop-line wording tightened ('Skipping all MCP
  config writes' for hard-fail; double-space lock between message
  and parens).
- py-arch nit3: shared RUNTIME_TO_CANONICAL_TARGET dict in
  integration/targets.py used by both alias-canonicalization in the
  gate and the runtime adapter resolver.
- devux N1 + growth N1: --mcp NAME help text wordsmithed to land
  the 'gated by targets:' contract in one line.
- doc-writer R1: packages/apm-guide commands.md --mcp row spells
  out the gate behavior + the -g carve-out.
- doc-writer R2: docs/reference/targets-matrix.md MCP section no
  longer claims unconditional writes 'for every target with an
  adapter'.
- doc-writer N1, N2 + scsec R1: docs/consumer/install-mcp-servers.md
  consolidates per-runtime opt-in + project-gate paragraphs into
  one rule, mentions greenfield NoHarnessError parity, and frames
  the -g user-scope carve-out explicitly.
- tc R1: end-to-end gating integration test covers whitelist
  suppression, multi-target allow, and greenfield fail-closed.
- growth R1: CHANGELOG leads with the user-visible delta, then
  audit detail, then the strictness extension and -g carve-out.

8480 unit tests + 3 new integration tests pass; ruff check + format
both silent.

Refs #1335

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests,docs): pass explicit_target in MCP integrator tests; fix broken docs link

The 5 failing CI tests in tests/unit/test_mcp_overlays.py and
tests/unit/test_mcp_integrator_characterisation.py called
MCPIntegrator.install(runtime="vscode") without an explicit_target.
With the new strict targets gate (#1335), the gate falls back to
cwd-based harness detection when explicit_target is None. The CI
worker subprocess cwd has no harness markers visible, so the gate
hits NoHarnessError and skips _install_for_runtime. Production
callers always pass explicit_target alongside runtime; the tests
now mirror that contract.

Also fix a broken absolute link in
docs/src/content/docs/consumer/install-mcp-servers.md
(/consumer/install-packages/#where-files-land) that did not include
the docs site base prefix /apm/. Converted to relative form
(../install-packages/#where-files-land) to match the convention used
elsewhere in the doc tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Daniel Meppiel <copilot-rework@github.com>
sergio-sisternes-epam added a commit that referenced this pull request May 19, 2026
…1289)

* fix(audit): drift check skip-with-info on cache miss (closes #1200)

* fix(audit): drift check skip-with-info on cache miss (closes #1200)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(audit): extract DRIFT_SKIP_PREFIX constant; rewrite aggregate test

- Add DRIFT_SKIP_PREFIX constant to ci_checks.py; use it in the
  skip message and import it in audit.py to replace the brittle
  hardcoded string literal in the startswith() check.
- Rewrite test_cache_miss_does_not_block_other_checks to call
  _check_drift directly (monkeypatch was previously a no-op via
  run_baseline_checks which never invokes _check_drift); now
  builds a CIAuditResult directly and asserts the aggregate passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(commands): note cache-miss drift skip in apm audit guide

When the install cache has not been warmed the drift check now skips
with an informational message rather than failing.  Add a sentence to
the apm-usage commands reference so the guide stays in sync with the
CLI behaviour documented in baseline-checks.md and drift-detection.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Sergio Sisternes <sergio.sisternes@epam.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com>
Co-authored-by: Daniel Meppiel <copilot-rework@github.com>
sergio-sisternes-epam pushed a commit that referenced this pull request May 19, 2026
…1336)

* fix: respect targets: whitelist for all runtimes during MCP install (#1335)

Two defects caused the targets: field in apm.yml to be silently ignored
during MCP server installation:

1. _gate_project_scoped_runtimes read apm_config.get('target') (singular)
   instead of using parse_targets_field() which handles both target: and
   targets: keys.

2. Only codex and claude were in _PROJECT_SCOPED_RUNTIMES; all other
   runtimes (copilot, vscode, cursor, opencode, gemini, windsurf) were
   never filtered against the user's whitelist.

Fix: when an explicit targets field is present (or --target CLI flag),
gate ALL runtimes against the active target set.  When no targets field
exists, preserve backward-compat by gating only project-scoped runtimes.

Adds 10 unit tests covering both singular/plural keys, user_scope bypass,
explicit_target override, backward-compat auto-detect, and edge cases.

Closes #1335

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: act on panel review for #1336 -- fail-closed + visible whitelist gating

Implements all five panel follow-ups in-PR rather than deferring:

1. Supply-chain + py-architect: narrow except to (ConflictingTargetsError,
   EmptyTargetsListError) and fail closed with _rich_warning + return [].
   Malformed apm.yml targets no longer silently widen the MCP write surface
   via auto-detect fallback.

2. Test-coverage: two new tests cover the fail-closed path
   (test_conflicting_targets_field_fails_closed, test_empty_targets_list_fails_closed).
   12/12 TestGateProjectScopedRuntimes pass.

3. CLI-logging + DevX UX: promote whitelist gating from _log.debug to
   _rich_info so users see confirmation that their declared targets:
   filter took effect ("Targets whitelist active: skipped MCP config for X").
   The auto-detect debug log on the backward-compat path is unchanged.

4. Doc-writer: docs/consumer/install-mcp-servers.md now documents the
   targets: whitelist semantics and fail-closed behavior in the
   canonical 'What apm install writes to disk' section.

5. OSS growth: CHANGELOG entry rewritten to lead with the user promise
   ('MCP server installation now respects the explicit targets: whitelist
   for all runtimes'), name the user-facing symptom, and call out the
   backward-compat split + fail-closed semantics.

Plus the supply-chain user_scope security comment and the py-architect
falsy-list rationale comment on config_target.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address Copilot PR review on #1336

Triage of 7 Copilot comments:

LEGIT and applied:
- Replace non-ASCII em/en dashes and right-arrows in mcp_integrator.py
  and test_mcp_integrator.py with ASCII equivalents (encoding rule).
- Update explicit_target type hint from `str | None` to
  `str | list[str] | None`; document CSV-string acceptance via the
  _wire_bundle_mcp_servers caller.
- Normalize CSV `explicit_target` ("claude,copilot") to a list
  before calling active_targets(), which would otherwise treat the CSV
  as one unknown token and drop every runtime. Adds
  test_explicit_target_csv_string_normalized as regression guard.

Already addressed in d770ce33 (no-op):
- Broad `except Exception` was already narrowed to
  (ConflictingTargetsError, EmptyTargetsListError) with fail-closed
  semantics in the prior follow-up commit; comment was stale.

Rejected:
- Suggestion to reference PR # in CHANGELOG: declined. Every recent
  CHANGELOG entry references the issue # (#1313, #1289, #1222, #1299,
  etc.) -- this is the established repo convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: align MCP gate with apm-install targets engine UX

Drop the codex/claude backward-compat special case. The gate now
mirrors active_targets() resolution (explicit > directory detection >
[copilot] fallback) for every runtime, exactly the same model
'apm install' uses for skills, agents, and instructions. A user
running 'apm install' on a project with no .codex/ directory no
longer leaks an MCP write to ~/.codex/config.toml -- no matter
whether 'targets:' is declared.

Drops _PROJECT_SCOPED_RUNTIMES constant. Aliases the 'vscode'
runtime to the 'copilot' canonical target inside the gate (mirrors
the alias active_targets honors for explicit_target). Updates docs,
CHANGELOG, and the two backward-compat-specific tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: honor APM-install UX in MCP gate (audit-caught call-site bypass)

Paired CLI-logging + DevX-UX advisory review caught five UX
divergences between MCP install gating and the canonical
'apm install' targets engine. The most critical was a hidden
call-site bypass that silently broke the PR's own promise.

Audit-caught fix (B1, devx-ux blocking)
  commands/install.py:1795 built mcp_apm_config from APMPackage.target
  (singular legacy field) only. APMPackage.from_apm_yml never read
  'targets:' plural, so for any user on the canonical syntax
  apm_package.target = None, the gate saw an empty config dict, and
  fell back to permissive directory detection. The whitelist was
  silently bypassed.

  - APMPackage now exposes a 'targets: list[str] | None' field and
    parses it in from_apm_yml.
  - The call site builds mcp_apm_config conditionally, forwarding only
    the key the user actually declared (avoids tripping the gate's
    target/targets mutex check with a None placeholder).
  - Regression test test_apm_package_targets_plural_forwards_through_call_site
    locks in the full apm.yml -> APMPackage -> mcp_apm_config path.

CLI-logging UX parity (B1+B2+B3)
  - Malformed-targets branch now emits two _rich_error lines (red [x])
    instead of a single _rich_warning that glued the multi-line
    EmptyTargetsListError body mid-sentence into a yellow [!]. Mirrors
    install/phases/targets.py:213's voice. No SystemExit because the
    gate runs mid-bundle in local_bundle_handler callers.
  - Drop branch now passes symbol='info' explicitly and includes
    '(active targets: ...)' in the same '  ' double-space shape as
    format_provenance's canonical 'Targets: X  (source: Y)' line.

DevX-UX parity (B3+B4)
  - apm install --mcp NAME help text mentions the gate.
  - docs/consumer/install-mcp-servers.md cross-links to install-packages
    'Where files land' instead of duplicating the rule.

Closes the PR's own promise (#1335) end-to-end. The gate was correct;
the call site dropped 'targets:' plural before reaching it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: extend MCP-gate strictness to greenfield + apply panel docs/UX polish

Closes the last asymmetry vs 'apm install': greenfield projects
(no targets:, no --target, no detected signals) now fail closed
with the same NoHarnessError voice as the canonical install path,
instead of silently falling back to [copilot] for the MCP write.

Apply apm-review-panel recommendations:
- scsec R2: catch UnknownTargetError in the gate so malformed
  --target value renders an [x] error instead of a Python traceback.
- scsec N1: local_bundle_handler builds plural 'targets:' list shape
  (was a CSV string -- pre-empted dependency-shape drift).
- cli-log N1, N2: drop-line wording tightened ('Skipping all MCP
  config writes' for hard-fail; double-space lock between message
  and parens).
- py-arch nit3: shared RUNTIME_TO_CANONICAL_TARGET dict in
  integration/targets.py used by both alias-canonicalization in the
  gate and the runtime adapter resolver.
- devux N1 + growth N1: --mcp NAME help text wordsmithed to land
  the 'gated by targets:' contract in one line.
- doc-writer R1: packages/apm-guide commands.md --mcp row spells
  out the gate behavior + the -g carve-out.
- doc-writer R2: docs/reference/targets-matrix.md MCP section no
  longer claims unconditional writes 'for every target with an
  adapter'.
- doc-writer N1, N2 + scsec R1: docs/consumer/install-mcp-servers.md
  consolidates per-runtime opt-in + project-gate paragraphs into
  one rule, mentions greenfield NoHarnessError parity, and frames
  the -g user-scope carve-out explicitly.
- tc R1: end-to-end gating integration test covers whitelist
  suppression, multi-target allow, and greenfield fail-closed.
- growth R1: CHANGELOG leads with the user-visible delta, then
  audit detail, then the strictness extension and -g carve-out.

8480 unit tests + 3 new integration tests pass; ruff check + format
both silent.

Refs #1335

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests,docs): pass explicit_target in MCP integrator tests; fix broken docs link

The 5 failing CI tests in tests/unit/test_mcp_overlays.py and
tests/unit/test_mcp_integrator_characterisation.py called
MCPIntegrator.install(runtime="vscode") without an explicit_target.
With the new strict targets gate (#1335), the gate falls back to
cwd-based harness detection when explicit_target is None. The CI
worker subprocess cwd has no harness markers visible, so the gate
hits NoHarnessError and skips _install_for_runtime. Production
callers always pass explicit_target alongside runtime; the tests
now mirror that contract.

Also fix a broken absolute link in
docs/src/content/docs/consumer/install-mcp-servers.md
(/consumer/install-packages/#where-files-land) that did not include
the docs site base prefix /apm/. Converted to relative form
(../install-packages/#where-files-land) to match the convention used
elsewhere in the doc tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Daniel Meppiel <copilot-rework@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

apm audit drift: skip-with-info on cache miss instead of failing

3 participants