Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `apm install` now automatically discovers and deploys local `.apm/` primitives (skills, instructions, agents, prompts, hooks, commands) to target directories, with local content taking priority over dependencies on collision (#626, #644)
- Marketplace-based version management: plugins can declare `versions[]` arrays with semver version-to-ref mappings (#514)
- Semver range resolution for marketplace installs: `apm install plugin@marketplace#^2.0.0` supports `^`, `~`, `>=`, `>`, `<`, `<=`, `!=`, exact, and compound ranges (#514)
- `apm view plugin@marketplace` displays available marketplace versions with their refs (#514)
- `apm outdated` checks marketplace versions and shows a "Source" column distinguishing marketplace vs git updates (#514)
- `apm marketplace publish` command to add version entries to `marketplace.json` from `apm.yml` defaults and git HEAD (#514)
- `apm marketplace validate` command with schema validation, semver format checks, and duplicate detection (#514)
- Version immutability advisory: caches version-to-ref pins and warns when a previously pinned version's ref changes (#514)
- Multi-marketplace shadow detection: warns when the same plugin name appears in multiple registered marketplaces (#514)

### Fixed

Expand Down
114 changes: 113 additions & 1 deletion docs/src/content/docs/guides/marketplaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ Marketplaces can declare a `metadata.pluginRoot` field to specify the base direc

With `pluginRoot` set to `./plugins`, the source `"my-tool"` resolves to `owner/repo/plugins/my-tool`. Sources that already contain a path separator (e.g. `./custom/path`) are not affected by `pluginRoot`.

### Versioned plugins

Plugins can declare a `versions` array that maps semver versions to Git refs:

```json
{
"name": "code-review",
"description": "Automated code review agent",
"source": { "type": "github", "repo": "acme/code-review-plugin" },
"versions": [
{ "version": "2.1.0", "ref": "abc123def456" },
{ "version": "2.0.0", "ref": "v2.0.0" },
{ "version": "1.0.0", "ref": "v1.0.0" }
]
}
```

When `versions` is present, APM uses semver resolution instead of the source-level ref. The `ref` field accepts commit SHAs, tags, or branch names. List versions newest-first by convention.

Plugins without `versions` continue using the source-level ref -- this is fully backward compatible.

## Register a marketplace

```bash
Expand Down Expand Up @@ -125,13 +146,38 @@ use `apm marketplace browse <name>` instead.
Use the `NAME@MARKETPLACE` syntax to install a plugin from a specific marketplace:

```bash
# Install latest version
apm install code-review@acme-plugins

# Install exact version
apm install code-review@acme-plugins#2.0.0

# Install compatible range (^2.0.0 means >=2.0.0, <3.0.0)
apm install code-review@acme-plugins#^2.0.0

# Install with tilde range (~2.1.0 means >=2.1.0, <2.2.0)
apm install code-review@acme-plugins#~2.1.0

# Compound constraint
apm install code-review@acme-plugins#>=1.0.0,<3.0.0
```

APM resolves the plugin name against the marketplace index, fetches the underlying Git repository, and installs it as a standard APM dependency. The resolved source appears in `apm.yml` and `apm.lock.yaml` just like any direct dependency.
The `#` separator carries a version specifier only when the plugin declares `versions`. For plugins without `versions`, APM uses the source defined in the marketplace manifest, including any `source.ref` value; `#<ref>` does not override unversioned entries.

APM resolves the plugin name against the marketplace index, fetches the underlying Git repository using the ref defined by the selected marketplace entry, and installs it as a standard APM dependency. The resolved source appears in `apm.yml` and `apm.lock.yaml` just like any direct dependency.

For full `apm install` options, see [CLI Commands](../../reference/cli-commands/).

## View versions

Show available versions for a marketplace plugin:

```bash
apm view code-review@acme-plugins
```

Displays a table of versions with their refs, sorted newest-first. Plugins without `versions` show a "no version history" message.

## Provenance tracking

Marketplace-resolved plugins are tracked in `apm.lock.yaml` with full provenance:
Expand Down Expand Up @@ -187,3 +233,69 @@ apm marketplace remove acme-plugins --yes
```

Removing a marketplace does not uninstall plugins previously installed from it. Those plugins remain pinned in `apm.lock.yaml` to their resolved Git sources.

## Publish versions

Add a version entry to a marketplace's `marketplace.json`. Reads defaults from `apm.yml` and resolves the current Git HEAD:

```bash
# Publish current version (reads apm.yml + git HEAD)
apm marketplace publish --marketplace acme-plugins

# Publish with explicit values
apm marketplace publish --marketplace acme-plugins --plugin code-review --version 2.1.0 --ref abc123

# Preview without writing
apm marketplace publish --marketplace acme-plugins --dry-run
```

The command appends a version entry to the plugin's `versions` array. Use `--force` to overwrite an existing version entry with a different ref.

For full option details, see [CLI Commands](../../reference/cli-commands/).

## Validate a marketplace

Check a marketplace manifest for schema errors, invalid semver formats, and duplicate entries:

```bash
apm marketplace validate acme-plugins

# Verbose output
apm marketplace validate acme-plugins --verbose
```

Catches: missing required fields, malformed version strings, duplicate versions within a plugin, and duplicate plugin names (case-insensitive).

:::note[Planned]
The `--check-refs` flag will verify that version refs are reachable over the network. It is accepted but not yet implemented.
:::

For full option details, see [CLI Commands](../../reference/cli-commands/).

## Security

### Version immutability

APM caches version-to-ref mappings in `~/.apm/cache/marketplace/version-pins.json`. On subsequent installs, APM compares the marketplace ref against the cached pin. If a version's ref has changed, APM warns:

```
WARNING: Version 2.0.0 of code-review@acme-plugins ref changed: was 'v2.0.0', now 'deadbeef'. This may indicate a ref swap attack.
```

This detects marketplace maintainers (or compromised accounts) silently pointing an existing version at different code.

### Shadow detection

When installing a marketplace plugin, APM checks all other registered marketplaces for plugins with the same name. A match produces a warning:

```
WARNING: Plugin 'code-review' also found in marketplace 'other-plugins'. Verify you are installing from the intended source.
```

Shadow detection runs automatically during install -- no configuration required.

### Best practices

- **Use commit SHAs as refs** -- tags and branches can be moved; commit SHAs cannot.
- **Keep plugin names unique across marketplaces** -- avoids shadow warnings and reduces confusion.
- **Review immutability warnings** -- a changed ref for an existing version is a strong signal of tampering.
70 changes: 67 additions & 3 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ apm install [PACKAGES...] [OPTIONS]
```

**Arguments:**
- `PACKAGES` - Optional APM packages to add and install. Accepts shorthand (`owner/repo`), HTTPS URLs, SSH URLs, FQDN shorthand (`host/owner/repo`), local filesystem paths (`./path`, `../path`, `/absolute/path`, `~/path`), or marketplace references (`NAME@MARKETPLACE`). All forms are normalized to canonical format in `apm.yml`.
- `PACKAGES` - Optional APM packages to add and install. Accepts shorthand (`owner/repo`), HTTPS URLs, SSH URLs, FQDN shorthand (`host/owner/repo`), local filesystem paths (`./path`, `../path`, `/absolute/path`, `~/path`), or marketplace references (`NAME@MARKETPLACE[#version_spec]`). All forms are normalized to canonical format in `apm.yml`.

**Options:**
- `--runtime TEXT` - Target specific runtime only (copilot, codex, vscode)
Expand Down Expand Up @@ -167,6 +167,9 @@ apm install -g microsoft/apm-sample-package

# Install a plugin from a registered marketplace
apm install code-review@acme-plugins

# Install a specific version from a marketplace
apm install code-review@acme-plugins#^2.0.0
```

**Auto-Bootstrap Behavior:**
Expand Down Expand Up @@ -621,7 +624,7 @@ apm view PACKAGE [FIELD] [OPTIONS]
```

**Arguments:**
- `PACKAGE` - Package name, usually `owner/repo` or a short repo name
- `PACKAGE` - Package name: `owner/repo`, short repo name, or `NAME@MARKETPLACE` for marketplace plugins
- `FIELD` - Optional field selector. Supported value: `versions`

**Options:**
Expand All @@ -638,6 +641,9 @@ apm view apm-sample-package
# List remote tags and branches without cloning
apm view microsoft/apm-sample-package versions

# View available versions for a marketplace plugin
apm view code-review@acme-plugins

# Inspect a package from user scope
apm view microsoft/apm-sample-package -g
```
Expand All @@ -647,6 +653,7 @@ apm view microsoft/apm-sample-package -g
- Shows package name, version, description, source, install path, context files, workflows, and hooks
- `versions` lists remote tags and branches without cloning the repository
- `versions` does not require the package to be installed locally
- `NAME@MARKETPLACE` syntax shows the plugin's declared `versions` array sorted newest-first; plugins without `versions` show no version history

### `apm outdated` - Check locked dependencies for updates

Expand Down Expand Up @@ -680,8 +687,10 @@ apm outdated -j 8
- Reads the current lockfile (`apm.lock.yaml`; legacy `apm.lock` is migrated automatically)
- For tag-pinned deps: compares the locked semver tag against the latest available remote tag
- For branch-pinned deps: compares the locked commit SHA against the remote branch tip SHA
- For marketplace deps with `versions`: compares against the latest version in the marketplace (respects `version_spec` range when set)
- For deps with no ref: compares against the default branch (main/master) tip SHA
- Displays `Package`, `Current`, `Latest`, and `Status` columns
- Displays `Package`, `Current`, `Latest`, `Status`, and `Source` columns
- `Source` shows `marketplace: <name>` for marketplace-sourced deps
- Status values are `up-to-date`, `outdated`, and `unknown`
- Local dependencies and Artifactory dependencies are skipped

Expand Down Expand Up @@ -1060,6 +1069,61 @@ apm marketplace remove acme-plugins
apm marketplace remove acme-plugins --yes
```

#### `apm marketplace publish` - Publish a version entry

Add a version entry to a plugin's `versions` array in `marketplace.json`. Reads defaults from `apm.yml` and resolves the current Git HEAD.

```bash
apm marketplace publish [OPTIONS]
```

**Options:**
- `-m, --marketplace TEXT` - Target marketplace name
- `--plugin TEXT` - Plugin name in the marketplace (default: `name` from `apm.yml`)
- `--version TEXT` - Version to publish as semver `X.Y.Z` (default: `version` from `apm.yml`)
- `--ref TEXT` - Git ref or commit SHA (default: current HEAD)
- `--force` - Overwrite existing version entry with a different ref
- `--dry-run` - Show what would be published without making changes
- `-v, --verbose` - Show detailed output

**Examples:**
```bash
# Publish current version (reads apm.yml + git HEAD)
apm marketplace publish --marketplace acme-plugins

# Publish with explicit values
apm marketplace publish -m acme-plugins --plugin code-review --version 2.1.0 --ref abc123

# Preview without writing
apm marketplace publish -m acme-plugins --dry-run
```

See the [Marketplaces guide](../../guides/marketplaces/) for version schema details.

#### `apm marketplace validate` - Validate a marketplace manifest

Validate `marketplace.json` for schema errors, invalid semver formats, duplicate versions, and duplicate plugin names.

```bash
apm marketplace validate NAME [OPTIONS]
```

**Arguments:**
- `NAME` - Name of the marketplace to validate

**Options:**
- `--check-refs` - Verify version refs are reachable (network). *Not yet implemented.*
- `-v, --verbose` - Show detailed output

**Examples:**
```bash
# Validate a marketplace
apm marketplace validate acme-plugins

# Verbose output
apm marketplace validate acme-plugins --verbose
```

### `apm search` - Search plugins in a marketplace

Search for plugins by name or description within a specific marketplace.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/lockfile-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ fields:
| `is_dev` | boolean | MAY | `true` if the dependency was resolved through [`devDependencies`](../manifest-schema/#5-devdependencies). Omitted when `false`. Dev deps are excluded from `apm pack --format plugin` bundles. |
| `deployed_files` | array of strings | MUST | Every file path APM deployed for this dependency, relative to project root. |
| `source` | string | MAY | Dependency source. `"local"` for local path dependencies. Omitted for remote (git) dependencies. |
| `version_spec` | string | MAY | Original semver range from the install specifier (e.g., `"^2.0.0"`). Present only for marketplace dependencies installed with a version constraint. Used by `apm outdated` to evaluate updates within the pinned range. |
| `resolved_version` | string | MAY | Concrete version selected after marketplace semver resolution (e.g., `"2.3.1"`). Present only when APM resolved a marketplace dependency from a version constraint. Omitted for raw git refs and unversioned plugins. |
| `local_path` | string | MAY | Filesystem path (relative or absolute) to the local package. Present only when `source` is `"local"`. |

Fields with empty or default values (empty strings, `false` booleans, empty
Expand Down
4 changes: 4 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@
| `apm marketplace browse NAME` | Browse marketplace packages | -- |
| `apm marketplace update [NAME]` | Update marketplace index | -- |
| `apm marketplace remove NAME` | Remove a marketplace | `-y` skip confirm |
| `apm marketplace publish` | Publish version to marketplace.json | `-m MARKETPLACE`, `--plugin`, `--version`, `--ref`, `--force`, `--dry-run` |
| `apm marketplace validate NAME` | Validate marketplace manifest | `--check-refs`, `-v` |
| `apm search QUERY@MARKETPLACE` | Search marketplace | `--limit N` |
| `apm install NAME@MKT#^X.Y.Z` | Install with semver range | Supports `^`, `~`, `>=`, exact |
| `apm view NAME@MARKETPLACE` | View marketplace versions | -- |

## MCP servers

Expand Down
16 changes: 16 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ dependencies:
| Branch | `owner/repo#main` | Development -- tracks latest |
| Commit SHA | `owner/repo#abc123d` | Maximum reproducibility |
| No ref | `owner/repo` | Resolves default branch at install time |
| Marketplace semver | `plugin@marketplace#^2.0.0` | Marketplace plugins with `versions[]` |

## Marketplace version specifiers

When a marketplace plugin declares `versions[]`, the `#` suffix is a semver range:

| Specifier | Meaning | Example |
|-----------|---------|---------|
| `2.0.0` | Exact version | `plugin@mkt#2.0.0` |
| `^2.0.0` | Compatible (`>=2.0.0, <3.0.0`) | `plugin@mkt#^2.0.0` |
| `~2.1.0` | Patch-level (`>=2.1.0, <2.2.0`) | `plugin@mkt#~2.1.0` |
| `>=1.5.0` | Minimum version | `plugin@mkt#>=1.5.0` |
| `>=1.0.0,<3.0.0` | Compound range | `plugin@mkt#>=1.0.0,<3.0.0` |
| *(omitted)* | Latest version | `plugin@mkt` |

Plugins without `versions[]` continue using the source-level ref (backward compatible).

## What the lockfile pins

Expand Down
29 changes: 26 additions & 3 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,25 +150,41 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, lo
mkt_ref = None

if mkt_ref is not None:
plugin_name, marketplace_name = mkt_ref
plugin_name, marketplace_name, version_spec = mkt_ref
try:
if logger:
logger.verbose_detail(
f" Resolving {plugin_name}@{marketplace_name} via marketplace..."
)
canonical_str, resolved_plugin = resolve_marketplace_plugin(
canonical_str, resolved_plugin, resolved_version = resolve_marketplace_plugin(
plugin_name,
marketplace_name,
version_spec=version_spec,
auth_resolver=auth_resolver,
warning_handler=logger.warning if logger else None,
)
if logger:
logger.verbose_detail(
f" Resolved to: {canonical_str}"
)
# Show resolved version when available (marketplace installs)
# resolved_version is added to provenance by Bug B1; use
# safe .get() so this works before and after B1 lands.
# Security-critical: record marketplace provenance so
# the lockfile tracks where each dependency was
# discovered. These fields enable supply-chain audits
# and prevent silent marketplace source confusion.
marketplace_provenance = {
"discovered_via": marketplace_name,
"marketplace_plugin_name": plugin_name,
"version_spec": version_spec,
"resolved_version": resolved_version,
}
resolved_ver = marketplace_provenance.get("resolved_version")
if resolved_ver and logger:
logger.verbose_detail(
f" Resolved version: {resolved_ver}"
)
package = canonical_str
except Exception as mkt_err:
reason = str(mkt_err)
Expand Down Expand Up @@ -706,7 +722,7 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbo
)
# Short-circuit: all packages failed validation — nothing to install
if outcome.all_failed:
return
sys.exit(1)
# Note: Empty validated_packages is OK if packages are already in apm.yml
# We'll proceed with installation from apm.yml to ensure everything is synced

Expand Down Expand Up @@ -2616,11 +2632,18 @@ def _collect_descendants(node, visited=None):
if dep_key in _package_hashes:
locked_dep.content_hash = _package_hashes[dep_key]
# Attach marketplace provenance if available
# Security-critical: discovered_via and marketplace_plugin_name
# MUST be set for every marketplace-sourced dependency so the
# lockfile records supply-chain origin. Missing provenance
# would leave marketplace deps indistinguishable from direct
# Git refs, defeating audit and shadow-detection checks.
if marketplace_provenance:
for dep_key, prov in marketplace_provenance.items():
if dep_key in lockfile.dependencies:
lockfile.dependencies[dep_key].discovered_via = prov.get("discovered_via")
lockfile.dependencies[dep_key].marketplace_plugin_name = prov.get("marketplace_plugin_name")
lockfile.dependencies[dep_key].version_spec = prov.get("version_spec")
lockfile.dependencies[dep_key].resolved_version = prov.get("resolved_version")
# Selectively merge entries from the existing lockfile:
# - For partial installs (only_packages): preserve all old entries
# (sequential install — only the specified package was processed).
Expand Down
Loading
Loading