feat: Plugin coexistence — apm pack --format plugin, apm init --plugin, devDependencies#379
Merged
danielmeppiel merged 19 commits intomainfrom Mar 20, 2026
Merged
Conversation
- Add plugin_exporter.py: transforms .apm/ layout to plugin-native directories (agents, skills, commands, instructions, contexts) with collision handling, hooks/MCP merging, devDependencies filtering, and security scanning - Add synthesize_plugin_json_from_apm_yml() to plugin_parser.py for generating plugin.json from apm.yml identity fields - Wire fmt='plugin' delegation in packer.py to export_plugin_bundle() - Add --force flag to pack CLI for last-writer-wins collision override - Export export_plugin_bundle from bundle __init__.py - Add 73 unit tests covering export engine, synthesis, and packer integration - Document --format plugin in pack-distribute guide with mapping table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add --plugin flag to init: creates plugin.json + apm.yml with devDependencies - Add _validate_plugin_name() for kebab-case validation (max 64 chars) - Add _create_plugin_json() helper for plugin.json generation - Extend _create_minimal_apm_yml() with plugin= parameter - Add dev_dependencies field to APMPackage dataclass - Parse devDependencies in from_apm_yml() (same logic as dependencies) - Add get_dev_apm_dependencies() and get_dev_mcp_dependencies() accessors - Add 32 unit tests for init --plugin and devDependencies model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'Exporting APM packages as plugins' section to plugins guide - Document --plugin flag on init and --format plugin on pack in CLI reference - Add devDependencies section (§5) to manifest schema reference - Add cross-references between guides and reference pages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add recursion depth limit (_MAX_MERGE_DEPTH=20) to _deep_merge to prevent stack overflow from maliciously crafted hooks/MCP configs - Filter symlinks in tar archive creation (TOCTOU mitigation) - Add plugin format info message after successful pack - Add test for depth limit enforcement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Enables “plugin coexistence” by adding a new plugin bundle format so APM-authored packages can be exported as standalone Copilot CLI / Claude Code plugin directories, plus introduces a devDependencies manifest model and a plugin-oriented apm init flow.
Changes:
- Add
apm pack --format pluginwith a dedicated exporter that remaps.apm/artifacts into plugin-native directories, merges hooks/MCP, handles collisions (--force), and synthesizes/updatesplugin.json. - Add
apm init --pluginto generate bothapm.yml(includingdevDependencies) andplugin.json, with plugin-name validation. - Extend the manifest model (
APMPackage) and docs/tests to support and describedevDependencies.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/bundle/plugin_exporter.py |
Implements plugin export: component collection/remapping, hooks/MCP merge, devDependencies exclusion, collision handling, archiving. |
src/apm_cli/bundle/packer.py |
Delegates fmt="plugin" packing to export_plugin_bundle() and threads through force. |
src/apm_cli/bundle/__init__.py |
Re-exports export_plugin_bundle. |
src/apm_cli/commands/pack.py |
Adds --force flag and a plugin-format informational message. |
src/apm_cli/commands/init.py |
Adds --plugin flag, early plugin-name validation, and plugin-specific output/next steps. |
src/apm_cli/commands/_helpers.py |
Adds _validate_plugin_name() and _create_plugin_json(), and extends _create_minimal_apm_yml(..., plugin=...) to include devDependencies. |
src/apm_cli/models/apm_package.py |
Adds dev_dependencies parsing and accessors for dev APM/MCP deps. |
src/apm_cli/deps/plugin_parser.py |
Adds synthesize_plugin_json_from_apm_yml() helper for minimal plugin.json generation. |
tests/unit/test_plugin_exporter.py |
Unit/integration tests for plugin exporter behavior (mapping, collisions, hooks/MCP merge, devDeps filtering, archive, etc.). |
tests/unit/test_plugin_synthesis.py |
Focused tests for plugin.json synthesis from apm.yml. |
tests/unit/test_init_plugin.py |
Focused CLI tests for apm init --plugin. |
tests/unit/test_init_command.py |
Adds additional coverage for the --plugin init path and plugin name validation. |
tests/unit/test_apm_package.py |
Tests devDependencies parsing and accessors. |
docs/src/content/docs/guides/pack-distribute.md |
Documents plugin pack format and the APM→plugin output mapping table. |
docs/src/content/docs/guides/plugins.md |
Adds guidance for exporting APM packages as plugins. |
docs/src/content/docs/reference/cli-commands.md |
Documents --plugin, --force, and plugin format behavior; notes planned --dev. |
docs/src/content/docs/reference/manifest-schema.md |
Adds devDependencies schema section and updates section numbering. |
CHANGELOG.md |
Adds Unreleased entries for plugin export, plugin init, devDependencies, and related utilities. |
- Add content_hash.py utility with deterministic package hashing (sorted paths, POSIX format, skips .git/__pycache__/symlinks) - Add content_hash field to LockedDependency with backward compat - Compute hash after download during lockfile generation - Verify hash on cached packages; re-download on mismatch - 22 tests covering hashing, verification, and lockfile roundtrip Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add is_dev field to DependencyNode for dev/prod distinction - Resolver now loads root devDependencies into BFS queue with is_dev=True - Transitive deps of dev deps inherit is_dev from parent - Prod wins: if a dep appears in both dependencies and devDependencies, it is marked is_dev=False (prod takes precedence) - Add comprehensive tests for lockfile is_dev round-trip, resolver dev dep marking, prod-wins semantics, and install --dev flag behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capture SHA-256 hashes at download/verify time (3 append points) and use the cached dict when enriching the lockfile, instead of re-computing later. Prevents an attacker with filesystem access from modifying package content between verification and lockfile write. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- cli-commands: promote --dev from planned to implemented, add example - manifest-schema: update devDependencies and lockfile structure sections - lockfile-spec: add content_hash and is_dev fields, new §4.4 Content Integrity - pack-distribute: update devDependencies exclusion with --dev example - security: promote content integrity from planned to implemented Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
.apm/context/ and .apm/memory/ are APM-internal conventions, not plugin primitives. Remove their mapping to contexts/ in plugin export output, the corresponding tests, and documentation references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sanitize pkg_name/pkg_version in bundle_dir to prevent path traversal - Clean-slate bundle_dir creation + resolve-check on each dest path - Distinguish invalid plugin.json from missing plugin.json in warnings - Remove deprecated 'apm compile' from init --plugin next steps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace inline is_relative_to checks and raw shutil.rmtree with ensure_path_within, safe_rmtree, and PathTraversalError from the existing path_security module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
src/apm_cli/bundle/plugin_exporter.py:563
- Same symlink/race risk as above:
shutil.rmtree(bundle_dir)can follow a symlink and delete outsideoutput_dir. Usesafe_rmtree(bundle_dir, output_dir)(or equivalent containment + non-symlink checks) when removing the directory after creating the archive.
return info
tar.add(bundle_dir, arcname=bundle_dir.name, filter=_tar_filter)
src/apm_cli/bundle/plugin_exporter.py:515
shutil.rmtree(bundle_dir)is unsafe ifbundle_diris a symlink created by an attacker (rmtree will follow it and can delete outsideoutput_dir). Since you already havesafe_rmtree()+ensure_path_within()utilities, use them here (and/or explicitly reject symlink bundle_dir) to make the clean-slate step path-traversal safe.
)
# 11. Write files to output directory (clean slate to prevent symlink attacks)
if bundle_dir.exists():
…ering - Use sanitized bundle_dir.name for archive path + ensure_path_within - Remove dead _dep_install_path() from install.py - Skip content hash computation for local path dependencies - Remove unused 'import shutil as _shutil_hash' - Remove duplicate TestInitPluginFlag class (covered by test_init_plugin.py) - Use lockfile is_dev flag for dev dep filtering in plugin exporter, with apm.yml URL fallback for older lockfiles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'Plugin authoring' section to plugins.md explaining APM as supply-chain layer with three modes (APM-only, Plugin-only, Hybrid) - Add devDependencies section to dependencies.md guide - Add dev dependency isolation + symlink tar filtering to security.md - Add is_dev prose and example to lockfile-spec.md - Add plugin exclusion note to pack-distribute.md and cli-commands.md - Add plugin CI/CD example to ci-cd.md - Add plugin authoring cross-references to first-package.md, key-concepts.md, ide-tool-integration.md - Fix broken table in dependencies.md (3 rows concatenated on 1 line) - Fix duplicate code fence in integration-testing.md - Fix broken link in migration.md (quickstart → quick-start) - Fix CHANGELOG date typo (0.7.0: 2025-12-19 → 2024-12-19) - Fix dead WIP file reference in dependencies.md - Fix duplicate sidebar order in private-packages.md (8 → 9) - Fix gh-aw.md contradiction (claimed install && compile) - Standardize apm.yaml → apm.yml in security.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two bugs found during hero scenario E2E testing: 1. DevDependencies URL matching used only repo_url, causing ALL packages from the same repo to be excluded when any one was a devDep (e.g., marking one github/awesome-copilot sub-package as dev excluded all context-engineering skills too). Fix: use (repo_url, virtual_path) composite key for precise matching. 2. Local path dependencies with plugin.json (but no apm.yml or SKILL.md) were rejected as 'not a valid APM package'. Fix: also check for plugin.json via find_plugin_json() in both validation and install gates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mory docs - Add _collect_bare_skill() to plugin exporter for SKILL.md-only packages - Call normalize_plugin_directory() for local marketplace_plugin deps - Remove all .apm/context/ and .apm/memory/ references from docs (6 files) - Add 5 unit tests for bare skill detection (63 total plugin exporter tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the if/elif cascade that maps disk contents → PackageType into a single pure function in validation.py. Replace 3 duplicate sites: - validation.py: validate_apm_package() inline cascade - install.py:1292: fresh local install detection (missed HOOK_PACKAGE) - install.py:1497: cached package detection (missed HOOK_PACKAGE) The two install.py copies were incomplete — they lacked HOOK_PACKAGE and plugin evidence (agents/skills/commands dirs) fallback. A hooks-only package installed locally would have been silently misclassified. Add 11 unit tests covering all 6 PackageType variants + precedence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 Coexistence + Content Integrity + DevDependencies
Implements the plugin coexistence strategy from
WIP/plugin-coexistence-spec-v2.md, SHA-256 content integrity hashing (#315), and fulldevDependenciessupport.What's included
apm pack --format pluginplugin_exporter.py,packer.py,pack.pyapm init --plugininit.py,_helpers.pydevDependenciesmodelapm_package.pyapm install --devinstall.pyis_devtrackingapm_resolver.py,dependency_graph.py,lockfile.pycontent_hash.py,install.py,lockfile.pyplugin_parser.pyapm pack --format pluginExports an APM package as a standalone Copilot CLI / Claude Code plugin directory:
.apm/prompts/→commands/,.apm/context/+.apm/memory/→contexts/, etc.hooks.json, MCP configs into.mcp.jsonplugin.jsonfromapm.ymlif none existsdevDependenciesfrom output--forceflag for collision override,--dry-runfor previewapm init --pluginCreates both
plugin.jsonandapm.ymlwith adevDependenciessection. Validates plugin name per Copilot CLI spec (kebab-case, max 64 chars). No SKILL.md or empty dirs.apm install --devAdds packages to
devDependencies.apminstead ofdependencies.apm. Dev deps are installed locally but excluded fromapm pack --format pluginbundles. The resolver uses BFS with prod-wins semantics — if a dep appears as both prod and dev, it's marked as prod.SHA-256 Content Integrity (#315)
content_hash: "sha256:<hex>"in lockfileSecurity hardening
_deep_mergedepth limit (20) prevents stack overflow from malicious configsDesign decisions
plugin_exporter.pymodule — clean separation from packer. Packer orchestrates, exporter transforms.apm packdefault unchanged.Test results
Closes #378
Closes #315