You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix: target-agnostic local-bundle install (#1207)
apm pack no longer hardcodes pack.target: copilot. Bundles now ship in
the Anthropic plugin layout as a transport convention only -- the
consumer target is resolved by apm install from project context and
primitives are routed per the resolved target's layout.
Defects fixed (per #1207 RCA):
- D1: pack records pack.target = "all" by default (or the auto-detected
target). The flag remains for back-compat but is no longer the
authoritative binding.
- D2.a: plugin.json is skipped on install regardless of casing in
the bundle manifest. .mcp.json is also skipped from the verbatim
deploy loop -- MCP wiring is surfaced as a follow-up notice.
- D2.b: instructions/*.md for compile-only targets (opencode, codex,
gemini) are now staged under apm_modules/<slug>/.apm/instructions/
so apm compile picks them up. Slug is validated through
validate_path_segments and the destination is checked with
ensure_path_within to block traversal escapes.
- D3: the local-bundle install path sets summary_rendered = True
before returning so the outer finally-block no longer prints a
misleading "Install interrupted" line on success.
check_target_mismatch returns None when the bundle records "all", so
target-agnostic bundles never warn against any consumer layout.
Tests:
- tests/unit/install/test_install_local_bundle_issue1207.py (13 cases)
- tests/integration/test_install_local_bundle_e2e.py
TestInstallLocalBundleIssue1207: 7-row pack -> install matrix over
copilot, claude, cursor, opencode, codex, gemini plus a multi-target
consumer asserting both native and staged deployment paths.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: stamp CHANGELOG with PR #1217
* Address panel review findings for #1207
- Case-insensitive .mcp.json skip in deploy loop and fallback walk
(services.py) so case-folding filesystems cannot smuggle a renamed
MCP file past the skip.
- Tighten staged_instructions disjunct in local_bundle_handler.py
(drop dead first disjunct that always triggered for compile-only
targets) and case-insensitive bundle_mcp detection.
- Compile hint now names the resolved target and primitive count.
- "Share with: apm install <bundle>" success line in apm pack output.
- Unit test class TestMcpJsonNeverDeployed covers case variants.
- e2e compile-hint assertion uses whitespace-collapsed output to
survive logger line-wrap.
- Docs sweep:
* pack-distribute.md: rewrote "Targeting mental model" as
target-agnostic; removed --target examples; simplified cross-
target -> plugin layout normalization.
* cli-commands.md: marked apm pack --target deprecated; updated
bundle install section; removed target-filter table.
* lockfile-spec.md: pack.target is deprecated/optional informational
metadata; bundle_files documented.
* enterprise/security.md: added apm_modules/ allowed prefix with
slug-validation note; new "Local bundle install trust model"
subsection.
* apm-usage skill (commands.md, workflow.md): pack.target is
informational; OpenCode added to compile-needed list; new
"Local bundle install" subsection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Iterate on panel review: harden slug validation + cleanup
Address apm-review-panel findings on PR #1217:
- sec-1: Enforce documented [A-Za-z0-9._-] slug whitelist in
install/services.py before validate_path_segments, with explicit
rejection of leading/trailing dots, '..', forward slashes, null bytes,
and whitespace. Aligns code with security.md trust model.
- sec-3: Add parameterized adversarial slug test cases
(forward slash, null byte, leading/trailing dot, '@', whitespace).
- py-arch-2: Hoist plugin.json / .mcp.json case-insensitive metadata
skip above the per-target deploy loop so multi-target installs do
not inflate the skipped counter once per target.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1217 review: ASCII slug, MCP wiring, doc audit
- ASCII-only slug whitelist
- Preserve nested instruction subdirs in stage_root
- Pack success line mentions embedded apm.lock.yaml
- Strengthened plugin.json leak assertion (whole-tree walk)
- URL trailing-dot fix in security/marketplace error messages
- Auto-wire bundle .mcp.json through MCPIntegrator (multi-target)
so non-Claude harnesses get servers in their native MCP config
- New TestBundleMcpWiring unit suite (parse + wire helpers)
- Doc audit: pack-distribute, lockfile-spec, plugins,
cli-commands, first-package, security, CHANGELOG
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* tests: add e2e coverage for bundle .mcp.json -> MCPIntegrator boundary
Four e2e cases covering the wiring path that was previously only
covered at the unit level:
- bundle .mcp.json reaches MCPIntegrator.install with the bundle's
servers and the resolved-target CSV passed via explicit_target
- bundle without .mcp.json does not invoke the integrator (no
spurious 'No MCP dependencies found' warnings on every install)
- integrator failure does not break the install (file deploys are
not undone by an MCP wiring hiccup)
- dry-run never fires the integrator (zero side effects on the
consumer's MCP config)
Per-target file writes (Claude project .mcp.json, .vscode/mcp.json,
.cursor/mcp.json) are owned by MCPIntegrator's own suite; testing
them here would require installed runtime binaries on the CI host.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: fix broken anchor #authoring-workflow -> #plugin-authoring
Deploy Docs CI fails link validation; the actual heading is
'Plugin authoring' at the top of guides/plugins.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Daniel Meppiel <copilot-rework@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Daniel Meppiel <[email protected]>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
22
-`apm install` now accepts the YAML list form under `target:` (e.g. `target: [copilot, claude]`); previously crashed with a garbled `Unknown target` error. (#1197)
23
23
-`apm install --update` now falls back from a stale `ADO_APM_PAT` to an `az login` AAD bearer in the preflight auth probe, matching the behavior of `apm install` and every other ADO call site. Previously the preflight raised `AuthenticationError` on 401/403 even when `az login` would have succeeded. The bearer env also pops any pre-existing `GIT_TOKEN` so the JWT flows only via `GIT_CONFIG_VALUE_0`, and the per-host stale-PAT warning dedup is lock-guarded so parallel installs against the same ADO host emit one warning instead of one-per-thread. (#1212)
24
24
-`Unknown target` error suggestions no longer advertise the `agent-skills` meta-target, which `apm targets` intentionally omits from its table. The canonical set still accepts `agent-skills` via `--target` and `apm.yml`, but the recovery path printed on errors now matches what the discovery command actually lists. (#1215)
25
+
-`apm pack` no longer hardcodes `pack.target` into bundles; bundles are target-agnostic and `apm install <bundle>` resolves the consumer target from project context and wires bundle `.mcp.json` servers per target via `MCPIntegrator`. (#1217)
Copy file name to clipboardExpand all lines: docs/src/content/docs/enterprise/security.md
+10-1Lines changed: 10 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -190,11 +190,20 @@ APM deploys files only to controlled subdirectories within the project root.
190
190
All deploy paths are validated before any file operation:
191
191
192
192
1. **No `..` segments.** Any path containing `..` is rejected outright.
193
-
2. **Allowed prefixes only.** Paths must start with an allowed prefix (`.github/`, `.claude/`, `.cursor/`, or `.opencode/`).
193
+
2. **Allowed prefixes only.** Paths must start with an allowed target-integrator prefix (`.github/`, `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.gemini/`, `.windsurf/`, `.agents/`). In addition, the local-bundle install path stages instructions for compile-only targets under `apm_modules/<slug>/.apm/instructions/` with its own containment check (the resolved path must remain within `apm_modules/`) and `<slug>` validation rejecting traversal sequences and characters outside `[A-Za-z0-9._-]`.
194
194
3. **Resolution containment.** The fully resolved path must remain within the project root directory.
195
195
196
196
A path must pass all three checks. Failure on any check prevents the file from being written.
197
197
198
+
### Local bundle install trust model
199
+
200
+
`apm install <bundle>` accepts a directory or `.tar.gz` produced by `apm pack`. Bundles are imperative (no policy / dependency-resolver / network) and target-agnostic; the consumer's project drives where files land. Trust boundaries:
201
+
202
+
1. **`bundle_files` keys are untrusted.** They come from the bundle's own `apm.lock.yaml` and are validated for traversal sequences before any filesystem path is constructed; resolved destinations must remain within the deploy root. Unsafe entries are skipped with a warning.
203
+
2. **`plugin.json` is bundle metadata, never deployed.** It is recognized case-insensitively and skipped in both the manifest-driven deploy loop and the lockfile-less fallback walk so case-folding filesystems (HFS+, NTFS) cannot smuggle a renamed file past the skip.
204
+
3. **`.mcp.json` is bundle metadata, never deployed verbatim.** It is recognized case-insensitively and skipped from the deploy loop. After files deploy, `apm install` parses the bundle's `.mcp.json` (Anthropic plugin schema, `mcpServers` map) and routes each entry through `MCPIntegrator.install` as a self-defined dependency, so the consumer's resolved target(s) get the servers in their own native MCP config (Claude `.mcp.json`, Copilot `~/.copilot/mcp-config.json`, VS Code `.vscode/mcp.json`, Cursor `.cursor/mcp.json`, etc.). `MCPIntegrator` enforces the same validation and runtime gating used by `apm.yml`-declared servers; per-server parse errors are isolated and do not block the rest of the install.
205
+
4. **Slug validation.** The bundle's `id` (used as `<slug>` for staged instructions and the install label) is rejected if it contains traversal sequences or characters outside `[A-Za-z0-9._-]`.
206
+
198
207
### Symlink handling
199
208
200
209
Symlinks are never followed during file discovery or artifact operations:
Copy file name to clipboardExpand all lines: docs/src/content/docs/guides/pack-distribute.md
+29-48Lines changed: 29 additions & 48 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,15 +36,9 @@ The left side (install, pack) runs where APM is available. The right side (downl
36
36
Creates a self-contained bundle from installed dependencies. Reads the `deployed_files` manifest in `apm.lock.yaml` as the source of truth -- it does not scan the disk.
37
37
38
38
```bash
39
-
# Default: Claude Code plugin directory, target auto-detected
39
+
# Default: target-agnostic plugin bundle that installs into any consumer
# Legacy APM bundle layout (consumed by microsoft/apm-action restore)
49
43
apm pack --format apm
50
44
@@ -63,61 +57,42 @@ apm pack --dry-run
63
57
| Flag | Default | Description |
64
58
|------|---------|-------------|
65
59
|`--format`|`plugin`| Bundle format. `plugin` emits a Claude Code plugin directory with `plugin.json`. `apm` emits the legacy APM bundle layout. |
66
-
|`-t, --target`|auto-detect|File filter: `copilot`, `claude`, `cursor`, `opencode`, `all`. `vscode` is a deprecated alias for `copilot`. |
60
+
|`-t, --target`|(deprecated)|Deprecated. Emits a warning; the value is recorded in `pack.target` as diagnostic metadata only and is ignored by `apm install` target resolution. Bundles are target-agnostic; the consumer's project decides where files land at install time. |
67
61
|`--archive`| off | Produce `.tar.gz` instead of directory |
68
62
|`-o, --output`|`./build`| Output directory |
69
63
|`--dry-run`| off | List files without writing |
70
64
|`--force`| off | On collision (plugin format), last writer wins |
71
65
72
-
### Target filtering
73
-
74
-
The target flag controls which deployed files are included based on path prefix:
75
-
76
-
| Target | Includes |
77
-
|--------|----------|
78
-
|`copilot`| Paths starting with `.github/`|
79
-
|`vscode`| Deprecated alias for `copilot`|
80
-
|`claude`| Paths starting with `.claude/`|
81
-
|`cursor`| Paths starting with `.cursor/`|
82
-
|`opencode`| Paths starting with `.opencode/`|
83
-
|`all`|`.github/`, `.claude/`, `.cursor/`, and `.opencode/`|
84
-
85
-
When no target is specified, APM auto-detects from the `target` field in `apm.yml`, falling back to `all`.
86
-
87
-
### Cross-target path mapping
66
+
### Plugin layout normalization
88
67
89
-
Skills and agents are semantically identical across targets -- `.github/skills/X` and `.claude/skills/X` contain the same content. When the lockfile records files under a different target prefix than the one you are packing for, APM automatically remaps `skills/` and `agents/` paths:
68
+
`apm pack` (default `--format plugin`) emits an Anthropic plugin directory regardless of which targets installed the source files. Skills and agents are semantically identical across targets, so APM normalizes paths into the plugin convention:
Only `skills/` and `agents/` are remapped. Commands, instructions, and hooks are target-specific and are never mapped.
98
-
99
-
The enriched lockfile inside the bundle uses the remapped paths, so the bundle is self-consistent. When mapping occurs, the `pack:` section includes a `mapped_from` field listing the original prefixes.
75
+
Commands, instructions, and hooks are also rehomed under the plugin's top-level convention dirs. The bundle is self-consistent and target-agnostic; the consumer's project drives where files land at install time.
100
76
101
77
### Targeting mental model
102
78
103
-
**Choose your target when you pack. Unpack delivers exactly what was packed.**
79
+
**Bundles are target-agnostic. The consumer's project decides where the files land.**
80
+
81
+
A bundle ships in Anthropic plugin layout (`agents/`, `skills/`, `commands/`, `instructions/`, `hooks/`) as a transport convention -- not a target binding. When a consumer runs `apm install <bundle>`, APM resolves the consumer's target from their project context (same precedence as registry installs: `--target` flag, then `apm.yml`, then directory detection) and routes the bundle's primitives through the integrators for that target.
104
82
105
-
A bundle is a deployable snapshot, not a retargetable source artifact. Target selection happens at pack time because that is when the full context is available -- which file types are remappable (skills, agents) and which are target-specific (commands, instructions, hooks).
83
+
Concretely: the same `team-skills.tgz` installed into a Copilot project lands under `.github/`; installed into a Claude project, lands under `.claude/`; installed into an OpenCode project, lands under `.opencode/` with instructions staged for `apm compile`.
106
84
107
-
`apm unpack` does not remap paths. If the bundle was packed for Claude, the files land under `.claude/`. If you need a different target, re-pack from source with the desired `--target`flag, or use `--target all` to include all platforms.
85
+
`--target` on `apm pack` is **deprecated**. The field is informational and never overrides consumer-side target resolution; an advisory warning may still print at install time if the bundle's recorded `pack.target`differs from the resolved install target.
108
86
109
-
When unpacking, APM reads the bundle's `pack:` metadata and shows the target it was packed for. If the bundle target does not match the project's detected target, a warning is displayed:
87
+
Compile-only targets (OpenCode, Codex, Gemini) receive instructions under `apm_modules/<slug>/.apm/instructions/` so [`apm compile`](../../guides/compilation/) merges them into `AGENTS.md` / `GEMINI.md` on the next compile.
110
88
111
89
```
112
-
$ apm unpack team-skills.tar.gz
113
-
[*] Unpacking team-skills.tar.gz -> .
114
-
[i] Bundle target: claude (1 dep(s), 3 file(s))
115
-
[!] Bundle target 'claude' differs from project target 'copilot'
116
-
[+] Unpacked 3 file(s) (verified)
90
+
$ apm install team-skills.tgz
91
+
[>] Installing local bundle from team-skills.tgz
92
+
[*] Installed 3 file(s) from local bundle
93
+
[!] Bundle staged 1 instruction(s) for compile (target: opencode). Run 'apm compile' to merge them into AGENTS.md / GEMINI.md / equivalent.
117
94
```
118
95
119
-
This is informational -- the files still extract. The warning helps users understand why their tool may not see the unpacked files and suggests the correct workflow.
120
-
121
96
## Plugin format vs APM format
122
97
123
98
`apm pack` produces one of two output shapes. The default is the plugin format.
@@ -126,7 +101,7 @@ This is informational -- the files still extract. The warning helps users unders
126
101
|---|---|---|
127
102
| Output layout | Claude Code plugin directory with `plugin.json` at the root and convention dirs (`agents/`, `skills/`, `commands/`, `instructions/`, `hooks/`) | Mirrors `apm install` deploy paths (`.github/`, `.claude/`, `.cursor/`, `.opencode/`) plus an enriched `apm.lock.yaml`|
128
103
|`plugin.json`| Synthesized (or updated from existing) and validates against the [official Claude Code plugin manifest schema](https://json.schemastore.org/claude-code-plugin.json)| Not emitted |
129
-
|`apm.lock.yaml` inside output |Not emitted (no APM-specific files) | Enriched copy with a `pack:` metadata section |
104
+
|`apm.lock.yaml` inside output |Enriched copy with a `pack:` metadata section (when the project has a lockfile) | Enriched copy with a `pack:` metadata section |
130
105
| Drop-in for | Any Claude Code plugin consumer (Copilot CLI, Claude Code, Cursor, ...) |`microsoft/apm-action`'s restore mode and bundle-aware tooling |
131
106
|`devDependencies`| Excluded | Included (full install layout) |
132
107
@@ -279,15 +254,17 @@ build/my-project-1.0.0/
279
254
280
255
The bundle is self-describing: its `apm.lock.yaml` lists every file it contains and the dependency graph that produced them.
281
256
282
-
## Lockfile enrichment (APM format only)
257
+
## Lockfile enrichment
283
258
284
-
When `--format apm` is used, the bundle includes a copy of `apm.lock.yaml`enriched with a `pack:` section. The project's own `apm.lock.yaml` is never modified.
259
+
Both formats embed an enriched `apm.lock.yaml`in the bundle when the project has a lockfile. The project's own `apm.lock.yaml` is never modified; the embedded copy carries an additional `pack:` section so consumers verify integrity at install time without re-running the upstream pack.
The `pack:` section records the bundle `format`, the effective `target` filter, and a `packed_at` UTC timestamp. Plugin-format output has no `apm.lock.yaml` -- consumers verify by re-running the upstream pack instead.
284
+
The `pack:` section records the bundle `format`, the per-file `bundle_files` SHA-256 manifest, and a `packed_at` UTC timestamp.
308
285
309
286
## `apm unpack`
310
287
288
+
:::note
289
+
For APM consumers, prefer `apm install <bundle>` over `apm unpack`. `apm install` deploys both formats target-agnostically, persists provenance to the project lockfile (`local_deployed_files`), and works with directory or `.tar.gz` inputs. `apm unpack` is retained for the legacy APM-format restore-without-APM workflow consumed by `microsoft/apm-action@v1`.
290
+
:::
291
+
311
292
Extracts an APM bundle (produced with `--format apm`) into a project directory. Accepts both `.tar.gz` archives and unpacked bundle directories. Plugin-format output is consumed directly by Claude Code and other plugin hosts and does not need `apm unpack`.
312
293
313
294
```bash
@@ -462,4 +443,4 @@ During unpack, verification found files listed in the bundle's lockfile that are
462
443
If `apm pack` produces zero files, check:
463
444
464
445
1. Your dependencies have `deployed_files` entries in `apm.lock.yaml`. This can happen if `apm install` completed but no integration files were deployed (e.g., the package has no prompts or agents for the active target).
465
-
2. The `--target` filter matches where files were deployed. For example, if files are under `.github/` but you pack with `--target claude`, APM will remap `skills/` and `agents/` automatically. If no remappable files exist, the bundle will be empty. Try `--target all` or check `apm.lock.yaml` to see which prefixes your files use.
446
+
2. The bundle is built from the `deployed_files` in `apm.lock.yaml` directly. Cross-target remapping for the convention dirs (`skills/`, `agents/`, `commands/`, `instructions/`, `hooks/`) runs automatically. If `apm.lock.yaml` shows zero deployed files, run `apm install` first; if files exist there but the bundle is empty, file an issue.
Copy file name to clipboardExpand all lines: docs/src/content/docs/guides/plugins.md
+2-18Lines changed: 2 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,23 +8,7 @@ APM treats plugins and packages as the same artifact. Every APM package is plugi
8
8
9
9
## Plugin authoring
10
10
11
-
The only authoring decision is whether you also keep an `apm.yml`:
12
-
13
-
-**With `apm.yml` (recommended).** You get dependency management, lockfile pinning, [security scanning](../../enterprise/security/), [`devDependencies`](../../reference/manifest-schema/#5-devdependencies), and multi-runtime deploy during development. `apm pack` still emits a plugin-compatible bundle for non-APM consumers. This is what `apm init --plugin` produces.
14
-
-**From an existing `plugin.json`.** APM consumes it natively - `apm install owner/repo` works against any plugin repo without migration. Metadata is synthesized from `plugin.json`. Add an `apm.yml` later if you want APM tooling during development.
15
-
16
-
For why `apm.yml` adds value on top of `plugin.json`, see [Anatomy -- Why not just ship a `plugin.json`?](../../introduction/anatomy-of-an-apm-package/#why-not-just-ship-a-pluginjson).
apm install --dev owner/helpers # dev-only dependency (excluded from pack)
23
-
apm install owner/core-rules # production dependency
24
-
apm pack # plugin-compatible bundle by default
25
-
```
26
-
27
-
The packed directory contains no APM-specific files. See [Pack & Distribute -- Plugin format](../../guides/pack-distribute/#plugin-format-vs-apm-format) for the output mapping and [Without APM: what you give up](../../guides/pack-distribute/#without-apm-what-you-give-up) for the consumer-side trade-off.
11
+
For the authoring flow (`apm init --plugin`, dev/prod dependency split, `apm pack` output mapping, hybrid mode with `apm.yml` + `plugin.json`), see [Pack & Distribute](../../guides/pack-distribute/) and the [first package walkthrough](../../getting-started/first-package/#6-ship-as-a-plugin-optional). For why APM still adds value when you already have a `plugin.json`, see [Anatomy -- Why not just ship a `plugin.json`?](../../introduction/anatomy-of-an-apm-package/#why-not-just-ship-a-pluginjson).
28
12
29
13
## Overview
30
14
@@ -353,7 +337,7 @@ This:
353
337
354
338
## Exporting APM packages as plugins
355
339
356
-
Use the [authoring workflow](#authoring-workflow) to develop plugins with APM's full tooling and export them as standalone plugin directories. See [Pack & Distribute -- Plugin format](../../guides/pack-distribute/#plugin-format-vs-apm-format) for the output mapping and structure.
340
+
Use the [authoring workflow](#plugin-authoring) to develop plugins with APM's full tooling and export them as standalone plugin directories. See [Pack & Distribute -- Plugin format](../../guides/pack-distribute/#plugin-format-vs-apm-format) for the output mapping and structure.
0 commit comments