Skip to content

feat: Plugin coexistence — apm pack --format plugin, apm init --plugin, devDependencies#379

Merged
danielmeppiel merged 19 commits intomainfrom
feature/plugin-coexistence
Mar 20, 2026
Merged

feat: Plugin coexistence — apm pack --format plugin, apm init --plugin, devDependencies#379
danielmeppiel merged 19 commits intomainfrom
feature/plugin-coexistence

Conversation

@danielmeppiel
Copy link
Collaborator

@danielmeppiel danielmeppiel commented Mar 20, 2026

Plugin Coexistence + Content Integrity + DevDependencies

Implements the plugin coexistence strategy from WIP/plugin-coexistence-spec-v2.md, SHA-256 content integrity hashing (#315), and full devDependencies support.

What's included

Feature Files Tests
apm pack --format plugin plugin_exporter.py, packer.py, pack.py 61 + 13
apm init --plugin init.py, _helpers.py 20
devDependencies model apm_package.py 12
apm install --dev install.py 24
Resolver is_dev tracking apm_resolver.py, dependency_graph.py, lockfile.py (in 24 above)
SHA-256 content hashing (#315) content_hash.py, install.py, lockfile.py 22
Plugin.json synthesis plugin_parser.py (in 61 above)

apm pack --format plugin

Exports an APM package as a standalone Copilot CLI / Claude Code plugin directory:

  • Maps .apm/prompts/commands/, .apm/context/ + .apm/memory/contexts/, etc.
  • Merges hooks into single hooks.json, MCP configs into .mcp.json
  • Synthesizes plugin.json from apm.yml if none exists
  • Filters out devDependencies from output
  • Runs security scan on bundled files
  • --force flag for collision override, --dry-run for preview

apm init --plugin

Creates both plugin.json and apm.yml with a devDependencies section. Validates plugin name per Copilot CLI spec (kebab-case, max 64 chars). No SKILL.md or empty dirs.

apm install --dev

Adds packages to devDependencies.apm instead of dependencies.apm. Dev deps are installed locally but excluded from apm pack --format plugin bundles. 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)

  • Computes deterministic hash over sorted file paths + contents after download
  • Stores as content_hash: "sha256:<hex>" in lockfile
  • Verifies cached packages on subsequent installs (mismatch → warning + re-download)
  • TOCTOU-safe: hashes captured at download/verify time, not re-computed at lockfile write
  • Backward compatible: old lockfiles without hashes work fine

Security hardening

  • _deep_merge depth limit (20) prevents stack overflow from malicious configs
  • Tar archive symlink filter rejects symlinks injected after write
  • TOCTOU elimination in content hash verification pipeline

Design decisions

  1. New plugin_exporter.py module — clean separation from packer. Packer orchestrates, exporter transforms.
  2. Lockfile ordering for collisions — deterministic first-wins based on lockfile sort order.
  3. Prod-wins semantics — if a dependency appears via both prod and dev paths, it's marked as prod.
  4. No breaking changes — all new behavior behind flags. apm pack default unchanged.

Test results

2741 passed, 100 skipped, 9 deselected in 102.71s

Closes #378
Closes #315

danielmeppiel and others added 5 commits March 20, 2026 00:55
- 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>
Copilot AI review requested due to automatic review settings March 20, 2026 00:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 plugin with a dedicated exporter that remaps .apm/ artifacts into plugin-native directories, merges hooks/MCP, handles collisions (--force), and synthesizes/updates plugin.json.
  • Add apm init --plugin to generate both apm.yml (including devDependencies) and plugin.json, with plugin-name validation.
  • Extend the manifest model (APMPackage) and docs/tests to support and describe devDependencies.

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.

danielmeppiel and others added 7 commits March 20, 2026 01:20
- 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>
@danielmeppiel danielmeppiel requested a review from Copilot March 20, 2026 00:59
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>
Copy link
Contributor

Copilot AI left a comment

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 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 outside output_dir. Use safe_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 if bundle_dir is a symlink created by an attacker (rmtree will follow it and can delete outside output_dir). Since you already have safe_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():

danielmeppiel and others added 6 commits March 20, 2026 02:12
…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>
@danielmeppiel danielmeppiel merged commit 411aab5 into main Mar 20, 2026
9 checks passed
@danielmeppiel danielmeppiel deleted the feature/plugin-coexistence branch March 20, 2026 10:19
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.

feat: Plugin coexistence — apm pack --format plugin, apm init --plugin, devDependencies Content integrity hashing in lockfile (SHA-256)

2 participants