fix: audit contained local plugin sources (supersedes #2444, closes #2428) - #2460
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject manifest-file symlink escapes and fail strict audit when no plugin is verified. This addresses panel and Copilot follow-ups for #2428. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make strict local-audit failures actionable, document verified local behavior, and cover metadata.pluginRoot through the installed CLI. Addresses CEO follow-ups from the PR #2444 panel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR closes the local-marketplace audit coverage gap by auditing local string-source plugins only after symlink-aware containment, and by making --strict fail when nothing could be verified (preventing false CI “success” with all-skipped plugins).
Changes:
- Add
resolve_local_plugin_path()as the canonical containment/target-resolution helper and route localapm.ymlreads through it. - Extend marketplace audit to read local plugin manifests safely (skip traversal/symlink escapes; treat malformed/unreadable as verification errors).
- Add unit + real-binary lifecycle coverage plus an architecture-boundary lint/test guard to prevent future bypasses.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/marketplace/resolver.py |
Introduces resolve_local_plugin_path() as the symlink-aware containment authority (with relative_target). |
src/apm_cli/marketplace/audit.py |
Reads local string-source apm.yml via the resolver and threads plugin_root through audit calls. |
src/apm_cli/commands/marketplace/audit.py |
Updates --strict semantics/output and adds failure when no plugins were verified. |
scripts/lint-architecture-boundaries.sh |
Adds AC10b guard to enforce the single containment authority for local audit paths. |
tests/unit/marketplace/test_marketplace_audit.py |
Adds coverage for clean local reads + traversal/symlink escape rejection + strict all-skipped exit behavior. |
tests/integration/test_marketplace_local_audit_lifecycle.py |
Adds installed-binary lifecycle tests for clean/skipped/traversal/symlink/pluginRoot scenarios. |
tests/integration/test_architecture_authorities.py |
Adds mutation-style tests proving the architecture boundary guard rejects bypasses. |
docs/src/content/docs/reference/cli/marketplace.md |
Documents local-source audit behavior and updated strict semantics. |
packages/apm-guide/.apm/skills/apm-usage/commands.md |
Updates maintained command reference to reflect new audit semantics for local sources. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
| ``_fetcher`` is a test seam with the same signature as | ||
| :func:`fetch_plugin_apm_yml`. |
| "https://microsoft.github.io/apm/reference/cli/marketplace/#apm-marketplace-audit-name" | ||
| ) | ||
|
|
||
| no_plugins_audited = ok_count == 0 and bypass_total == 0 |
Fail strict audits that skip a local source, avoid a redundant verbose recovery hint, and cover the install resolver's symlink escape boundary. Addresses final panel follow-ups for PR #2460. Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make partial skipped-source strict failures actionable and avoid a skipped-source recovery hint for verification errors. Addresses final panel follow-ups for PR #2460. Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | One resolver authority remains for local containment. |
| CLI Logging Expert | 0 | 0 | 0 | Strict skipped-source failures now explain recovery. |
| DevX UX Expert | 0 | 0 | 0 | Verbose guidance only appears when skipped reasons exist. |
| Supply Chain Security Expert | 0 | 0 | 0 | Traversal and escaping symlinks fail closed. |
| OSS Growth Hacker | 0 | 0 | 0 | Changelog and reference explain the CI contract. |
| Doc Writer | 0 | 0 | 0 | Prior validated review found documentation accurate. |
| Test Coverage Expert | 0 | 0 | 0 | Audit and install escape paths have regression coverage. |
| Performance Expert | 0 | 0 | 0 | No marketplace audit performance regression found. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Architecture
classDiagram
direction LR
class MarketplaceAuditCommand {
<<IOBoundary>>
+audit(name, strict, verbose)
}
class MarketplaceAuditModule {
<<Adapter>>
+fetch_plugin_apm_yml(plugin, source, plugin_root)
+run_audit(manifest, source)
}
class MarketplaceResolverModule {
<<CanonicalAuthority>>
+resolve_local_plugin_path(source, marketplace, plugin_root, relative_target) Path
}
class PathSecurity {
<<Guard>>
+ensure_path_within(path, base_dir) Path
}
MarketplaceAuditCommand ..> MarketplaceAuditModule : invokes
MarketplaceAuditModule ..> MarketplaceResolverModule : resolves local manifest
MarketplaceResolverModule ..> PathSecurity : delegates containment
flowchart TD
A["marketplace audit strict"] --> B["run_audit"]
B --> C["resolve_local_plugin_path target apm.yml"]
C --> D["ensure_path_within marketplace root"]
D -->|contained| E["read and audit manifest"]
D -->|escape or traversal| F["skipped source"]
E --> G{"strict?"}
F --> G
G -->|all verified| H["exit 0"]
G -->|skip error or bypass| I["exit 1"]
Folded in this run
- (panel) Strict mode now fails and explains a partial skipped source -- resolved in
a3a7a32fdanda7dd62511. - (panel) Verbose recovery guidance no longer repeats when skipped details are already visible -- resolved in
a3a7a32fd. - (panel) Local install resolution rejects escaping symlink targets -- resolved in
a3a7a32fd. - (panel) Changelog and strict-mode documentation now describe skipped-source behavior -- resolved in
a3a7a32fd.
Copilot signals reviewed
- No inline Copilot findings were present in this run.
Regression-trap evidence (mutation-break gate)
test_strict_exits_nonzero_when_a_clean_plugin_is_skipped-- deleted the strict skipped-source guard; test failed as expected; guard restored.test_install_rejects_local_marketplace_symlink_escape-- removed containment enforcement; test failed as expected; guard restored.
Lint contract
uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ were silent. Pylint R0801, auth-signal lint, and architecture-boundary lint also passed.
CI
All PR checks passed on a7dd62511d0d14a9b5515126ffd40a552feeee86 after 0 CI fix iterations.
Mergeability status
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #2460 | a7dd625 |
ship_now | 2 | 4 | 0 | 1 | green | MERGEABLE | BLOCKED | awaiting maintainer review |
Recommendation
Ship now. The exact head passed the required validation, and the panel has no remaining follow-ups.
Full per-persona findings
Python Architect
No remaining findings after convergence.
CLI Logging Expert
No remaining findings after convergence.
DevX UX Expert
No remaining findings after convergence.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
No findings.
Auth Expert -- inactive
No auth-relevant surface touched.
Doc Writer
Prior validated documentation review found no findings; final retry schema output was malformed twice.
Test Coverage Expert
No findings.
Performance Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
…ve (#2480) The v0.28.0 release build failed on the windows-latest unit shard: two assertions in tests/unit/marketplace/test_resolver_local_git.py compare `result.canonical` against an f-string that hard-codes "/" separators, so they only match on POSIX. `resolve_marketplace_plugin` returns local canonicals as OS-native filesystem paths -- #2460 routed them through `resolve_local_plugin_path`, which returns a `Path` stringified with the platform separator. That is the correct contract (`DependencyReference.is_local_path` accepts both `C:\` and `C:/` forms, and the sibling root-source test already asserts against `str(tmp_path)`), so this is a test-expectation bug, not a resolver regression. Build the expectations with `tmp_path / ... / ...` instead. Mark the three local-path tests `windows_compat` so the PR-time Windows Compatibility Gate covers them. The gate selects by marker, not by file, and these tests are exactly the backslash-separator defect class the marker exists for (see .github/instructions/tests.instructions.md). Without the marker this regression stays invisible until the post-merge Windows matrix runs -- which is how it reached a release tag. Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 11dbb707-3685-4a9e-80a4-19c73831232a
fix(marketplace): audit contained local plugin sources
TL;DR
This supersedes #2444 and closes #2428. It audits local marketplace plugin manifests only after symlink-aware containment, and strict audit now fails when no plugin could be verified. The branch also makes the failure actionable, documents the local workflow, and proves the metadata.pluginRoot path with the installed CLI.
Important
A local source that resolves outside its registered marketplace is reported as skipped and is never read.
Problem (WHY)
apm.yml.The validation approach follows "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action.". The implementation stays surgical because "Favor small, chainable primitives over monolithic frameworks.".
Approach (WHAT)
Implementation (HOW)
src/apm_cli/marketplace/resolver.py-- exposesresolve_local_plugin_path()as the containment authority for a source, optional plugin root, and requiredapm.ymltarget.src/apm_cli/marketplace/audit.py-- reads local manifests through that resolver and threadsMarketplaceManifest.plugin_rootinto each local audit.src/apm_cli/commands/marketplace/audit.py-- exits strict mode when nothing was audited and directs users to verbose skipped-source reasons.scripts/lint-architecture-boundaries.shandtests/integration/test_architecture_authorities.py-- prevent audit code from bypassing the local resolver or weakening containment.tests/unit/marketplace/test_marketplace_audit.py-- covers clean local reads, missing manifests, traversal, plugin and manifest symlink escapes, and strict status.tests/integration/test_marketplace_local_audit_lifecycle.py-- exercises clean, skipped, traversal, escaped symlink, andmetadata.pluginRootcases through the installed binary.docs/src/content/docs/reference/cli/marketplace.mdandpackages/apm-guide/.apm/skills/apm-usage/commands.md-- describe local-source scope, skip versus verification-error behavior, strict semantics, and runnable local commands.Diagrams
Legend: the new contained-resolution stage is the only path to a local
apm.yml; strict mode treats a report set with no audited plugin as a failure.flowchart LR subgraph Resolve[Resolve] S[local plugin source] --> R[resolve_local_plugin_path] R --> C{contained after resolve} end subgraph Audit[Audit] C -->|yes| M[read apm.yml] C -->|no| K[skipped report] M --> A[audit report] K --> A end subgraph Strict[Strict] A --> T{none audited} T -->|yes| E[exit 1] T -->|no| X[complete] end classDef new stroke-dasharray: 5 5; class R,C,K,T,E new;Trade-offs
--verboseremains the detailed diagnostics surface.plugin_root.Benefits
apm.ymlpassesapm marketplace audit NAME --strictwith exit 0.metadata.pluginRootis exercised by an installed-binary lifecycle test.Validation
APM_E2E_TESTS=1 uv run --extra dev pytest tests/integration/test_marketplace_local_audit_lifecycle.py::test_local_marketplace_audit_strict_lifecycle -q:uv run --extra dev pytest tests/unit/marketplace/test_marketplace_audit.py tests/integration/test_architecture_authorities.py::test_local_marketplace_audit_paths_have_single_owner tests/integration/test_architecture_authorities.py::test_local_marketplace_audit_path_owner_guard_rejects_bypass -q:Lint and mutation evidence
Removing
plugin_root=manifest.plugin_rootmadetest_local_marketplace_audit_strict_with_plugin_rootfail with the strict audit exit 1; the call was restored before validation.Scenario Evidence
tests/integration/test_marketplace_local_audit_lifecycle.py::test_local_marketplace_audit_strict_lifecycle(regression-trap for #2428)tests/integration/test_marketplace_local_audit_lifecycle.py::test_local_marketplace_audit_strict_lifecyclemetadata.pluginRootcompletes strict audit.tests/integration/test_marketplace_local_audit_lifecycle.py::test_local_marketplace_audit_strict_with_plugin_roottests/unit/marketplace/test_marketplace_audit.py::TestAuditCLI::test_strict_exits_nonzero_when_every_plugin_is_skippedHow to test
plugins/clean/apm.yml, add it, and run strict audit; expect exit 0.apm.yml; run strict audit and expect exit 1 with the verbose recovery command.../outsideand a symlink outside the root; run strict verbose audit and expect skipped reasons with exit 1.metadata.pluginRoottopluginswith a bare plugin source; strict audit should report one clean plugin.bash scripts/lint-architecture-boundaries.sh; expect the local audit resolution authority check to pass.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com