Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `apm install --ssh` / `--https` flags and `APM_GIT_PROTOCOL=ssh|https` env to pick the initial transport for shorthand dependencies (#778)
- `apm install --allow-protocol-fallback` flag and `APM_ALLOW_PROTOCOL_FALLBACK=1` env as the migration escape hatch for cross-protocol fallback (#778)
- Add APM Review Panel skill (`.github/skills/apm-review-panel/`) and four new specialist personas (`devx-ux-expert`, `supply-chain-security-expert`, `apm-ceo`, `oss-growth-hacker`) with auto-activating per-persona skills. Routes specialist findings through an APM CEO arbiter for strategic / breaking-change calls, with the OSS growth hacker side-channeling adoption insights via `WIP/growth-strategy.md`. Instrumentation per Handbook Ch. 9 (`The Instrumented Codebase`); PROSE-compliant (thin SKILL.md routers, persona detail lazy-loaded via markdown links, explicit boundaries per persona).
- `apm view plugin@marketplace` displays marketplace plugin metadata (name, version, source, description) (#514)
- `apm outdated` checks marketplace plugin refs and shows a "Source" column distinguishing marketplace vs git updates (#514)
- `apm marketplace validate` command with schema validation and duplicate name detection (#514)
- Ref immutability advisory: caches plugin-to-ref pins and warns when a previously pinned plugin's ref changes (#514)
- Multi-marketplace shadow detection: warns when the same plugin name appears in multiple registered marketplaces (#514)

### Fixed

Expand Down
83 changes: 82 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,21 @@ 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 `version` field and a `source.ref` that points to a specific Git tag or commit:

```json
{
"name": "code-review",
"description": "Automated code review agent",
"version": "2.1.0",
"source": { "type": "github", "repo": "acme/code-review-plugin", "ref": "v2.1.0" }
}
```

The `version` field is informational (displayed by `apm view` and `apm outdated`). The `source.ref` determines which Git ref APM checks out during install.

## Register a marketplace

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

```bash
# Install using the source ref from the marketplace entry
apm install code-review@acme-plugins

# Install with a specific git ref override
apm install code-review@acme-plugins#v2.0.0

# Install from a specific branch
apm install code-review@acme-plugins#main
```

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 raw git ref that overrides the `source.ref` from the marketplace entry. Without `#`, APM uses the ref defined in the marketplace manifest.

APM resolves the plugin name against the marketplace index, fetches the underlying Git repository using the resolved ref, 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 plugin details

Show metadata for a marketplace plugin:

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

Displays the plugin's name, version, description, source, and tags.

## Provenance tracking

Marketplace-resolved plugins are tracked in `apm.lock.yaml` with full provenance:
Expand Down Expand Up @@ -187,3 +221,50 @@ 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.

## Validate a marketplace

Check a marketplace manifest for schema errors and duplicate entries:

```bash
apm marketplace validate acme-plugins

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

Catches: missing required fields and duplicate plugin names (case-insensitive).

:::note[Planned]
The `--check-refs` flag will verify that source 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.
39 changes: 36 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[#ref]`). All forms are normalized to canonical format in `apm.yml`.

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

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

# Install a specific ref from a marketplace
apm install code-review@acme-plugins#v2.0.0
```

**Auto-Bootstrap Behavior:**
Expand Down Expand Up @@ -645,7 +648,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 @@ -662,6 +665,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 @@ -671,6 +677,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 marketplace plugin metadata (name, version, source, description, tags)

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

Expand Down Expand Up @@ -704,8 +711,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: compares the installed ref against the marketplace entry's current `source.ref`
- 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 @@ -1084,6 +1093,30 @@ apm marketplace remove acme-plugins
apm marketplace remove acme-plugins --yes
```

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

Validate `marketplace.json` for schema errors 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
3 changes: 3 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,10 @@
| `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 validate NAME` | Validate marketplace manifest | `--check-refs`, `-v` |
| `apm search QUERY@MARKETPLACE` | Search marketplace | `--limit N` |
| `apm install NAME@MKT[#ref]` | Install from marketplace | Optional `#ref` override |
| `apm view NAME@MARKETPLACE` | View marketplace plugin info | -- |

## MCP servers

Expand Down
12 changes: 12 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ 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 ref | `plugin@marketplace#ref` | Override marketplace source ref |

## Marketplace ref override

When installing from a marketplace, the `#` suffix overrides the `source.ref` from the marketplace entry:

| Syntax | Meaning | Example |
|--------|---------|---------|
| `plugin@mkt` | Use marketplace source ref | `plugin@mkt` |
| `plugin@mkt#v2.0.0` | Override with specific tag | `plugin@mkt#v2.0.0` |
| `plugin@mkt#main` | Override with branch | `plugin@mkt#main` |
| `plugin@mkt#abc123d` | Override with commit SHA | `plugin@mkt#abc123d` |

## What the lockfile pins

Expand Down
6 changes: 5 additions & 1 deletion src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,20 @@ 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:
warning_handler = None
if logger:
warning_handler = lambda msg: logger.warning(msg)
logger.verbose_detail(
f" Resolving {plugin_name}@{marketplace_name} via marketplace..."
)
canonical_str, resolved_plugin = resolve_marketplace_plugin(
plugin_name,
marketplace_name,
version_spec=version_spec,
auth_resolver=auth_resolver,
warning_handler=warning_handler,
)
if logger:
logger.verbose_detail(
Expand Down
87 changes: 87 additions & 0 deletions src/apm_cli/commands/marketplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,92 @@ def remove(name, yes, verbose):
sys.exit(1)


# ---------------------------------------------------------------------------
# marketplace validate
# ---------------------------------------------------------------------------


@marketplace.command(help="Validate a marketplace manifest")
@click.argument("name", required=True)
@click.option(
"--check-refs", is_flag=True, help="Verify version refs are reachable (network)"
)
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output")
def validate(name, check_refs, verbose):
"""Validate the manifest of a registered marketplace."""
logger = CommandLogger("marketplace-validate", verbose=verbose)
try:
from ..marketplace.client import fetch_marketplace
from ..marketplace.registry import get_marketplace_by_name
from ..marketplace.validator import validate_marketplace

source = get_marketplace_by_name(name)
logger.start(f"Validating marketplace '{name}'...", symbol="gear")

manifest = fetch_marketplace(source, force_refresh=True)

logger.progress(
f"Found {len(manifest.plugins)} plugins",
symbol="info",
)

# Verbose: per-plugin details
if verbose:
for p in manifest.plugins:
source_type = "dict" if isinstance(p.source, dict) else "string"
logger.verbose_detail(
f" {p.name}: source type: {source_type}"
)

# Run validation
results = validate_marketplace(manifest)

# Check-refs placeholder
if check_refs:
logger.warning(
"Ref checking not yet implemented -- skipping ref "
"reachability checks",
symbol="warning",
)

# Render results
passed = 0
warning_count = 0
error_count = 0
click.echo()
click.echo("Validation Results:")
for r in results:
if r.passed and not r.warnings:
logger.success(
f" {r.check_name}: all plugins valid", symbol="check"
)
passed += 1
elif r.warnings and not r.errors:
for w in r.warnings:
logger.warning(f" {r.check_name}: {w}", symbol="warning")
warning_count += len(r.warnings)
else:
for e in r.errors:
logger.error(f" {r.check_name}: {e}", symbol="error")
for w in r.warnings:
logger.warning(f" {r.check_name}: {w}", symbol="warning")
error_count += len(r.errors)
warning_count += len(r.warnings)

click.echo()
click.echo(
f"Summary: {passed} passed, {warning_count} warnings, "
f"{error_count} errors"
)

if error_count > 0:
sys.exit(1)

except Exception as e:
logger.error(f"Failed to validate marketplace: {e}")
sys.exit(1)


# ---------------------------------------------------------------------------
# Top-level search command (registered separately in cli.py)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -467,3 +553,4 @@ def search(expression, limit, verbose):
except Exception as e:
logger.error(f"Search failed: {e}")
sys.exit(1)

Loading
Loading