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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
or end with `.` -- can host an APM governance policy repo for the first
time. (by @sergio-sisternes-epam; closes #1813) (#1830)

### Removed

- **Breaking (security):** executable dependencies -- including MCP servers and
canvas extensions -- now require explicit, persistent approval via `apm approve`,
closing the gap where canvas extensions were trusted per-run. The
`--trust-canvas-extensions` flag is removed as a consequence; canvas extensions
are now governed by the `allowExecutables` gate like every other executable
surface. Add an `allowExecutables: {}` block to `apm.yml` and run
`apm approve <pkg>` to trust them. (by @sergio-sisternes-epam) (#1865)

```diff
- apm install --trust-canvas-extensions # before: per-run trust flag
+ apm approve <pkg> # after: one-time, user-local approval
```

CI / non-interactive pipelines that previously passed the flag should
instead pre-seed approvals before `apm install`, e.g.
`apm approve <pkg>` (writes `~/.apm/approvals.yml` directly, no prompt),
so the gate finds the package already trusted and never prompts.

### Fixed

- `apm install <pkg>@<marketplace>` now preserves GitLab and other
Expand All @@ -36,6 +56,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Codex MCP config writes, falling back to `~/.codex/config.toml` when unset.
(closes #1861) (#1863)

### Security

- The `allowExecutables` default-deny gate now enforces `mcp` server writes and
`canvas` extensions in addition to `hooks` and `bin`, bringing all four executable
surfaces under one approval model. `apm approve` decisions are also stored
user-local in `~/.apm/approvals.yml` instead of the committed project `apm.yml`,
so cloning a repository no longer silently inherits another developer's executable
approvals. (by @sergio-sisternes-epam) (#1865)

## [0.21.0] - 2026-06-19

### Added
Expand Down
26 changes: 24 additions & 2 deletions CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,28 @@
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_should_refuse_credential_on_non_https_git_over_http"
]
},
{
"conformance_class": "consumer",
"id": "req-sc-009",
"keyword": "MUST",
"section": "10.13",
"status": "active",
"test_count": 1,
"tests": [
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_must_deny_executable_primitive_without_allowexecutables_approval"
]
},
{
"conformance_class": "consumer",
"id": "req-sc-010",
"keyword": "MUST",
"section": "10.13",
"status": "active",
"test_count": 1,
"tests": [
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_must_persist_approvals_user_locally_not_in_project_manifest"
]
},
{
"conformance_class": "consumer",
"id": "req-tg-001",
Expand Down Expand Up @@ -1024,7 +1046,7 @@
"spec_version": "v0.1.1",
"summary_by_class": {
"consumer": {
"active": 61,
"active": 63,
"skipped": 1,
"unbound": 0,
"xfail": 0
Expand All @@ -1048,5 +1070,5 @@
"xfail": 0
}
},
"total_requirements": 90
"total_requirements": 92
}
4 changes: 3 additions & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
| Class | Active | Skipped | Xfail | Unbound |
|-------|-------:|--------:|------:|--------:|
| Producer | 12 | 0 | 0 | 0 |
| Consumer | 61 | 1 | 0 | 0 |
| Consumer | 63 | 1 | 0 | 0 |
| Registry | 1 | 0 | 0 | 0 |
| Governance | 15 | 0 | 0 | 0 |

Expand Down Expand Up @@ -113,6 +113,8 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
| [req-sc-006](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-006) | MUST | 4.2.3 | consumer | active | 1 |
| [req-sc-007](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-007) | MUST | 10.3 | consumer | active | 1 |
| [req-sc-008](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-008) | SHOULD | 10.3 | consumer | active | 1 |
| [req-sc-009](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-009) | MUST | 10.13 | consumer | active | 1 |
| [req-sc-010](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-010) | MUST | 10.13 | consumer | active | 1 |
| [req-tg-001](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-001) | MUST | 8.4 | consumer | active | 1 |
| [req-tg-002](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-002) | MUST | 8.5 | consumer | active | 1 |
| [req-tg-003](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-003) | MUST | 8.5 | consumer | active | 1 |
Expand Down
8 changes: 8 additions & 0 deletions docs/public/specs/manifests/openapm-v0.1.requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ requirements:
keyword: SHOULD
section: "10.3"
conformance_class: consumer
- id: req-sc-009
keyword: MUST
section: "10.13"
conformance_class: consumer
- id: req-sc-010
keyword: MUST
section: "10.13"
conformance_class: consumer
- id: req-rg-001
keyword: MUST
section: "11.3.3"
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/concepts/primitives-and-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Model Context Protocol servers declared as dependencies. APM writes the per-harn

### Canvas extensions (experimental)

GitHub Copilot CLI canvas extensions: a directory bundle whose entry file is `extension.mjs` (executable Node.js). Copilot-only. Behind the `canvas` experimental flag; dependency-provided canvases are blocked unless `--trust-canvas-extensions` is passed, because they are arbitrary executable code. Project scope deploys to `.github/extensions/`; `--global` deploys a dependency canvas to `~/.copilot/extensions/` (always requiring the trust flag).
GitHub Copilot CLI canvas extensions: a directory bundle whose entry file is `extension.mjs` (executable Node.js). Copilot-only. Behind the `canvas` experimental flag; dependency-provided canvases are blocked unless approved via `allowExecutables` in `apm.yml` and `apm approve <pkg>`, because they are arbitrary executable code. Project scope deploys to `.github/extensions/`; `--global` deploys a dependency canvas to `~/.copilot/extensions/` (always requiring approval).

- Source: `.apm/extensions/<name>/extension.mjs`
- Deploys to: `.github/extensions/<name>/` (project) or `~/.copilot/extensions/<name>/` (`--global`)
Expand Down Expand Up @@ -135,7 +135,7 @@ How to read a cell:
- `commands / copilot = unsupported` -- Copilot has no commands primitive; the same source `.prompt.md` reaches Copilot as a native prompt instead.
- `plugins / *` -- APM unpacks the plugin at install time into the primitives in the rows above; routing then follows those rows.
- `MCP servers / *` -- APM writes the harness's standard MCP config. Transitive MCP servers brought in by deep dependencies must be explicitly declared or trusted with `--trust-transitive-mcp` -- effectively `gated` for those, `native` for direct dependencies.
- `canvas / copilot = gated` -- requires the `canvas` experimental flag; a canvas shipped by a dependency is executable code, so it stays blocked until you pass `--trust-canvas-extensions`. First-party canvases in your own package deploy at project scope once the flag is on. With `--global`, a dependency canvas deploys to `~/.copilot/extensions/` and always requires the trust flag (first-party global install is not supported). Every other harness is `unsupported`: a canvas is a Copilot CLI construct only.
- `canvas / copilot = gated` -- requires the `canvas` experimental flag; a canvas shipped by a dependency is executable code, so it stays blocked until the package is approved via `allowExecutables` in `apm.yml` (`apm approve <pkg>`). First-party canvases in your own package deploy at project scope once the flag is on. With `--global`, a dependency canvas deploys to `~/.copilot/extensions/` and always requires approval (first-party global install is not supported). Every other harness is `unsupported`: a canvas is a Copilot CLI construct only.

## Where compiled context files land

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/enterprise/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ APM has no runtime footprint. Once `apm install` or `apm compile` completes, the

- **No runtime component.** APM generates files then terminates. It does not run alongside your application.
- **No network calls after install.** All network activity (git clone/fetch) occurs during dependency resolution. There are no callbacks, webhooks, or phone-home requests.
- **No arbitrary code execution.** APM does not execute scripts from packages, evaluate expressions in templates, or run downloaded code. (**Canvas exception:** the experimental `canvas` primitive deploys executable `extension.mjs` (Node.js) code to `.github/extensions/` or `~/.copilot/extensions/`; this surface is gated by both the `canvas` experimental flag and `--trust-canvas-extensions` for dependency-provided canvases. See [Canvas extensions](/apm/integrations/canvas/).)
- **No arbitrary code execution.** APM does not execute scripts from packages, evaluate expressions in templates, or run downloaded code. (**Canvas exception:** the experimental `canvas` primitive deploys executable `extension.mjs` (Node.js) code to `.github/extensions/` or `~/.copilot/extensions/`; this surface is gated by both the `canvas` experimental flag and, when the project opts in via `allowExecutables:` in `apm.yml`, the `allowExecutables` approval gate for dependency-provided canvases. See [Canvas extensions](/apm/integrations/canvas/).)
- **No access to application data.** APM never reads databases, API responses, application state, or user data.
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
- **No persistent background processes.** APM does not install daemons, services, or scheduled tasks.
- **No telemetry or data collection.** APM collects no usage data, analytics, or diagnostics. Nothing is transmitted to Microsoft or any third party.
Expand Down
53 changes: 34 additions & 19 deletions docs/src/content/docs/integrations/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,51 @@ is not picked up mid-session.

## Trust gate for dependency canvases

A canvas shipped by a **dependency** is arbitrary executable Node.js code. APM
blocks dependency-provided canvases by default. To deploy them, opt in
explicitly:
A canvas shipped by a **dependency** is arbitrary executable Node.js code. When
the project opts in to the executable gate (by adding `allowExecutables:` to
`apm.yml`), APM blocks dependency-provided canvases unless the package has been
explicitly approved. To deploy them:

```yaml
# apm.yml (committed -- opts the project in to the gate)
allowExecutables: {}
```
```bash
apm install --target copilot --trust-canvas-extensions
# Run once per developer; approval is stored in ~/.apm/approvals.yml (NOT committed)
apm approve some-org/canvas-package
apm install --target copilot
```

`apm approve` writes grants to `~/.apm/approvals.yml` -- a user-local file
that is never committed to source control. This means cloning a project with
`allowExecutables: {}` does **not** automatically grant trust to any package;
each developer must explicitly approve packages they want to deploy. For
automated CI pipelines, grants can alternatively be listed directly in
`apm.yml` (committed, shared with the team).

The trust gate is independent of the experimental flag:

- The **experimental flag** decides whether the canvas primitive is processed at
all. It is a feature-availability gate, not a security gate.
- The **`--trust-canvas-extensions` flag** decides whether *dependency*
canvases may deploy. Your own first-party canvas (in the root package you are
installing from) deploys freely once the flag is on; only dependency-provided
canvases need the trust flag.
- The **`allowExecutables` block** decides whether *dependency* canvases may
deploy. Your own first-party canvas (in the root package you are installing
from) deploys freely once the flag is on; only dependency-provided canvases
need approval.

When a dependency canvas is blocked, APM prints a diagnostic naming the package,
the canvas, the `extension.mjs` entry point, the deploy directory, and the
opt-in flag. The same gate is enforced on offline bundle install
(`apm install <bundle>`) and on `apm unpack`, so a vendored bundle cannot
smuggle an executable canvas past trust.
When a dependency canvas is blocked, APM prints a diagnostic naming the package
and the canvas, and instructs you to run `apm approve <pkg>`. The same gate is
enforced on offline bundle install (`apm install <bundle>`), so a vendored
bundle cannot smuggle an executable canvas past trust.

## Install globally (user scope)

To make a canvas available in **every** Copilot session, install it globally so
it lands in `~/.copilot/extensions/<name>/`:

```bash
apm install <package> --global --trust-canvas-extensions
apm approve <package>
apm install <package> --global
```

Global canvas install is intentionally limited in this experimental release:
Expand All @@ -109,8 +124,8 @@ Global canvas install is intentionally limited in this experimental release:
globally, so APM records it in the user lockfile and `apm uninstall --global`
can prune it. A first-party root `.apm/extensions/` canvas is **not** deployed
at user scope -- package it and install it as a dependency instead.
- **Trust is always required.** A global canvas has full-account blast radius,
so `--trust-canvas-extensions` is mandatory even though the project-scope
- **Approval is always required.** A global canvas has full-account blast radius,
so `allowExecutables` approval is mandatory even though the project-scope
first-party path does not need it.
- **Default `~/.copilot` only.** If `$COPILOT_HOME` is set to a non-default
location, APM refuses the global canvas install rather than deploy to a path
Expand All @@ -132,12 +147,12 @@ experimental flag, so a previously-installed canvas can always be removed.
(`--target claude`, `cursor`, etc.) never receive it.
- **Global install is dependency-only.** User-scope (`--global`) deployment to
`~/.copilot/extensions/` supports dependency-provided canvases (always
requiring `--trust-canvas-extensions`) and the default `~/.copilot` location
requiring `allowExecutables` approval) and the default `~/.copilot` location
only; first-party root canvases deploy at project scope only.
- **No compile/list surfacing yet.** Canvases are not yet shown by
`apm list`/`apm compile`; they are deployed at install only.
- **No policy-file control yet.** Canvas trust is controlled only by the
`--trust-canvas-extensions` CLI flag; governing it via `apm-policy.yml` is
- **No policy-file control yet.** Canvas trust is governed by `allowExecutables`
in `apm.yml`; a dedicated `apm-policy.yml` field for org-wide canvas policy is
planned but not part of this experimental release.

See the [primitives and targets](/apm/concepts/primitives-and-targets/) matrix
Expand Down
71 changes: 43 additions & 28 deletions docs/src/content/docs/reference/cli/approve.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,28 @@ sidebar:

```bash
apm approve [PACKAGE_REF...] [OPTIONS]
apm deny [PACKAGE_REF...] [OPTIONS]
apm deny [PACKAGE_REF...]
```

## Description

APM blocks executable primitives (hooks, bin/ executables) from
dependency packages by default. The `allowExecutables` block in
`apm.yml` records which packages have been explicitly approved to
deploy executables.
APM blocks executable primitives (hooks, bin/ executables, self-defined MCP
servers, and canvas extensions) from dependency packages by default. The
`allowExecutables` block in `apm.yml` opts the **project** in to the gate.
User-specific approvals are stored in **`~/.apm/approvals.yml`** -- a
personal file that is never committed to source control.

`apm approve` adds a package to the allowlist. `apm deny` removes it.
`apm approve` adds a package to the user-local allowlist. `apm deny` removes it.

### How the gate works

When `apm install` encounters a dependency that ships hooks or bin/
executables:
When `apm install` encounters a dependency that ships executable primitives:

1. If `allowExecutables` is **absent** from `apm.yml`, everything is
approved (backward-compatible, no gate).
2. If `allowExecutables` is **present** (even empty `{}`), only listed
packages may deploy executables.
2. If `allowExecutables` is **present** (even empty `{}`), only packages
approved in `~/.apm/approvals.yml` (or listed directly in `apm.yml`
for CI pipelines) may deploy executables.
3. In interactive mode, `apm install` prompts for each unapproved
package. In CI (non-interactive), unapproved executables cause a
hard error.
Expand All @@ -42,9 +43,21 @@ Local project content (the root `.apm/` directory) is always trusted.
|------|-------|-------|
| Hooks (`.apm/hooks/`, `hooks/`) | Yes | Auto-fire in IDE on lifecycle events |
| Bin executables (`bin/`) | Yes | Deployed to agent PATH via symlinks |
| MCP servers | No | Enforcement deferred to a future release |
| MCP servers (self-defined) | Yes | `registry: false` servers write to IDE MCP config |
| Canvas extensions (`.apm/extensions/`) | Yes | Deploys executable Node.js to IDE extensions |
| Text primitives (skills, agents, instructions) | No | No code execution risk |

### Where approvals are stored

| Store | Path | Who manages it | Committed? |
|-------|------|----------------|------------|
| User-local approvals | `~/.apm/approvals.yml` | `apm approve` / `apm deny` | No |
| Project gate + CI grants | `apm.yml` (`allowExecutables`) | Project maintainer / CI setup | Yes |

Approvals from both stores are merged at install time. The project `apm.yml`
signals that the gate is enabled and may include pre-approved packages for CI;
developer approvals live in the user file and are personal.

## Options

### `apm approve`
Expand All @@ -59,20 +72,20 @@ Local project content (the root `.apm/` directory) is always trusted.

| Flag | Description |
|------|-------------|
| `PACKAGE_REF` | One or more packages to deny (removes from allowlist). |
| `PACKAGE_REF` | One or more packages to deny (removes from user-local allowlist). |

## Manifest format
## User approvals file format

Approvals are stored in `apm.yml` under `allowExecutables`, keyed by
`name#version` with per-type boolean flags:
`apm approve` writes to `~/.apm/approvals.yml`. The file stores the approvals
mapping directly, keyed by `name#version` with per-type boolean flags:

```yaml
allowExecutables:
"ci-hooks@acme#1.2.0":
hooks: true
bin: true
"dev-tools@org#0.5.0":
hooks: true
# ~/.apm/approvals.yml (auto-generated, do not commit)
"ci-hooks@acme#1.2.0":
hooks: true
bin: true
"dev-tools@org#0.5.0":
hooks: true
```

Version pinning means approval must be renewed when a package updates.
Expand Down Expand Up @@ -107,14 +120,16 @@ apm deny ci-hooks@acme

In CI environments (`CI=true`, `APM_NON_INTERACTIVE=1`, or when stdin
is not a TTY), `apm install` fails with exit code 1 if any dependency
has unapproved executables. Pre-approve packages in `apm.yml` before
CI runs:
has unapproved executables. Pre-approve packages by listing them
directly in `apm.yml` (this is the only way to share approvals via
source control):

```bash
# One-time setup: approve all current dependencies
apm approve --all
git add apm.yml
git commit -m "Approve executable dependencies"
```yaml
# apm.yml
allowExecutables:
"ci-hooks@acme#1.2.0":
hooks: true
bin: true
```

## See also
Expand Down
1 change: 0 additions & 1 deletion docs/src/content/docs/reference/cli/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA
| `--audit <off\|warn\|block>` | (config/policy) | Run a content audit over the files this install deploys. `warn` records findings in the summary; `block` halts the install on critical findings. Overrides your `audit-on-install` config but cannot relax an org policy floor. Requires the `external-scanners` experimental flag. |
| `--no-audit` | off | Disable the install-time audit for this invocation (equivalent to `--audit off`). Cannot relax an org policy `block` floor. |
| `--trust-transitive-mcp` | off | Trust self-defined MCP servers shipped by transitive packages without re-declaring them in your `apm.yml`. |
| `--trust-canvas-extensions` | off | Trust executable canvas extensions (`extension.mjs`) shipped by dependencies. Required for dependency-provided canvases; first-party canvases deploy without it. Requires the `canvas` experimental flag. |
| `--allow-insecure` | off | Permit direct `http://` (non-TLS) dependencies. |
| `--allow-insecure-host HOSTNAME` | unset | Permit transitive `http://` dependencies from `HOSTNAME`. Repeatable. |

Expand Down
Loading
Loading