harden(executables): expand allowExecutables gate to mcp and canvas; store approvals user-local#1865
Conversation
- Add EXEC_TYPE_CANVAS; all four types (hooks, bin, mcp, canvas) are now in ENFORCED_EXEC_TYPES - Remove --trust-canvas-extensions / trust_canvas everywhere; canvas trust is now governed by allowExecutables + apm approve - Keep --trust-transitive-mcp (orthogonal concern) - exec_gate.check_executable_approval returns 4-tuple (hooks_ok, bin_ok, mcp_ok, canvas_ok) - Canvas gate in integrate_package_primitives uses is_first_party and is_package_approved independently (defence-in-depth: a dep named _local with is_first_party=False is still blocked) - MCP gate added in commands/install.py after collect_transitive - apm unpack (deprecated) canvas gate simplified to feature-flag-only - Update all tests; fix service.py (missed trust_canvas= kwarg) - Update docs: canvas.md, security.md, primitives-and-targets.md, install.md, unpack.md, experimental.md, commands.md, governance.md, package-authoring.md, apm-contributor-dashboard README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR expands the allowExecutables opt-in default-deny model beyond hooks and bin/ to also cover MCP server writes and Copilot canvas extensions, while removing --trust-canvas-extensions in favor of apm approve <pkg> + allowExecutables in apm.yml. It updates the install pipeline, bundle/unpack paths, validation diagnostics, tests, and documentation to reflect the unified approval model.
Changes:
- Adds
canvasas an executable type and promotesmcpto enforced executable gating (now four enforced types). - Removes the
trust_canvas/--trust-canvas-extensionsplumbing from CLI/options/context/pipeline and routes canvas trust throughallowExecutables. - Updates bundle install (
apm install <bundle>) andapm unpackbehavior plus associated docs/tests and diagnostics.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_unpacker.py | Updates unpacker tests for canvas gating now being feature-flag-only in apm unpack. |
| tests/unit/test_models_validation_rules.py | Adjusts validation-warning assertions to reference apm approve instead of the removed flag. |
| tests/unit/security/test_executables.py | Updates expectations now that MCP is enforced and exec_types/summary include MCP. |
| tests/unit/integration/test_canvas_integrator.py | Updates canvas integrator tests to the allowExecutables-based approval messaging and behavior. |
| tests/unit/install/test_install_local_bundle.py | Updates local-bundle install tests for new canvas approval/enforcement semantics. |
| tests/unit/install/test_architecture_invariants.py | Updates commands/install.py LOC budget and narrative to reflect new MCP gate and flag removal. |
| tests/unit/commands/test_install_phase3.py | Removes trust_canvas field expectations in install context construction. |
| tests/unit/commands/test_install_context.py | Removes trust_canvas from required fields and round-trip tests. |
| tests/unit/commands/test_install_context_and_resolution.py | Removes trust_canvas from context defaults and resolution tests. |
| src/apm_cli/security/executables.py | Adds EXEC_TYPE_CANVAS, enforces MCP+canvas, and scans .apm/extensions/*/extension.mjs. |
| src/apm_cli/models/validation.py | Updates canvas validation warning text to direct users to apm approve. |
| src/apm_cli/integration/canvas_integrator.py | Updates docs/diagnostics to allowExecutables/apm approve trust model. |
| src/apm_cli/install/services.py | Expands approval tuple, adds canvas skip logging, and enforces canvas approval for deps with defense-in-depth against _local spoofing. |
| src/apm_cli/install/service.py | Removes stale trust_canvas kwarg from service->pipeline call. |
| src/apm_cli/install/request.py | Removes trust_canvas field from InstallRequest. |
| src/apm_cli/install/pipeline.py | Removes trust_canvas param threading through the install pipeline. |
| src/apm_cli/install/local_bundle_handler.py | Threads allow_executables into local-bundle integration path. |
| src/apm_cli/install/exec_gate.py | Extends approval check to return (hooks, bin, mcp, canvas) and records blocked executables. |
| src/apm_cli/install/context.py | Removes trust_canvas from InstallContext. |
| src/apm_cli/core/experimental.py | Updates experimental-flag hint text to describe allowExecutables approval. |
| src/apm_cli/commands/pack.py | Removes --trust-canvas-extensions from apm unpack command surface and messaging. |
| src/apm_cli/commands/install.py | Removes --trust-canvas-extensions, adds allowExecutables read for bundle installs, and introduces MCP allowExecutables filtering. |
| src/apm_cli/bundle/unpacker.py | Drops trust flag from unpacker; canvas is stripped only when feature flag is off. |
| src/apm_cli/bundle/plugin_exporter.py | Updates comment to reflect approval via allowExecutables/apm approve. |
| packages/apm-guide/.apm/skills/apm-usage/package-authoring.md | Updates authoring docs to replace the removed flag with apm approve. |
| packages/apm-guide/.apm/skills/apm-usage/governance.md | Updates governance docs to reflect allowExecutables approval model for canvases. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Removes --trust-canvas-extensions from the apm install command reference row. |
| packages/apm-contributor-dashboard/README.md | Updates installation instructions to require apm approve before install. |
| docs/src/content/docs/reference/experimental.md | Updates canvas flag description to reference allowExecutables approval. |
| docs/src/content/docs/reference/cli/unpack.md | Removes --trust-canvas-extensions from unpack CLI docs. |
| docs/src/content/docs/reference/cli/install.md | Removes --trust-canvas-extensions from install CLI docs. |
| docs/src/content/docs/integrations/canvas.md | Updates trust-gate documentation to allowExecutables + apm approve. |
| docs/src/content/docs/enterprise/security.md | Updates security model mention of canvas trust gating. |
| docs/src/content/docs/concepts/primitives-and-targets.md | Updates primitive matrix/canvas notes to describe allowExecutables-based approval. |
…ovals.yml
Previously apm approve wrote grants to the project apm.yml, which gets
committed to source control. Anyone cloning the repo would inherit all
executable approvals automatically -- a security threat where a single
developer's approve decision silently grants trust to the whole team.
This commit separates the two concerns:
- apm.yml keeps the gate SIGNAL (allowExecutables: {}) to tell APM the
gate is enabled; specific package grants written there are still valid
for CI pipelines that need shared approvals.
- ~/.apm/approvals.yml is the new user-local store for interactive apm
approve decisions. The file is never committed (not a git artifact).
Changes:
- security/executables.py: add get_user_approvals_path(), load_user_approvals(),
save_user_approvals(), effective_allow_executables() helpers; load_user_approvals
reads the raw mapping directly (no allowExecutables wrapper).
- commands/approve.py: _load_allow_executables() reads from user file via
get_user_approvals_path(); _approve_all_pending / _approve_packages write via
save_user_approvals(); deny_cmd no longer requires project context.
- install/phases/integrate.py: interactive approval prompt seeds from
load_user_approvals() and persists via save_user_approvals() instead
of writing back to apm.yml.
- install/template.py: _effective_allow(ctx) merges project gate signal
with user approvals at wire-up time.
- commands/install.py: MCP gate uses effective_allow_executables() to
merge project + user approvals.
- tests/unit/commands/test_approve_deny.py: fully rewritten; all tests
patch get_user_approvals_path to a tmp_path file and assert grants
land in the user file, NOT in apm.yml.
- Docs: reference/cli/approve.md, integrations/canvas.md, governance.md,
commands.md updated to reflect the user-local store.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s-gate-mcp-canvas
…uard, doc accuracy
- install.py: use _mcp_dep.name (correct MCPDependency attr) so MCP gate
actually filters unapproved transitive MCP servers (was always no-op via
non-existent source_package/package_id attrs)
- install.py: fail-closed on allowExecutables parse error in bundle-install
path; was silently passing None (no enforcement) on any exception; now
logs a warning and defaults to {} (gate enabled, nothing approved)
- services.py: wrap extensions_root.iterdir() in _log_canvas_skip() with
try/except OSError to prevent an unreadable extensions dir from aborting
the entire install
- canvas.md, security.md, governance.md: clarify enforcement is opt-in
(only when allowExecutables block is present in apm.yml); was misleadingly
stated as 'blocked by default'
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….py below CI 2100-line limit - Move 32-line MCP allowExecutables filter block into filter_mcp_by_allow_executables() in security/executables.py - Move 20-line bundle allowExecutables read block into read_bundle_allow_executables() in security/executables.py - install.py: 2139 -> 2095 lines (CI limit: 2100) - No behaviour change; existing tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add new Section 10.13 'Executable primitive approval gate' to the OpenAPM v0.1 spec with two consumer MUSTs: - req-sc-009: consumer MUST fail-closed on allowExecutables gate -- any hook, bin, MCP server, or canvas extension from a dependency not in the effective approval set MUST NOT be deployed. - req-sc-010: consumer MUST persist interactive approval decisions user-locally (not in project apm.yml) so one developer's approval cannot propagate via VCS to teammates. Updates: - Section 10.11 summary table: rows 11 and 12 - Section 11.3.2 Consumer enumeration: req-sc-009 and req-sc-010 - Appendix C index: two new rows - Section 1.3 + Appendix C trailer: 90 -> 92 normative statements - Revision history: v0.1.5 entry - Manifest: openapm-v0.1.requirements.yml (req-sc-009, req-sc-010) - Conformance tests: two new @pytest.mark.req tests, both passing This satisfies the Mode B spec-conformance gate requirement that changes to critical paths (install/, integration/) carry spec citations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Auto-generated by 'uv run --extra dev python -m tests.spec_conformance.gen_statement'. Adds req-sc-009 and req-sc-010 entries; consumer active count 61 -> 63; total_requirements 90 -> 92. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Advisory review + governance decisionHi @sergio-sisternes-epam -- this is a consolidated advisory synthesis (apm-review-panel + a 4-expert enterprise-governance panel arbitrated by the CEO persona). It is advisory: nothing here blocks merge. CI is green; the Stance: ship_with_followupsThe narrow security wins are strictly positive and should land: default-deny consent expanded to hooks/bin/mcp/canvas, and personal approvals moved off the committed project file. Two small must-fixes before merge, then a larger design follow-up that this PR should not be blocked on. Must-fix before merge (CI-invisible)
One design ask for this PR
Two governance gaps this PR surfaces (follow-up, not blockers)
Where the bigger design goesThese two gaps, plus a control-surface/vocabulary unification ( Thanks for the hardening work here -- the gate expansion is the right direction. |
danielmeppiel
left a comment
There was a problem hiding this comment.
Must-fix before merge (CI-invisible)
Stale integration test contradicts the new source. src/apm_cli/security/executables.py now sets ENFORCED_EXEC_TYPES = (HOOKS, BIN, MCP, CANVAS), but tests/integration/test_executables_gate_integration.py was not updated and still asserts the opposite:
line 511: assert EXEC_TYPE_MCP not in ENFORCED_EXEC_TYPES
line 136: assert EXEC_TYPE_MCP not in decl.exec_types
This is invisible on this PR because ci.yml runs only tests/unit; the integration suite runs in a separate workflow. It will break the integration pipeline the moment it is next collected. Please update these assertions to match the new enforced set.
Missing CHANGELOG entry. This is a user-visible hardening (and removes --trust-canvas-extensions, which is breaking). Please add a CHANGELOG line under [Unreleased], and call out the flag removal explicitly.
harden(executables): expand allowExecutables gate to mcp and canvas; store approvals in user-local file
TL;DR
The
allowExecutablesdefault-deny gate (introduced in #1723) previouslyenforced only
hooksandbin, leaving MCP server config writes and canvasdeploys outside its control. This PR promotes
mcpandcanvasto first-classenforced types and removes
--trust-canvas-extensions, unifying all fourexecutable surfaces under a single approval model. It also fixes a supply-chain
security threat:
apm approvepreviously wrote grants intoapm.yml(committed),meaning anyone cloning a project inherited all executable trusts automatically.
Approvals now live in
~/.apm/approvals.yml— user-local, never committed.Important
Breaking changes:
--trust-canvas-extensionsis removed. Consumers of dependency-provided canvasesmust add an
allowExecutables: {}block toapm.ymland runapm approve <pkg>.Existing
allowExecutablesentries inapm.ymlstill work for CI pipelines.Personal approvals from interactive
apm approvesessions now go to~/.apm/approvals.yml.Problem (WHY)
security/executables.pymarked MCP as"Reserved for future enforcement" — a self-described gap. A dependency could write
an MCP config entry without any approval step.
--trust-canvas-extensionswas inconsistent with the gate model. The flagwas a one-shot CLI opt-in that bypassed the
allowExecutablestable, so canvastrust was invisible to
apm denyand to any future policy enforcement._local.check_executable_approvalshort-circuited for
package_name == "_local"before checkingis_first_party,meaning a malicious dependency named
_localwithis_first_party=Falsewouldbypass the canvas gate via the name shortcut alone.
apm approvewrote to projectapm.yml— a supply-chain security threat.Committed approvals mean any developer cloning the repo inherits all executable
trusts automatically. One developer's
apm approve <malicious-pkg>would silentlygrant execution rights across the entire team.
The approval gate was designed to deliver
"No arbitrary code execution"
for all executable surfaces — MCP server processes and canvas Node.js bundles
both qualify and both need the same governance; and approval must remain personal.
Approach (WHAT)
EXEC_TYPE_CANVAS; moveEXEC_TYPE_MCPfrom "tracked" toENFORCED_EXEC_TYPES— four types, one gatecheck_executable_approvalreturns 4-tuple(hooks_ok, bin_ok, mcp_ok, canvas_ok)is_package_approvedindependently of_localname shortcutcollect_transitiveincommands/install.py--trust-canvas-extensions/trust_canvasfrom all CLI options, dataclasses, and pipeline kwargsapm approvewrites to~/.apm/approvals.yml(user-local);effective_allow_executables()merges project gate signal + user approvals at install timeImplementation (HOW)
security/executables.py—EXEC_TYPE_CANVASadded;ENFORCED_EXEC_TYPESholds all four;ExecutableDeclarationgainscanvas_count/canvas_details; scanner detects.apm/extensions/*/extension.mjs; new helpersget_user_approvals_path,load_user_approvals,save_user_approvals,effective_allow_executables.install/exec_gate.py—check_executable_approvalexpanded to 4-tuple.install/services.py— 4-tuple destructured; canvas gate usesis_first_party+ explicitis_package_approvedcall (bypasses_localname shortcut for defence-in-depth).install/service.py— removed staletrust_canvas=request.trust_canvaskwarg.commands/install.py—--trust-canvas-extensionsandInstallOptions.trust_canvasremoved; MCP gate inserted aftercollect_transitive; MCP lookup useseffective_allow_executables().install/context.py,request.py,pipeline.py—trust_canvasfield and param removed throughout.commands/approve.py—_load_allow_executables()reads from~/.apm/approvals.ymlviaget_user_approvals_path();_approve_all_pending/_approve_packageswrite viasave_user_approvals();deny_cmdno longer requires project manifest.install/phases/integrate.py— interactive approval prompt seeds fromload_user_approvals()and persists viasave_user_approvals().install/template.py—_effective_allow(ctx)helper merges project gate signal with user approvals.approve.md,canvas.md,security.md,primitives-and-targets.md,install.md,unpack.md,experimental.md,governance.md,package-authoring.md,commands.md,apm-contributor-dashboard/README.mdupdated.test_executables.py,test_canvas_integrator.py,test_install_local_bundle.py,test_unpacker.py,test_models_validation_rules.py,test_install_context.py,test_approve_deny.pyupdated.Diagrams
Gate chain after this PR — new nodes (dashed) show the MCP and canvas gates added to the existing hooks gate.
flowchart LR A["scan_package_executables"] --> B["4-tuple result"]:::new B --> C{"hooks blocked?"} B --> D{"mcp blocked?"}:::new B --> E{"canvas blocked?"}:::new C -->|yes| F["skip + warn apm approve"] C -->|no| G["deploy hooks"] D -->|yes| H["skip MCP config write"]:::new D -->|no| I["deploy MCP config"] E -->|yes| J["skip canvas deploy"]:::new E -->|no| K["deploy .github/extensions/"] classDef new stroke-dasharray: 5 5; class B,D,E,H,J new;Approval storage split — project file enables the gate and holds CI grants; user-local file holds personal approvals.
flowchart LR P["apm.yml\n(allowExecutables: {})"] U["~/.apm/approvals.yml\n(user-local, not committed)"] E["effective_allow_executables()"] I["apm install\nexecutable gate"] P -->|"gate signal + CI grants"| E U -->|"personal approvals"| E E -->|"merged approvals"| ITrade-offs
--trust-canvas-extensionsremoved (not deprecated). Breaking change; callers must migrate toallowExecutables+apm approve.apm approve --allnow writes only to~/.apm/approvals.yml. The old CI pattern ofapm approve --all && git add apm.ymlno longer works automatically. CI pipelines must either commit grants directly inapm.ymlor runapm approvein a step that has write access to the runner's home directory. Documented inapprove.md.deny_cmdno longer requires project manifest.apm denycan now be run from any directory. This is intentional — revoking a trust grant should not depend on having the project checked out.apm unpack(deprecated) canvas gate simplified. Canvas deploys freely when thecanvasfeature flag is ON inapm unpack, since the deprecated command has no project context forallowExecutableslookup. Narrow surface, accepted regression.Benefits
allowExecutablestable — oneapm approve, one audit trail.allowExecutables: {}no longer auto-grants any executable trust; each developer must explicitly approve packages._localspoofing of the canvas gate is closed.Validation
Lint chain (all clean)
Test suite (17,342 passed)
Scenario Evidence
allowExecutablesblock — gate disabled, hooks/bin/mcp/canvas deploy freely (backward compat)test_canvas_deploys_when_allow_executables_nonetest_dispatch_dependency_named_local_is_not_first_party_localwithis_first_party=Falsestill blocked by canvas gatetest_dispatch_dependency_named_local_is_not_first_partyapm approve <pkg>writes grant to~/.apm/approvals.yml, NOT toapm.ymltest_approve_all_writes_user_fileapm deny <pkg>removes from user file, not project filetest_deny_existing_entryHow to test
allowExecutables: {}to a testapm.yml; runapm approve <pkg>; confirm~/.apm/approvals.ymlis updated andapm.ymlis unchanged.apm install --target copilotand confirm approved canvas deploys to.github/extensions/.apm approve.apm install --trust-canvas-extensionsexits with "unrecognized option" (flag is gone).uv run --extra dev pytest tests/unit/ -q— confirm 0 failures.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com