fix: harden plugin security, validation, tests, and docs (follow-up to #83)#208
Merged
danielmeppiel merged 11 commits intomainfrom Mar 9, 2026
Merged
fix: harden plugin security, validation, tests, and docs (follow-up to #83)#208danielmeppiel merged 11 commits intomainfrom
danielmeppiel merged 11 commits intomainfrom
Conversation
- plugin_parser: skip symlinks entirely instead of dereferencing them (symlinks=False still copies target content, enabling exfiltration) - apm_package: require plugin.json or component dirs for MARKETPLACE_PLUGIN (empty/README-only directories no longer auto-classify as plugins) - agent_integrator: pick up plain .md files in .apm/agents/ (plugins may not use .agent.md convention) - deps info: resolve virtual sub-path packages at any directory depth - cli: preserve existing lockfile entries on sequential apm install
…tests - E2E hero scenario tests covering install/compile/deps/info/uninstall - E2E lockfile preservation test for sequential installs - Comprehensive plugin_parser unit tests (manifest parsing, artifact mapping, symlink skip, apm.yml generation, normalization) - Fix test_apm_package_models to match new validation behavior - Extract test_plugin_integrator_deployment as proper test method
- Correct priority: root > .github/plugin/ > .claude-plugin/ - Remove Legacy APM Format section (plugins/ directory is not checked) - Fix required fields: only name is required per the Claude plugin spec - Fix troubleshooting section to match
Contributor
There was a problem hiding this comment.
Pull request overview
This PR is a hardening follow-up to #83 (plugin management system). It fixes security, validation, and behavioral bugs introduced in #83, adds a comprehensive test suite (800+ lines), and corrects documentation.
Changes:
- Security: Replaced
symlinks=Falsewith an_ignore_symlinksignore callback inplugin_parser.pyto skip symlinks entirely during copytree, preventing content exfiltration - Validation hardening: Empty/README-only directories no longer auto-classify as
MARKETPLACE_PLUGIN;plugin.jsonor a standard component directory is now required - Bug fixes: Agent
.mddiscovery, deep virtual sub-path resolution indeps info, and lockfile preservation across sequential installs
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/deps/plugin_parser.py |
Replaces symlinks=False with _ignore_symlinks callback to skip symlinks, adds Symlinks are skipped docstring note |
src/apm_cli/models/apm_package.py |
Requires plugin.json or component dirs to classify as MARKETPLACE_PLUGIN; empty dirs now return invalid |
src/apm_cli/integration/agent_integrator.py |
Picks up plain .md files in .apm/agents/ for plugins that don't use .agent.md naming |
src/apm_cli/commands/deps.py |
Adds direct-path match for deep virtual sub-path packages before 2-level fallback scan |
src/apm_cli/cli.py |
Merges existing lockfile entries when only a subset of packages is installed |
tests/unit/test_plugin_parser.py |
New unit tests covering manifest parsing, artifact mapping, symlink skip, apm.yml generation, normalization, validation |
tests/test_apm_package_models.py |
Updates expected behavior to match new validation (empty dirs are invalid) |
tests/integration/test_plugin_e2e.py |
New E2E integration tests covering the full plugin lifecycle, lockfile preservation, symlink safety |
tests/integration/test_marketplace_plugin_integration.py |
Extracts previously orphaned test content into test_plugin_integrator_deployment method |
docs/plugins.md |
Corrects plugin.json priority order, removes non-existent legacy format, fixes required fields |
- Dynamic package type in synthesized apm.yml based on actual components (#12) - Name validation warning in parse_plugin_manifest (#13) - Clean .prompt.md rename logic (#14) - Install counter includes cached packages (#15) - Package type persisted in lockfile (#17) - Custom component path arrays from plugin.json (#23) - README: plugin as supported source, awesome-copilot example, docs link - CHANGELOG: unreleased entry for plugin support - docs: MARKETPLACE_PLUGIN in manifest-schema, dependencies, primitives
- Remove dead type-detection logic in plugin_parser.py: the computed type field was never read by the install pipeline (get_effective_type routes on file presence, not the apm.yml type field) - Hardcode type: hybrid in generated apm.yml with explanatory comment - Remove unused pkg_type parameter from _generate_apm_yml() - Fix manifest-schema.md: remove invalid marketplace_plugin from type enum, correct default to hybrid, note field is reserved for future use New E2E tests (local, no network): - test_compile_discovers_plugin_primitives - test_lockfile_package_type_roundtrip - test_generated_apm_yml_type_is_hybrid New E2E tests (network, requires GITHUB_TOKEN): - test_compile_includes_plugin_primitives - test_prune_removes_orphaned_plugin - test_install_counter_includes_plugin - test_lockfile_records_package_type - test_idempotent_reinstall
- Fix symlink test: assert copied_linked does not exist (not a conditional check). Update comments to describe _ignore_symlinks callback behavior instead of the old symlinks=False approach. - Fix deps_info test: exercise the same direct_match lookup pattern that deps info() uses, not just filesystem + from_apm_yml(). - Fix lockfile merge bug: guard merge with `not update_refs` so stale entries are not preserved when user runs `apm install --update`.
The plugin spec allows hooks to be a directory path, a config file path,
or an inline JSON object. Previously only directory paths were handled.
- Add file-path handling: "hooks": "hooks.json" copies file to
.apm/hooks/hooks.json
- Add inline-object handling: "hooks": {...} serializes to
.apm/hooks/hooks.json
- Directory path still works as before (including custom paths)
Tests: custom agents string path, skills array merge, commands path,
hooks file path, hooks inline object, hooks directory path,
nonexistent custom paths silently ignored.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
src/apm_cli/cli.py:1975
- The cached package type detection at lines 1968-1975 sets
MARKETPLACE_PLUGINonly whenplugin_json_exists and not apm_yml_exists. However, aftervalidate_apm_packagehas run and synthesized anapm.ymlfor the plugin, the conditionnot apm_yml_existsisFalse, so the type falls through toAPM_PACKAGE(line 1974). This means that on a second install (cached), the lockfilepackage_typefor plugin packages will be overwritten asapm_packageinstead ofmarketplace_plugin, defeating the purpose of tracking the package_type. The detection should also check forplugin.jsonwhenapm.ymlexists (to handle normalized plugins), or rely on the existing lockfile's storedpackage_typeas a fallback.
from apm_cli.utils.helpers import find_plugin_json
plugin_json_exists = find_plugin_json(install_path) is not None
if plugin_json_exists and not apm_yml_exists:
cached_package_info.package_type = PackageType.MARKETPLACE_PLUGIN
elif skill_md_exists and apm_yml_exists:
cached_package_info.package_type = PackageType.HYBRID
elif skill_md_exists:
cached_package_info.package_type = PackageType.CLAUDE_SKILL
elif apm_yml_exists:
cached_package_info.package_type = PackageType.APM_PACKAGE
…n.json - _resolve_sources() now accepts both files and directories (not just dirs) - Array of subdirectories preserves dir names to avoid SKILL.md overwrites - Commands mapping handles individual file paths correctly - Added 4 unit tests for file path, mixed path, and subdirectory scenarios - Updated docs/plugins.md with custom component paths documentation
- Move 'import logging' to module level (plugin_parser.py) - Exclude README.md/CHANGELOG.md/etc from .apm/agents/ plain .md scan - Restore content-driven default for 'type' field in manifest-schema.md - Update PR description to reflect intentional hybrid hardcode - Add test for non-agent .md exclusion in agent_integrator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin Support Hardening
Follow-up to #83 (SebastienDegodez's plugin support). Adds security hardening, validation, tests, and docs for the plugin pipeline.
Security & Validation
ignore=_ignore_symlinkscallback (prevents content exfiltration)namefield missing from plugin.jsonCode Fixes
apm.ymluseshybridas a safe default so the standard pipeline handles all components; actual install behavior is content-driven (presence ofSKILL.md, component directories, etc.)plugin.jsonarrays and individual file paths for non-standard directory layouts (e.g."agents": ["./agents/planner.md", "./agents/coder.md"])SKILL.mdfiles)installed_countLockedDependencypersistspackage_typefor accurate metadata.mdfiles in.apm/agents/(excludes README, CHANGELOG, etc.)--update"hooks": "hooks.json"), inline object, and directory formsDocumentation
typefield docs reflect content-driven defaultTests
test_plugin_e2e.py): Hero scenario + lockfile preservation (473+ lines)test_plugin_parser.py): Comprehensive coverage (37 tests) — custom paths, file paths, mixed paths, hooks variantsNot in this PR (waiting for #201)
apm initto minimal-only mode (breaking change) #18)mcpServers/hooks definitions in plugin.json (should-fix feat: add support for Awesome Copilot Collections as APM packages #22)These require the MCP installation pipeline from PR #201 before they can be implemented.
Test Results