fix(audit): drift check skip-with-info on cache miss (closes #1200)#1289
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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_driftto returnpassed=Truewith a clear "drift skipped: install cache not populated ..." message on cache miss. - Update
apm auditstderr 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. |
- 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>
APM Review Panel:
|
| 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
- [Test Coverage Expert] Add integration test:
apm audit --ci+CacheMissErrorassertsexit_code == 0andpassed=truein 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_missintests/integration/test_drift_check.py. - [CLI Logging Expert] Change
STATUS_SYMBOLS['warning']toSTATUS_SYMBOLS['info']in thepassed=Trueskip branch (audit.py:729) --[!]on apassed=Trueresult is a UX contract violation; one-line fix suitable for this PR or immediate patch. - [Supply Chain Security Expert] Document cold-cache bypass in
enterprise/security.mdand open a follow-up issue for a--require-driftflag -- convergent signal from supply-chain and devx-ux: pipelines that skip the cache-warm step will silently skip drift. - [Python Architect / CLI Logging / DevX UX / Test Coverage] Add
skipped: bool = FalsetoCheckResultdataclass; replacestartswith('drift skipped')guard withdrift_check.skipped-- four panelists flagged this stringly-typed skip detection independently; clear technical-debt follow-up. - [OSS Growth Hacker] Rewrite CHANGELOG entry to lead with user benefit, not implementation detail; qualify
--no-driftmention 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
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"]
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)
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.pyelif couples to message string prefix instead of a first-class skipped state
The new elif inaudit.pydisambiguates a cache-miss skip viadrift_check.message.startswith("drift skipped"). This makes the caller depend on the exact wording of a message produced inci_checks.py. Askipped: bool = Falsefield onCheckResultwould let the caller testdrift_check.skippedinstead.
CLI Logging Expert
-
[recommended] Wrong STATUS_SYMBOLS key for a
passed=Trueskip atsrc/apm_cli/commands/audit.py:729
STATUS_SYMBOLS['warning']([!]) means "yellow -- should act". Butdrift_check.passedisTrue; the check did not fail. The correct symbol isSTATUS_SYMBOLS['info']([i]).
Suggested: ChangeSTATUS_SYMBOLS['warning']toSTATUS_SYMBOLS['info']in theelif drift_check.passedbranch only. -
[recommended]
startswith('drift skipped')couples routing logic to message text atsrc/apm_cli/commands/audit.py:726
A refactor that rephrases the message prefix will silently regress the UX. A dedicatedskipped: boolfield onCheckResultmakes the contract explicit.
Suggested: Addskipped: bool = FalsetoCheckResultdataclass. Setskipped=Truein theCacheMissErrorhandler. Change guard toelif drift_check.passed and drift_check.skipped and not drift_findings. -
[nit] Message string embeds the discriminator prefix
Once askippedfield exists, the'drift skipped: 'prefix can be dropped from the message.
DevX UX Expert
-
[recommended]
apm audit --ciexits 0 on cache miss with no distinguishable signal from a clean drift check atsrc/apm_cli/policy/ci_checks.py:450
CI pipelines relying onapm audit --cias a drift gate will silently regress to zero drift coverage on any workflow that does not warm the cache first. Consider a--require-driftflag or a structuredskipped: truefield in JSON output. -
[recommended] Skip detection relies on brittle
startswith('drift skipped')string match atsrc/apm_cli/commands/audit.py:726
Askipped: boolfield orCheckStatusenum would make this explicit and refactor-safe. -
[nit] No structured
skippedfield in JSON/SARIF output
passed=Truewithmessage='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 causeapm audit --cito exit 0 with drift silently skipped. All otherci_checks.pychecks 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 inenterprise/security.mdthat production CI must runapm installbeforeapm audit --ci. (2) Track a--require-driftflag for hardened pipelines. -
[nit]
CacheMissErrorexception variableexcdropped -- lose diagnostic detail in structured logs atsrc/apm_cli/policy/ci_checks.py:450
Droppingas excloses the specific cache miss reason inCheckResult.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-loadspassed=True,stderrbefore the user outcome. Reorder to lead with: "Fresh checkouts no longer failapm auditdue to a cold cache -- the drift check now skips with a clear message guiding you to runapm installfirst." -
[recommended] Skip message offers
--no-driftas 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.mdmissing the drift-skipped=exit-0 case atdocs/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 whenbullet 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 runningapm installfirst." -
[nit]
drift-detection.mdnew paragraph has run-on construction
Suggested: "When the install cache is cold (fresh checkout before the firstapm install), the drift check is skipped with an informational message; runapm installto warm the cache before the nextapm audit --cirun."
Test Coverage Expert
-
[recommended] No test asserts
--ciexit code is 0 on CacheMissError
The three new unit tests confirm theCheckResultshape, andtest_e10covers bare audit stderr. But no test exercises the full--cicommand path with aCacheMissErrorand assertsexit_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 --ciexits 0 (non-blocking) when drift cache is cold [devx, governed-by-policy] -
[nit]
startswithguard has no boundary test for non-skippassed=Truemessages
No unit test asserts that aCheckResultwithpassed=Truebut 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 · ◷
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>
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>
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>
…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>
…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>
…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>
Description
Treat cache miss during
apm audit driftas 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 returnspassed=Truewith skip message instead ofpassed=Falseaudit.py: Newelifbranch echoes skip message to stderr for UX visibilitybaseline-checks.md: Drift check entry updated with "Skips when" bullet for cache-miss casedrift-detection.md: Added cache-miss skip behaviour explanationCHANGELOG.md: Fixed entry under[Unreleased]Type of change
Testing
TestCheckDriftCacheMiss: 3 unit tests covering passed=True, skip message, non-blocking behaviourtest_e10_bare_audit_surfaces_cache_miss_on_stderr