Skip to content

Experimental canvas: support --global install to ~/.copilot/extensions/ (user scope) #1691

@sergio-sisternes-epam

Description

@sergio-sisternes-epam

Summary

The experimental Copilot canvas primitive (shipped in #1689) is deliberately project-scope-onlyapm install deploys a canvas to <git-root>/.github/extensions/<name>/. This issue tracks adding --global / user-scope support so a canvas can be deployed to ~/.copilot/extensions/<name>/ and become available in every Copilot session, not just the one repo.

Confirmation: Copilot DOES scan the global folder

Verified against the GitHub Copilot CLI runtime contract (bundled with the installed app):

Source Evidence
RPC schema — copilot-sdk/generated/rpc.d.ts ExtensionSource = "project" | "user"; "Extension discovered from the user's ~/.copilot/extensions directory."
Bundled SDK docs — copilot-sdk/docs/extensions.md "The CLI scans .github/extensions/ (project) and the user's copilot config extensions directory"
create-canvas skill Discovery scans immediate subdirs of .github/extensions/ (git root) and $COPILOT_HOME/extensions/ (default ~/.copilot).

So global install is technically supportable and useful.

What's already in place

APM's user-scope machinery already maps the copilot TargetProfile (user_root_dir=".copilot", anchored at Path.home()) + the canvas PrimitiveMapping(subdir="extensions") to ~/.copilot/extensions/ — the exact path Copilot discovers. Canvas is blocked at user scope only by:

  • unsupported_user_primitives=("canvas",) on the copilot profile (src/apm_cli/integration/targets.py:480), and
  • a belt-and-braces guard in the integrator: if scope is InstallScope.USER: return empty (src/apm_cli/integration/canvas_integrator.py:173-177).

Blocking implementation work (not just a flag flip)

A safe implementation must address these, because a canvas is arbitrary executable Node.js loaded into every future session:

  1. User-scope local-file tracking gap (critical). post_deps_local.py skips user scope (if ctx.scope is not InstallScope.PROJECT: return, lines 23-25 / 48-50). A first-party canvas deployed globally would not be recorded in local_deployed_files, so apm uninstall -g / stale-sync would not prune it — install works but uninstall leaks executable code globally. Fixing this touches the user-scope local-file tracking chokepoint for all user-scope primitives.
  2. $COPILOT_HOME is unhandled. It is read nowhere in APM (grep COPILOT_HOME src/ → none); APM hardcodes .copilot under Path.home(). If a user sets $COPILOT_HOME outside home, APM would deploy to the wrong place relative to where Copilot scans. Honouring it correctly interacts with the lockfile URI scheme — _deployed_path_entry (src/apm_cli/install/services.py:90-96) would mis-encode a dynamic Copilot root as cowork://.
  3. Cleanup divergence. cleanup.py:170-177 resolves deployed files against the current root. If $COPILOT_HOME differs between install and uninstall, the original global executable could be orphaned. Needs install-root identity persisted in the lockfile.
  4. CanvasIntegrator computes paths manually (canvas_integrator.py:192-195) instead of via TargetProfile.deploy_path(...), so it won't honour a dynamic resolver without rework.
  5. Local/offline bundle path (services.py:712-741) filters canvas paths itself with hardcoded .github/extensions/ messaging — needs scope-aware deploy root + diagnostics.

Security recommendation: scope-aware trust gate

Today first-party canvases deploy freely (flag only); dependency canvases need --trust-canvas-extensions. At global scope the blast radius is the whole user account (runs in every session), so:

  • project first-party canvas → flag only (unchanged)
  • global first-party canvas → require --trust-canvas-extensions (new)
  • dependency canvas → require trust in both scopes (unchanged)

npm/pip precedent (global binaries without a trust flag) is weak here — a global Copilot extension is a persistent plugin, not a PATH binary. Update the flag help (install.py:924-927) which currently says "provided by dependencies".

Other notes

  • Shadowing: a project .github/extensions/<name>/ shadows a user ~/.copilot/extensions/<name>/ at Copilot discovery time. On global install, if the current repo has a same-named project canvas, emit an informational warning.
  • Removing canvas from unsupported_user_primitives should not affect apm compile (driven by compile_family), but add a narrow regression test.

Acceptance criteria

  • apm install --global deploys a first-party canvas to $COPILOT_HOME/extensions/ (default ~/.copilot/extensions/), gated on the experimental flag and --trust-canvas-extensions.
  • apm uninstall -g (and stale-sync) reliably removes the global canvas — no orphaned executable files, including the $COPILOT_HOME-changed case (or a loud warning if unremovable).
  • $COPILOT_HOME honoured for both deploy and cleanup.
  • Scope-aware trust gate + updated diagnostics/help.
  • Tests (integrator user-scope deploy + trust gate; tracking/cleanup incl. out-of-home $COPILOT_HOME; compile-unchanged regression), docs (docs/src/content/docs canvas page + packages/apm-guide/.apm/skills/apm-usage/), CHANGELOG.

Discovered while validating #1689 end-to-end. Evidence and design critique captured in that work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/distributionInstallers (curl/PowerShell/Brew/Scoop), self-update, devcontainer, codespaces.area/docs-sitedocs/src/content (Starlight), README, doc generation.enhancementDeprecated: use type/feature. Kept for issue history; will be removed in milestone 0.10.0.status/needs-designDirection approved, design discussion required before code.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/securitySecure by default. Content scanning, lockfile integrity, MCP trust boundaries.type/featureNew capability, new flag, new primitive.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions