Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
044c567
fix: make Claude project LSPs discoverable
danielmeppiel Aug 30, 2026
da651e4
fix: harden Claude LSP plugin lifecycle
danielmeppiel Sep 1, 2026
44ad99f
fix: preserve LSP cleanup diagnostics
danielmeppiel Sep 1, 2026
c5d18d8
fix: preserve LSP trust across lifecycle paths
danielmeppiel Sep 1, 2026
03fb620
fix: close final Claude LSP trust gaps
danielmeppiel Sep 1, 2026
57cc888
test: guard local-bundle LSP force forwarding
danielmeppiel Sep 1, 2026
19951ef
fix: finish LSP trust convergence
danielmeppiel Sep 1, 2026
f5d5e3d
fix: keep install command within architecture budget
danielmeppiel Sep 1, 2026
89ce147
fix: reconcile LSP lifecycle ownership
danielmeppiel Sep 1, 2026
f60c4f6
perf: index local approval ancestry
danielmeppiel Sep 1, 2026
2e8a357
docs: align LSP trust diagnostics
danielmeppiel Sep 1, 2026
02e95b0
fix: skip empty LSP reconciliation
danielmeppiel Sep 1, 2026
90f074a
fix: finish LSP lifecycle convergence
danielmeppiel Sep 1, 2026
29c694f
fix: keep drift replay within module budget
danielmeppiel Sep 1, 2026
961b189
fix: close final LSP review gaps
danielmeppiel Sep 3, 2026
1797fca
chore: keep install command within lint budget
danielmeppiel Sep 3, 2026
46789a9
fix: harden local LSP bundle integration
danielmeppiel Sep 3, 2026
f3b72c2
fix: close final bundle trust gaps
danielmeppiel Sep 3, 2026
91ee0b2
fix: close final LSP trust review gaps
danielmeppiel Sep 3, 2026
e1746ac
fix: finish LSP ownership follow-ups
danielmeppiel Sep 3, 2026
c5244c5
fix: reject symlinked bundle LSP metadata
danielmeppiel Sep 3, 2026
2d093dd
docs: finish local bundle trust guidance
danielmeppiel Sep 3, 2026
2be4b49
fix: close local bundle LSP review gaps
danielmeppiel Sep 3, 2026
4d17279
fix: honor source policy for bundle roots
danielmeppiel Sep 3, 2026
757f5f6
fix: preflight bundle LSP target support
danielmeppiel Sep 3, 2026
320ed94
fix: resolve post-rebase validation
danielmeppiel Sep 3, 2026
4c2578a
fix: keep CLI files under lint budget
danielmeppiel Sep 3, 2026
ded6c4d
fix: use Claude personal skills plugin for LSP
danielmeppiel Sep 3, 2026
d291687
test: cover Claude LSP project cleanup lifecycle
danielmeppiel Sep 3, 2026
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
21 changes: 21 additions & 0 deletions .apm/architecture/owners/install-deployment.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@
"selectors": ["src/apm_cli/integration/base_integrator.py"],
"guards": ["install-deployment-base-integrator"]
},
{
"id": "executable-trust-context",
"decision": "Executable trust context and per-package approval",
"owner": "security/executables.py (exec_trust_context_for_project, resolve_exec_decision)",
"selectors": ["src/apm_cli/security/executables.py"],
"guards": ["install-deployment-executable-trust-context"]
},
{
"id": "lsp-target-contract",
"decision": "LSP target path, shape, and reserved Claude namespace",
"owner": "integration/lsp_integrator.py (LSPIntegrator)",
"selectors": ["src/apm_cli/integration/lsp_integrator.py"],
"guards": ["install-deployment-lsp-target-contract"]
},
{
"id": "lsp-lifecycle",
"decision": "LSP lifecycle orchestration and reconciliation",
"owner": "install/lsp/integration.py",
"selectors": ["src/apm_cli/install/lsp/integration.py"],
"guards": ["install-deployment-lsp-lifecycle"]
},
{
"id": "post-uninstall-dependency-reachability",
"decision": "Post-uninstall dependency reachability",
Expand Down
93 changes: 93 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `apm uninstall` now removes MCP servers only from recorded owning runtimes,
accepts JetBrains Copilot JSONC, and reports target cleanup failures after
attempting every owner. (by @aryansk, fixes #2551) (#2591)
- Claude project LSP servers now load from a discoverable APM-managed plugin
manifest instead of a path Claude Code ignored. Upgrade users can rerun
`apm install --target claude`; legacy
project-root `.lsp.json` files remain unchanged for manual review, and target
changes, package uninstall, and later executable denial remove only LSP
entries that APM created. (#2733)
- `apm install` now preserves previously deployed skills when package
integration is skipped instead of treating them as stale cleanup candidates.
(#2758)
Expand All @@ -47,6 +53,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
lockfile drift in projects that also carry MCP state. APM
trusts the locked type before remote materialization and validates the skill
shape when present, for repository-root and subdirectory skills. (#2446)
- OpenCode MCP generation now preserves safe passthrough fields while preventing
custom fields from injecting the modeled `environment` alias. (by @aryansk,
fixes #2510) (#2593)
- Hook commands such as `"${CLAUDE_PLUGIN_ROOT}"/hooks/probe.py` now rewrite to
`"${CLAUDE_PLUGIN_ROOT}/hooks/probe.py"` and warn when a supported plugin-root
placeholder remains unresolved instead of silently deploying a dead hook.
OpenAPM v0.1 (`docs/src/content/docs/specs/openapm-v0.1.md#req-tg-012`) binds
the behavior.
(by @MohammedAlkindi; closes #2639) (#2645)
- `apm uninstall --global` now cleans removed-only target files before deleting their ownership state, while preserving files owned by surviving packages. (#2658)
- Generic marketplace Git now prevents platform tokens from reaching native
credential-helper subprocesses while preserving HTTPS helper access; HTTP and
HTTPS-to-HTTP rewrites suppress credentials, and SSH is token-free and
noninteractive. (by @aryansk, #2594)
- Windows binary is now Authenticode-signed in the release workflow, eliminating
the `Trojan:Script/Wacatac.H!ml` Windows Defender false positive on unsigned
PyInstaller bundles. (#2435)
- Multi-target `apm compile` now avoids repeating expensive project analysis
for each target, making multi-target runs scale like single-target runs
without changing generated output. (closes #2482)
- `deployed-files-present` no longer false-positives on gitignored deploy
paths (e.g. `.agents/`), enabling `apm audit --ci` to pass on a fresh
checkout when deployed outputs are intentionally not committed. (closes
#2452, thanks @sergio-sisternes-epam)
- YAML expansion guard no longer rejects large anchor-free lockfiles (150K+
entries) with a false-positive "billion-laughs" error. APM-generated
lockfiles with no anchors or aliases now load without error. (#2389)
- `apm install` no longer skips the credential retry on non-English machines.
Git localises its diagnostics through gettext, so a translated stderr made an
authentication failure unrecognisable and private-repo installs failed with
misleading network guidance. Git subprocesses in the authentication retry
path now run with `LC_ALL=C` and `LANGUAGE=C`. (by @Naofel-eal, closes #2533)
- Claude project LSP servers now load from a discoverable APM-managed plugin
manifest instead of a path Claude Code ignored. Upgrade users can rerun
`apm install --target claude`; legacy
project-root `.lsp.json` files remain unchanged for manual review, and target
changes, package uninstall, and executable denial revoke only recorded
APM-owned entries. (#2733)
- `apm pack` now reports unavailable remote package metadata, exposes
certifiability in JSON, prevents `--check-clean` from certifying degraded
regeneration, and lets `--strict-metadata` fail before writes. (closes #2524)
- `apm pack --check-clean` is now read-only and detects marketplace drift
without overwriting artifacts. Release pipelines that also produce artifacts
must run `apm pack` separately; see
[Releasing from any CI](docs/src/content/docs/producer/releasing-from-any-ci.md#the-canonical-sequence).
(by @danielmeppiel, closes #2727, #2730)
- `apm update` now retains full-SHA pins without an eligible stable annotated
semver tag while continuing unrelated updates; malformed remote tag records
still fail before writes. The contract is recorded in `openapm-v0.1.md`.
(#2667)
- `apm install -g --mcp NAME` now creates or updates the user manifest and
deploys only to global-capable runtimes instead of rejecting `--global`.
Registry identities are validated before user-state writes, fail closed when
Expand All @@ -68,6 +124,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
must run `apm pack` separately; see
[Releasing from any CI](docs/src/content/docs/producer/releasing-from-any-ci.md#the-canonical-sequence).
(by @danielmeppiel, closes #2727, #2730)
- Claude project LSP installs now write a discoverable APM-managed plugin
manifest. Upgrade users can rerun `apm install --target claude`; legacy
project-root `.lsp.json` files remain unchanged for manual review, and target
changes, package uninstall, and executable denial revoke only recorded
APM-owned entries. (#2733)
- Distributed `apm compile` now reconciles existing managed-section
`AGENTS.md` files without overwriting hand-authored content, generates new
placements safely, and never discovers, writes, or cleans content across
nested Git repository or linked-worktree boundaries. (by @aryansk; fixes
#2560 and #2713) (#2578)
- Windows admin lifecycle policies now resolve from `%ProgramData%` instead of
assuming `C:\ProgramData`, while retaining the historical fallback.
(by @lukiod; closes #2684) (#2686)
- Claude project LSP installs now write a discoverable APM-managed plugin
manifest. Upgrade users can rerun `apm install --target claude`; legacy
project-root `.lsp.json` files remain unchanged for manual review. (#2733)
- Marketplace installs now materialize catalog-only LSP and MCP metadata
without requiring a package manifest in the downloaded source
(by @lkshrk, #2709).
- Private `github.com` subdirectory packages now populate the persistent Git
- Private `github.com` packages now populate the persistent Git
cache through repository-scoped credential fallback without storing
credentials in cache keys or remote URLs. (#2722)
- Plugin refreshes now keep the existing package and its registered hooks live
while replacement content downloads and validates. Failed refreshes retain
the prior package instead of accepting stale content. (#2723)
- Successful installs now remove inactive resolution staging directories left
by interrupted earlier runs while preserving active and unrelated entries.
(closes #2716)
- Successful installs now safely clean up temporary backups left by interrupted
lock-aware runs without disturbing active installs or unrelated files. Legacy
lockless backups are preserved with manual recovery guidance. (#2720)
- `apm doctor` now reports malformed project `executables` configuration as an
actionable informational warning instead of omitting the check. (#2719)
- `apm doctor` now reports malformed project `executables` or deprecated
`allowExecutables` configuration as an actionable informational warning
instead of omitting the check. (#2719)
- `apm doctor` now reports malformed project executable-trust configuration
under either `executables` or the deprecated `allowExecutables` key as an
actionable informational warning instead of omitting the check. (#2719)
Expand Down
20 changes: 12 additions & 8 deletions docs/src/content/docs/consumer/deploy-a-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ with a warning unless policy requires hashes. With
## How the install works

:::note[Governed offline installs]
When the policy cache is warm, offline bundles still apply target, MCP, and
integrity rules without network access.
When the policy cache is warm, offline bundles still apply target, MCP, LSP,
canvas, and integrity rules without network access.
:::

```
Expand All @@ -70,20 +70,24 @@ Steps APM runs:
1. **Detect.** Path exists and contains `plugin.json` at the bundle root
(zip archives and legacy tarballs are extracted to a temp directory first).
2. **Apply cached policy.** Resolve the actual targets, then apply any cached
org policy to bundle MCP entries and targets before files are written.
Bundle install never fetches policy from the network. `--no-policy` skips
this local gate for one invocation.
org policy to bundle MCP, LSP, canvas, and target rules before files are
written. Bundle install never fetches policy from the network. `--no-policy`
skips this local gate for one invocation.
3. **Verify integrity.** Hash every file listed in `pack.bundle_files`;
reject any symlink, hash mismatch, or unlisted file.
4. **Deploy.** Map `agents/`, `skills/`, `commands/`, `hooks/` into the
harness layout for each `--target` you passed. For plugin skill declaration
precedence, see [Package Types](../../reference/package-types/#plugin-collection-pluginjson).
4. **Deploy.** Map `agents/`, `skills/`, `commands/`, `hooks/`, LSP metadata,
and canvas extensions into the harness layout for each `--target` you
passed. For plugin skill declaration precedence, see
[Package Types](../../reference/package-types/#plugin-collection-pluginjson).
5. **Record.** Write a lockfile entry under the project's `apm.lock.yaml`
so [drift detection](../drift-and-secure-by-default/) can audit the
deployed files later.

`apm.yml` is never touched. Re-running the same command re-deploys (use
`--force` to overwrite locally-edited files).
If executable trust blocks bundle MCP, LSP, or canvas content, copy the exact
`name#version@sha256:<digest>` key printed by `apm install` into
`executables.allow`; changed bundle bytes require a new key.
For centrally governed installs, warm the policy cache before distributing an
offline bundle. See [APM policy](../../enterprise/apm-policy/).

Expand Down
95 changes: 67 additions & 28 deletions docs/src/content/docs/consumer/install-lsp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ for runtime-specific config details.

Declare an LSP server in `apm.yml` and run `apm install`:

For this root-manifest example, install `gopls` separately and make sure it is
available on `PATH`; APM configures the runtime but does not install external
language-server executables.

```yaml
dependencies:
lsp:
Expand All @@ -34,13 +38,35 @@ dependencies:
```

```bash
apm install
apm install --target claude
```

APM writes runtime-specific config for each detected target. Claude Code
uses `.lsp.json` or `~/.claude.json`; Copilot CLI uses `.github/lsp.json`
or `~/.copilot/lsp-config.json`. The runtime starts the configured
language servers automatically.
APM writes runtime-specific config for each detected target. At project scope,
Claude Code discovers LSP servers from the APM-managed plugin manifest at
`.claude/skills/apm-lsp/.claude-plugin/plugin.json`; user-scope installs use
`~/.claude/skills/apm-lsp/.claude-plugin/plugin.json`. Copilot CLI uses
`.github/lsp.json` or `~/.copilot/lsp-config.json`. This generated shape
matches each runtime's documented discovery contract.

Claude skills-directory plugin discovery requires Claude Code v2.1.157 or
newer. For project installs, accept Claude Code's workspace-trust prompt; LSP
servers start only after you trust the workspace. Start Claude from the
repository root so its primary working directory contains `.claude/skills/`:
project-scope skills-directory plugins do not walk up from a subdirectory to
the repo root. Personal-scope plugins under your home directory have no
workspace-trust gate. After APM reports that it configured or removed Claude
LSP servers, restart Claude Code or run `/reload-plugins` (use
`/reload-plugins --force` when Claude requests it). Open a file matching a
configured extension and confirm its LSP-backed diagnostics or navigation work
before relying on the integration. If another enabled Claude LSP server already
claims the same file extension, Claude uses the first registered server for
that extension and the others never start; for example, an APM-declared `.py`
server can lose to an installed `pyright-lsp`.

If an earlier APM version created a project-root `.lsp.json`, APM leaves it
unchanged because it may contain user-owned entries. Claude Code does not use
that file for project plugin discovery. Review it, migrate any entries you
still need, then remove it.

## The `lsp:` section in apm.yml

Expand Down Expand Up @@ -74,18 +100,21 @@ The full field reference is in the

| Runtime | Project file | User file (`-g`) | Language map key |
|---|---|---|---|
| Claude Code | `.lsp.json` | `~/.claude.json` `lspServers` | `extensionToLanguage` |
| Claude Code | `.claude/skills/apm-lsp/.claude-plugin/plugin.json` `lspServers` | `~/.claude/skills/apm-lsp/.claude-plugin/plugin.json` `lspServers` | `extensionToLanguage` |
| GitHub Copilot CLI | `.github/lsp.json` `lspServers` | `~/.copilot/lsp-config.json` `lspServers` | `fileExtensions` |

**Claude Code project-scope `.lsp.json` example:**
**Claude Code project-scope plugin manifest example:**

```json
{
"gopls": {
"command": "gopls",
"args": ["serve"],
"extensionToLanguage": {
".go": "go"
"name": "apm-lsp",
"lspServers": {
"gopls": {
"command": "gopls",
"args": ["serve"],
"extensionToLanguage": {
".go": "go"
}
}
}
}
Expand All @@ -108,15 +137,16 @@ The full field reference is in the
```

User-scope files keep the same runtime-specific server shape under their
`lspServers` section.
`lspServers` section. Claude skills-directory plugins are auto-discovered, so
APM does not write an `enabledPlugins` entry.

## Required and optional fields

Two fields are required for every LSP server definition (object form):

| Field | Type | Description |
|---|---|---|
| `command` | `string` | Binary to execute. Must be on `$PATH` or a relative path. |
| `command` | `string` | Binary to execute. Must resolve from `$PATH` or use an absolute or relative path. |
| `extensionToLanguage` | `map<string, string>` | Maps file extensions to LSP language identifiers (e.g. `".go": "go"`). |

Optional fields give you finer control:
Expand All @@ -142,33 +172,42 @@ entries, APM collects them transitively after installation. Direct
transitive package both declare a server with the same name, the
root definition wins.

Unlike MCP, LSP has no registry vs self-defined distinction. All
LSP servers from installed packages are treated as trusted.
Unlike MCP, LSP has no registry vs self-defined distinction. LSP commands from
dependency packages pass the executable trust gate for their declaring package
when the gate is enabled. Approve the package with `apm approve <package>`
before APM exposes its server to a supported runtime. Without a project or org
`executables` opt-in, the compatibility default permits dependency
executables. Root-project LSP declarations are trusted as local project
content.

## Stale server cleanup

When a previously installed LSP server is no longer declared by
any dependency, APM removes it from the target runtime configs it manages.
The lockfile tracks which servers APM manages, so hand-added servers are
never touched.
never touched. When cleanup removes the last managed server from an otherwise
empty APM-owned Claude project plugin, APM deletes the plugin and its empty
`apm-lsp` directory.

## Lockfile

`apm install` persists two fields in `apm.lock.yaml`:

- `lsp_servers` -- sorted list of APM-managed server names.
- `lsp_configs` -- server-name-to-config baseline for drift detection.

See the [Lockfile specification](../../reference/lockfile-spec/).
`apm install` records resolved LSP configuration, declaration ownership, and
target ownership in `apm.lock.yaml`. These fields let lifecycle commands
reconcile only APM-owned entries. See the
[Lockfile specification](../../reference/lockfile-spec/) for the canonical
field definitions.

## Plugin extraction

When APM installs a plugin that contains `lspServers` in `plugin.json`
or a `.lsp.json` file, the LSP servers are automatically extracted and
wired into the install pipeline. Plugin `.lsp.json` files may use either
a flat server map or a `{ "lspServers": { ... } }` envelope. The
When APM installs a plugin, it extracts LSP servers from an inline or
file-valued `lspServers` entry in `plugin.json`, or auto-discovers
`com.microsoft.apm/lsp.json`, `lsp.json`, or `.lsp.json`. The servers are
then wired into the install pipeline. Plugin LSP files may use either a flat
server map or a `{ "lspServers": { ... } }` envelope. The
`${CLAUDE_PLUGIN_ROOT}` placeholder in server configs is replaced with
the absolute plugin path for legacy Claude Code plugin compatibility.
These are source files shipped by a dependency package, distinct from the
`.claude-plugin/plugin.json` that APM generates for Claude discovery.
Plugins authored for Copilot CLI may use `fileExtensions` instead of
`extensionToLanguage` and `warmupTimeoutMs` instead of `startupTimeout`;
APM normalizes those aliases before validation. A non-null canonical value
Expand All @@ -190,7 +229,7 @@ success.

| Runtime | LSP support |
|---|---|
| Claude Code | `.lsp.json` / `~/.claude.json` |
| Claude Code | `.claude/skills/apm-lsp/.claude-plugin/plugin.json` / `~/.claude/skills/apm-lsp/.claude-plugin/plugin.json` |
| GitHub Copilot CLI | `.github/lsp.json` / `~/.copilot/lsp-config.json` |
| Others | Not yet supported |

Expand Down
Loading