Skip to content

Commit 891be0e

Browse files
sergio-sisternes-epamSergio SisternesCopilot
authored
feat: marketplace UX, security hardening, and lockfile provenance (#514) (#677)
- Marketplace-aware view and outdated commands (standard spec) - Shadow detector for cross-marketplace supply-chain advisories - Ref immutability pins (version_pins.py) - MarketplaceValidator (schema + duplicate names) - Auth-first client fix for private repos - Lockfile provenance (discovered_via, marketplace_plugin_name) Co-authored-by: Sergio Sisternes <sergio.sisternes@epam.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 54b4ef8 commit 891be0e

25 files changed

Lines changed: 2511 additions & 43 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- `apm install --ssh` / `--https` flags and `APM_GIT_PROTOCOL=ssh|https` env to pick the initial transport for shorthand dependencies (#778)
1818
- `apm install --allow-protocol-fallback` flag and `APM_ALLOW_PROTOCOL_FALLBACK=1` env as the migration escape hatch for cross-protocol fallback (#778)
1919
- 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).
20+
- `apm view plugin@marketplace` displays marketplace plugin metadata (name, version, source, description) (#514)
21+
- `apm outdated` checks marketplace plugin refs and shows a "Source" column distinguishing marketplace vs git updates (#514)
22+
- `apm marketplace validate` command with schema validation and duplicate name detection (#514)
23+
- Ref immutability advisory: caches plugin-to-ref pins and warns when a previously pinned plugin's ref changes (#514)
24+
- Multi-marketplace shadow detection: warns when the same plugin name appears in multiple registered marketplaces (#514)
2025

2126
### Fixed
2227

docs/src/content/docs/guides/marketplaces.md

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ Marketplaces can declare a `metadata.pluginRoot` field to specify the base direc
6363

6464
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`.
6565

66+
### Versioned plugins
67+
68+
Plugins can declare a `version` field and a `source.ref` that points to a specific Git tag or commit:
69+
70+
```json
71+
{
72+
"name": "code-review",
73+
"description": "Automated code review agent",
74+
"version": "2.1.0",
75+
"source": { "type": "github", "repo": "acme/code-review-plugin", "ref": "v2.1.0" }
76+
}
77+
```
78+
79+
The `version` field is informational (displayed by `apm view` and `apm outdated`). The `source.ref` determines which Git ref APM checks out during install.
80+
6681
## Register a marketplace
6782

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

127142
```bash
143+
# Install using the source ref from the marketplace entry
128144
apm install code-review@acme-plugins
145+
146+
# Install with a specific git ref override
147+
apm install code-review@acme-plugins#v2.0.0
148+
149+
# Install from a specific branch
150+
apm install code-review@acme-plugins#main
129151
```
130152

131-
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.
153+
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.
154+
155+
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.
132156

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

159+
## View plugin details
160+
161+
Show metadata for a marketplace plugin:
162+
163+
```bash
164+
apm view code-review@acme-plugins
165+
```
166+
167+
Displays the plugin's name, version, description, source, and tags.
168+
135169
## Provenance tracking
136170

137171
Marketplace-resolved plugins are tracked in `apm.lock.yaml` with full provenance:
@@ -187,3 +221,50 @@ apm marketplace remove acme-plugins --yes
187221
```
188222

189223
Removing a marketplace does not uninstall plugins previously installed from it. Those plugins remain pinned in `apm.lock.yaml` to their resolved Git sources.
224+
225+
## Validate a marketplace
226+
227+
Check a marketplace manifest for schema errors and duplicate entries:
228+
229+
```bash
230+
apm marketplace validate acme-plugins
231+
232+
# Verbose output
233+
apm marketplace validate acme-plugins --verbose
234+
```
235+
236+
Catches: missing required fields and duplicate plugin names (case-insensitive).
237+
238+
:::note[Planned]
239+
The `--check-refs` flag will verify that source refs are reachable over the network. It is accepted but not yet implemented.
240+
:::
241+
242+
For full option details, see [CLI Commands](../../reference/cli-commands/).
243+
244+
## Security
245+
246+
### Version immutability
247+
248+
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:
249+
250+
```
251+
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.
252+
```
253+
254+
This detects marketplace maintainers (or compromised accounts) silently pointing an existing version at different code.
255+
256+
### Shadow detection
257+
258+
When installing a marketplace plugin, APM checks all other registered marketplaces for plugins with the same name. A match produces a warning:
259+
260+
```
261+
WARNING: Plugin 'code-review' also found in marketplace 'other-plugins'. Verify you are installing from the intended source.
262+
```
263+
264+
Shadow detection runs automatically during install -- no configuration required.
265+
266+
### Best practices
267+
268+
- **Use commit SHAs as refs** -- tags and branches can be moved; commit SHAs cannot.
269+
- **Keep plugin names unique across marketplaces** -- avoids shadow warnings and reduces confusion.
270+
- **Review immutability warnings** -- a changed ref for an existing version is a strong signal of tampering.

docs/src/content/docs/reference/cli-commands.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ apm install [PACKAGES...] [OPTIONS]
8181
```
8282

8383
**Arguments:**
84-
- `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`.
84+
- `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`.
8585

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

192192
# Install a plugin from a registered marketplace
193193
apm install code-review@acme-plugins
194+
195+
# Install a specific ref from a marketplace
196+
apm install code-review@acme-plugins#v2.0.0
194197
```
195198

196199
**Auto-Bootstrap Behavior:**
@@ -645,7 +648,7 @@ apm view PACKAGE [FIELD] [OPTIONS]
645648
```
646649

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

651654
**Options:**
@@ -662,6 +665,9 @@ apm view apm-sample-package
662665
# List remote tags and branches without cloning
663666
apm view microsoft/apm-sample-package versions
664667
668+
# View available versions for a marketplace plugin
669+
apm view code-review@acme-plugins
670+
665671
# Inspect a package from user scope
666672
apm view microsoft/apm-sample-package -g
667673
```
@@ -671,6 +677,7 @@ apm view microsoft/apm-sample-package -g
671677
- Shows package name, version, description, source, install path, context files, workflows, and hooks
672678
- `versions` lists remote tags and branches without cloning the repository
673679
- `versions` does not require the package to be installed locally
680+
- `NAME@MARKETPLACE` syntax shows the marketplace plugin metadata (name, version, source, description, tags)
674681

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

@@ -704,8 +711,10 @@ apm outdated -j 8
704711
- Reads the current lockfile (`apm.lock.yaml`; legacy `apm.lock` is migrated automatically)
705712
- For tag-pinned deps: compares the locked semver tag against the latest available remote tag
706713
- For branch-pinned deps: compares the locked commit SHA against the remote branch tip SHA
714+
- For marketplace deps: compares the installed ref against the marketplace entry's current `source.ref`
707715
- For deps with no ref: compares against the default branch (main/master) tip SHA
708-
- Displays `Package`, `Current`, `Latest`, and `Status` columns
716+
- Displays `Package`, `Current`, `Latest`, `Status`, and `Source` columns
717+
- `Source` shows `marketplace: <name>` for marketplace-sourced deps
709718
- Status values are `up-to-date`, `outdated`, and `unknown`
710719
- Local dependencies and Artifactory dependencies are skipped
711720

@@ -1084,6 +1093,30 @@ apm marketplace remove acme-plugins
10841093
apm marketplace remove acme-plugins --yes
10851094
```
10861095

1096+
#### `apm marketplace validate` - Validate a marketplace manifest
1097+
1098+
Validate `marketplace.json` for schema errors and duplicate plugin names.
1099+
1100+
```bash
1101+
apm marketplace validate NAME [OPTIONS]
1102+
```
1103+
1104+
**Arguments:**
1105+
- `NAME` - Name of the marketplace to validate
1106+
1107+
**Options:**
1108+
- `--check-refs` - Verify version refs are reachable (network). *Not yet implemented.*
1109+
- `-v, --verbose` - Show detailed output
1110+
1111+
**Examples:**
1112+
```bash
1113+
# Validate a marketplace
1114+
apm marketplace validate acme-plugins
1115+
1116+
# Verbose output
1117+
apm marketplace validate acme-plugins --verbose
1118+
```
1119+
10871120
### `apm search` - Search plugins in a marketplace
10881121

10891122
Search for plugins by name or description within a specific marketplace.

packages/apm-guide/.apm/skills/apm-usage/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@
5757
| `apm marketplace browse NAME` | Browse marketplace packages | -- |
5858
| `apm marketplace update [NAME]` | Update marketplace index | -- |
5959
| `apm marketplace remove NAME` | Remove a marketplace | `-y` skip confirm |
60+
| `apm marketplace validate NAME` | Validate marketplace manifest | `--check-refs`, `-v` |
6061
| `apm search QUERY@MARKETPLACE` | Search marketplace | `--limit N` |
62+
| `apm install NAME@MKT[#ref]` | Install from marketplace | Optional `#ref` override |
63+
| `apm view NAME@MARKETPLACE` | View marketplace plugin info | -- |
6164

6265
## MCP servers
6366

packages/apm-guide/.apm/skills/apm-usage/dependencies.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ dependencies:
179179
| Branch | `owner/repo#main` | Development -- tracks latest |
180180
| Commit SHA | `owner/repo#abc123d` | Maximum reproducibility |
181181
| No ref | `owner/repo` | Resolves default branch at install time |
182+
| Marketplace ref | `plugin@marketplace#ref` | Override marketplace source ref |
183+
184+
## Marketplace ref override
185+
186+
When installing from a marketplace, the `#` suffix overrides the `source.ref` from the marketplace entry:
187+
188+
| Syntax | Meaning | Example |
189+
|--------|---------|---------|
190+
| `plugin@mkt` | Use marketplace source ref | `plugin@mkt` |
191+
| `plugin@mkt#v2.0.0` | Override with specific tag | `plugin@mkt#v2.0.0` |
192+
| `plugin@mkt#main` | Override with branch | `plugin@mkt#main` |
193+
| `plugin@mkt#abc123d` | Override with commit SHA | `plugin@mkt#abc123d` |
182194

183195
## What the lockfile pins
184196

src/apm_cli/commands/install.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,20 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, lo
193193
mkt_ref = None
194194

195195
if mkt_ref is not None:
196-
plugin_name, marketplace_name = mkt_ref
196+
plugin_name, marketplace_name, version_spec = mkt_ref
197197
try:
198+
warning_handler = None
198199
if logger:
200+
warning_handler = lambda msg: logger.warning(msg)
199201
logger.verbose_detail(
200202
f" Resolving {plugin_name}@{marketplace_name} via marketplace..."
201203
)
202204
canonical_str, resolved_plugin = resolve_marketplace_plugin(
203205
plugin_name,
204206
marketplace_name,
207+
version_spec=version_spec,
205208
auth_resolver=auth_resolver,
209+
warning_handler=warning_handler,
206210
)
207211
if logger:
208212
logger.verbose_detail(

src/apm_cli/commands/marketplace.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,92 @@ def remove(name, yes, verbose):
368368
sys.exit(1)
369369

370370

371+
# ---------------------------------------------------------------------------
372+
# marketplace validate
373+
# ---------------------------------------------------------------------------
374+
375+
376+
@marketplace.command(help="Validate a marketplace manifest")
377+
@click.argument("name", required=True)
378+
@click.option(
379+
"--check-refs", is_flag=True, help="Verify version refs are reachable (network)"
380+
)
381+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output")
382+
def validate(name, check_refs, verbose):
383+
"""Validate the manifest of a registered marketplace."""
384+
logger = CommandLogger("marketplace-validate", verbose=verbose)
385+
try:
386+
from ..marketplace.client import fetch_marketplace
387+
from ..marketplace.registry import get_marketplace_by_name
388+
from ..marketplace.validator import validate_marketplace
389+
390+
source = get_marketplace_by_name(name)
391+
logger.start(f"Validating marketplace '{name}'...", symbol="gear")
392+
393+
manifest = fetch_marketplace(source, force_refresh=True)
394+
395+
logger.progress(
396+
f"Found {len(manifest.plugins)} plugins",
397+
symbol="info",
398+
)
399+
400+
# Verbose: per-plugin details
401+
if verbose:
402+
for p in manifest.plugins:
403+
source_type = "dict" if isinstance(p.source, dict) else "string"
404+
logger.verbose_detail(
405+
f" {p.name}: source type: {source_type}"
406+
)
407+
408+
# Run validation
409+
results = validate_marketplace(manifest)
410+
411+
# Check-refs placeholder
412+
if check_refs:
413+
logger.warning(
414+
"Ref checking not yet implemented -- skipping ref "
415+
"reachability checks",
416+
symbol="warning",
417+
)
418+
419+
# Render results
420+
passed = 0
421+
warning_count = 0
422+
error_count = 0
423+
click.echo()
424+
click.echo("Validation Results:")
425+
for r in results:
426+
if r.passed and not r.warnings:
427+
logger.success(
428+
f" {r.check_name}: all plugins valid", symbol="check"
429+
)
430+
passed += 1
431+
elif r.warnings and not r.errors:
432+
for w in r.warnings:
433+
logger.warning(f" {r.check_name}: {w}", symbol="warning")
434+
warning_count += len(r.warnings)
435+
else:
436+
for e in r.errors:
437+
logger.error(f" {r.check_name}: {e}", symbol="error")
438+
for w in r.warnings:
439+
logger.warning(f" {r.check_name}: {w}", symbol="warning")
440+
error_count += len(r.errors)
441+
warning_count += len(r.warnings)
442+
443+
click.echo()
444+
click.echo(
445+
f"Summary: {passed} passed, {warning_count} warnings, "
446+
f"{error_count} errors"
447+
)
448+
449+
if error_count > 0:
450+
sys.exit(1)
451+
452+
except Exception as e:
453+
logger.error(f"Failed to validate marketplace: {e}")
454+
sys.exit(1)
455+
456+
371457
# ---------------------------------------------------------------------------
372458
# Top-level search command (registered separately in cli.py)
373459
# ---------------------------------------------------------------------------
@@ -467,3 +553,4 @@ def search(expression, limit, verbose):
467553
except Exception as e:
468554
logger.error(f"Search failed: {e}")
469555
sys.exit(1)
556+

0 commit comments

Comments
 (0)