feat(install): experimental Copilot canvas extensions#1689
feat(install): experimental Copilot canvas extensions#1689sergio-sisternes-epam wants to merge 10 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an experimental “canvas” primitive to let APM packages ship GitHub Copilot CLI canvas extensions (bundles under .apm/extensions/<name>/) and deploy them to .github/extensions/<name>/, guarded by (1) the canvas experimental flag and (2) an explicit --trust-canvas-extensions opt-in for dependency-provided executable code. Wiring covers normal installs, offline bundle installs, and apm unpack, plus documentation and tests.
Changes:
- Introduces
CanvasIntegratorand data-driven dispatch/target mapping for a Copilot-onlycanvasprimitive. - Plumbs
--trust-canvas-extensionsthrough install context/request/pipeline and enforces fail-closed behavior on bundle/unpack paths. - Adds unit tests + docs + changelog entry for the new primitive and gating model.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_unpacker.py | Adds unpack-path tests for the two-gate canvas blocking/deploy behavior. |
| tests/unit/integration/test_data_driven_dispatch.py | Updates dispatch/partition expectations to account for the new canvas primitive + counters. |
| tests/unit/integration/test_canvas_integrator.py | New comprehensive unit suite for CanvasIntegrator behavior, gating, collisions, and sync removal. |
| tests/unit/install/test_install_local_bundle.py | Adds tests ensuring offline bundle installs can’t smuggle canvases without both gates. |
| tests/unit/install/test_architecture_invariants.py | Updates the commands/install.py line-budget to accommodate new option/plumbing. |
| tests/unit/commands/test_install_phase3.py | Adds trust_canvas to InstallContext construction tests. |
| tests/unit/commands/test_install_context.py | Extends InstallContext field/round-trip tests for trust_canvas. |
| tests/unit/commands/test_install_context_and_resolution.py | Mirrors context resolution tests for trust_canvas. |
| tests/unit/bundle/test_plugin_exporter_canvas.py | New tests ensuring apm pack preserves extensions/ content into bundles. |
| src/apm_cli/integration/targets.py | Adds Copilot target mapping for canvas -> extensions/ and marks it unsupported at user scope. |
| src/apm_cli/integration/dispatch.py | Registers canvas in the primitive dispatch table. |
| src/apm_cli/integration/canvas_integrator.py | New integrator implementing discovery, validation, atomic deploy, trust gating, and sync removal for canvases. |
| src/apm_cli/install/template.py | Wires optional canvas integrator into template materialization + delta accounting. |
| src/apm_cli/install/services.py | Threads trust_canvas + is_first_party into dispatch and adds post-integrate hints for canvases. |
| src/apm_cli/install/service.py | Threads trust_canvas from InstallRequest to the pipeline. |
| src/apm_cli/install/request.py | Adds trust_canvas field describing the trust opt-in for dependency canvases. |
| src/apm_cli/install/pipeline.py | Adds trust_canvas parameter and threads it through to request/context. |
| src/apm_cli/install/phases/targets.py | Wires CanvasIntegrator into the per-target integrator set. |
| src/apm_cli/install/phases/local_content.py | Treats .apm/extensions/ as local content worth integrating. |
| src/apm_cli/install/phases/integrate.py | Accounts for canvases in root integration summary bookkeeping. |
| src/apm_cli/install/local_bundle_handler.py | Threads trust_canvas into offline bundle install handler. |
| src/apm_cli/install/drift.py | Excludes canvas deploy prefixes from drift replay comparison. |
| src/apm_cli/install/context.py | Adds trust_canvas to the runtime install context. |
| src/apm_cli/core/experimental.py | Registers the new canvas experimental flag and user-facing hint text. |
| src/apm_cli/commands/pack.py | Adds apm unpack --trust-canvas-extensions and surfaces blocked-canvas messaging. |
| src/apm_cli/commands/install.py | Adds apm install --trust-canvas-extensions and threads it into install context + local bundle install. |
| src/apm_cli/bundle/unpacker.py | Adds trust_canvas to unpack and enforces the two-gate blocking + canvas_blocked reporting. |
| src/apm_cli/bundle/plugin_exporter.py | Includes extensions/ in pack collection for .apm/ and root plugin formats. |
| packages/apm-guide/.apm/skills/apm-usage/package-authoring.md | Documents authoring .apm/extensions/<name>/extension.mjs and deploy behavior. |
| packages/apm-guide/.apm/skills/apm-usage/governance.md | Documents the trust model and fail-closed enforcement across install paths. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Updates CLI reference to mention --trust-canvas-extensions. |
| docs/src/content/docs/integrations/canvas.md | New user-facing integration guide for canvas extensions. |
| docs/src/content/docs/concepts/primitives-and-targets.md | Adds canvas to the primitives/targets model + matrix. |
| docs/astro.config.mjs | Adds the new canvas guide page to the docs sidebar. |
| CHANGELOG.md | Adds an Unreleased entry announcing the experimental canvas primitive. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | CanvasIntegrator fits BaseIntegrator subclass pattern; integrate_local_content breaks DI by inlining CanvasIntegrator() instead of a parameter; canvas phase counters not accumulated in integrate.py. |
| CLI Logging Expert | 0 | 3 | 3 | Trust-block mis-categorized as CATEGORY_WARNING (wrong color/order), package name duplicated in rendered output, path-traversal warnings lose package attribution; 3 nits. |
| DevX UX Expert | 0 | 4 | 2 | Canvas primitive is well-designed but three CLI reference docs are missing the new flag/feature, and install silently swallows --trust-canvas-extensions when the feature gate is off. |
| Supply Chain Security Expert | 0 | 3 | 2 | Trust gate logic is sound and no-spoof proof is in place; two recommended gaps: no integration-tier test for the gate and is_canvas_bundle_path false-negative on crafted lockfile paths. |
| OSS Growth Hacker | 0 | 3 | 2 | Canvas ships clean but missing a starter template, CHANGELOG session-reload note, and any story angle for APM's first native Copilot-CLI-extension primitive. |
| Auth Expert | 0 | 1 | 1 | Canvas trust gate is sound: is_first_party is call-path-controlled only, trust_canvas propagates correctly through context, no auth tokens touched; comment imprecision on 'root/local' scope worth fixing. |
| Doc Writer | 0 | 4 | 1 | canvas.md code-accurate; security.md cross-link misleads (no canvas trust content, 'no code execution' claim now false); /clear hint suspect; CHANGELOG missing credit and blank line. |
| Test Coverage Expert | 0 | 1 | 0 | 6 trust-gate scenarios have unit/fixture coverage; canvas drift-exclusion logic in drift.py (lines 589-646) has no test and will silently regress. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
-
[Doc Writer] security.md 'no code execution' claim is now actively false for canvas -- remove cross-link and add canvas exception callout -- canvas.md links to security.md for the trust rationale, but security.md still states 'APM does not execute scripts.' Canvas deploys executable Node.js via APM. Enterprise auditors following that cross-link receive an actively false assurance picture. Fix: remove the dangling cross-link from canvas.md; add a canvas exception callout to security.md under 'What APM does NOT do.' This is the highest documentation trust risk in the PR and must land before the draft exits.
-
[Supply Chain Security Expert] Add integration-with-fixtures test for the canvas trust gate across the full install pipeline -- All existing trust-gate coverage is unit-only (monkeypatched config, tmp_path). For a surface that deploys executable code, APM's tier-floor requires integration-with-fixtures. A future refactor mis-threading trust_canvas or is_first_party would not be caught until runtime. Add a tests/integration/ fixture-backed test that runs apm install without and with --trust-canvas-extensions against a real local dep shipping a canvas. Evidence.outcome: missing on secure-by-default and governed-by-policy surfaces.
-
[DevX UX Expert] Three CLI reference docs missing --trust-canvas-extensions: install.md, experimental.md, unpack.md -- same-PR fix -- A CLI flag introduced in a PR with no corresponding reference doc entry is an incomplete DevX contract. All three carry evidence.outcome: missing on static tier. The flag was added in this PR and the doc entries should ship in the same PR.
-
[Test Coverage Expert] Canvas drift-exclusion logic in drift.py has no regression-trap test at any tier -- drift.py:589-646 excludes canvas deploy paths from apm audit --check drift orphan detection. Zero test coverage found. Evidence.outcome: missing, integration-with-fixtures tier, cross-module surface (drift.py + canvas_integrator.py + KNOWN_TARGETS). A future refactor will silently re-surface deployed canvas paths as orphaned findings in apm audit with no automated signal. Suggested assertion:
assert not any('.github/extensions' in f.path for f in findings). -
[CLI Logging Expert] Trust-block fires at CATEGORY_WARNING not CATEGORY_POLICY; apm install silently swallows --trust-canvas-extensions when canvas flag is off -- Two UX consistency gaps: (1) Blocking executable Node code from a dependency is a policy-level security gate -- it should render red [x] at CATEGORY_POLICY alongside MCP trust blocks, not yellow [!] at CATEGORY_WARNING. (2) apm unpack already emits the correct two-case diagnostic when the flag is passed with the feature gate off; apm install should do the same. Both are targeted fixes in canvas_integrator.py.
Architecture
classDiagram
direction LR
class BaseIntegrator {
<<Template>>
+check_collision(target, rel, managed, force) bool
+normalize_managed_files(files) set
+is_content_identical_to_source(target, src) bool
+sync_remove_files(root, files, prefix) dict
+cleanup_empty_parents(paths, stop_at) None
}
class CanvasIntegrator {
<<ConcreteTemplate>>
+find_canvas_bundles(package_path) list
+integrate_canvases_for_target(target, pkg, root, ...) IntegrationResult
+sync_for_target(target, pkg, root, managed) dict
-_validate_canvas_name(name) None
-_deploy_bundle(bundle, ext_dir, root, ...) str
-_plan_bundle_files(bundle, canvas_root, ...) list
-_emit_trust_block(bundles, pkg_name, ...) None
}
class PromptIntegrator {
<<ConcreteTemplate>>
+integrate_prompts_for_target(...) IntegrationResult
+sync_for_target(...) dict
}
class IntegrationResult {
<<ValueObject>>
+files_integrated int
+files_skipped int
+files_adopted int
+target_paths list
+links_resolved int
}
class IntegratorBundle {
<<ValueObject>>
+prompt BaseIntegrator
+agent BaseIntegrator
+skill BaseIntegrator
+instruction BaseIntegrator
+command BaseIntegrator
+hook BaseIntegrator
+canvas BaseIntegrator_or_None
}
class TargetProfile {
<<ValueObject>>
+name str
+root_dir str
+primitives dict
+for_scope(user_scope) TargetProfile
}
class PrimitiveMapping {
<<ValueObject>>
+subdir str
+deploy_root str
}
BaseIntegrator <|-- CanvasIntegrator : extends
BaseIntegrator <|-- PromptIntegrator : extends
CanvasIntegrator ..> IntegrationResult : returns
IntegratorBundle o-- BaseIntegrator : canvas optional
TargetProfile *-- PrimitiveMapping : primitives canvas
CanvasIntegrator ..> TargetProfile : reads name primitives
class CanvasIntegrator:::touched
class IntegratorBundle:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm install command"] --> B["run_install_pipeline\ntrust_canvas stored in InstallContext"]
B --> C["targets.py: ctx.integrators[canvas] = CanvasIntegrator()"]
B --> D["integrate.py run()"]
D --> E["for dep in deps_to_install"]
E --> F["integrate_package_primitives\nIntegratorBundle(canvas=ctx.integrators.get('canvas'))"]
F --> G["dispatch: prim_name == 'canvas'"]
G --> K["CanvasIntegrator.integrate_canvases_for_target\ntrust_canvas=ctx.trust_canvas, is_first_party=False"]
K --> L{is_enabled('canvas')?}
L -- NO --> M["return empty"]
L -- YES --> N{"target == 'copilot'\nAND scope != USER"}
N -- NO --> M
N -- YES --> O["find_canvas_bundles\n.apm/extensions/ walk"]
O --> P{bundles found?}
P -- NO --> M
P -- YES --> Q{is_first_party OR trust_canvas?}
Q -- NO --> R["_emit_trust_block diagnostic\nreturn empty"]
Q -- YES --> S["_deploy_bundle: validate + plan + copy"]
S --> AA["[FS] shutil.copyfile to .github/extensions/name/"]
D --> BB["_integrate_root_project\nis_first_party=True, bypasses trust gate"]
BB --> K
Recommendation
The canvas primitive's security model is validated: two-gate trust, no-spoof proof, fail-closed defaults. Recommend addressing three items before the draft exits: (1) correct the security.md 'no code execution' claim -- it is actively false for canvas and must not reach enterprise auditors unpatched; (2) add the three missing --trust-canvas-extensions entries to install.md, experimental.md, and unpack.md -- a flag with no reference doc is an incomplete DevX contract in the same PR; (3) add by @sergio-sisternes-epam, #1689 to the CHANGELOG entry per project convention. The integration-tier trust gate test and the drift.py regression trap are the highest-signal post-merge follow-ups -- queue them as the first issues opened after merge so they do not drift.
Full per-persona findings
Python Architect
-
[recommended] integrate_local_content creates CanvasIntegrator() internally instead of accepting a canvas_integrator parameter, breaking the consistent DI pattern of all 6 sibling integrators at
src/apm_cli/install/services.py:580
Every other integrator is injected through IntegratorBundle. Canvas alone is created inline, so the call site in phases/integrate.py:335 cannot reuse ctx.integrators['canvas'], and test authors cannot substitute a mock without patching the class directly. This violates the Inject collaborators via constructor principle already working for the other six integrators.
Suggested: Add canvas_integrator: Any = None to integrate_local_content's keyword args and use (canvas_integrator or CanvasIntegrator()) in the IntegratorBundle constructor. Update the call in phases/integrate.py to pass canvas_integrator=ctx.integrators.get('canvas'). -
[nit] canvas count in deltas is silently dropped; no total_canvases_integrated on InstallContext; finalize verbose log has no canvas summary line at
src/apm_cli/install/phases/integrate.py:595
template.py populates deltas['canvases'] but integrate.py never accumulates it and InstallContext has no total_canvases_integrated field, diverging from the established hook/command/instruction pattern. -
[nit] canvas_root.parent.resolve() / name in _deploy_bundle is equivalent to canvas_root.resolve() but obscures intent at
src/apm_cli/integration/canvas_integrator.py:298
Simplify to ensure_path_within(canvas_root.resolve(), extensions_dir.resolve()) to make the containment check self-evident.
CLI Logging Expert
-
[recommended] Trust-block diagnostic uses CATEGORY_WARNING instead of CATEGORY_POLICY, understating severity and rendering in wrong position at
src/apm_cli/integration/canvas_integrator.py:416
_emit_trust_block calls diagnostics.warn() (CATEGORY_WARNING), which renders yellow [!] AFTER collision/overwrite warnings. Blocking executable Node code from a dependency is a policy-level security gate -- identical to an MCP trust block. Using CATEGORY_POLICY would render red [x], appear earlier in the summary, and match how other executable-code gates are surfaced.
Suggested: Change diagnostics.warn(message=..., package=pkg) to diagnostics.policy(message=..., package=pkg, severity='block'). Match the MCP trust block pattern. -
[recommended] Package name appears twice in rendered trust-block output: once in the [pkg] prefix, once embedded in the message body at
src/apm_cli/integration/canvas_integrator.py:418
_render_warning_group prepends [d.package] automatically, so the embedded "from 'pkg'" phrase is redundant noise in the rendered line.
Suggested: Remove the embedded "from '{pkg}'" phrase from the message body. -
[recommended] _plan_bundle_files emits path-traversal warnings with empty package attribution, making security events untraceable in multi-package installs at
src/apm_cli/integration/canvas_integrator.py:390
_deploy_bundle receives package_name but does not forward it to _plan_bundle_files. The _warn call passes empty string, so the rendered warning has no [pkg] prefix.
Suggested: Add package_name param to _plan_bundle_files and pass it through from _deploy_bundle. -
[nit] _warn fallback calls _rich_warning(message) without symbol='warning', dropping the [!] prefix when no DiagnosticCollector is present at
src/apm_cli/integration/canvas_integrator.py:433 -
[nit] --trust-canvas-extensions help text says 'Requires the canvas experimental feature' without telling the user the enable command at
src/apm_cli/commands/install.py:926 -
[nit] The /clear hint may mislead users: /clear clears session context but may not trigger Copilot extension re-discovery at
src/apm_cli/install/services.py:161
/clear keeps the session alive; extension discovery happens at session start. 'or restart' should lead the hint.
DevX UX Expert
-
[recommended] apm install CLI reference missing --trust-canvas-extensions in the Policy and trust options table at
docs/src/content/docs/reference/cli/install.md:56
docs/src/content/docs/reference/cli/install.md has a 'Policy and trust' section listing --trust-transitive-mcp. --trust-canvas-extensions belongs there and is absent. A CLI change not reflected in the reference doc in the same PR is incomplete per DevX contract.
Proof (missing at):docs/src/content/docs/reference/cli/install.md-- proves: apm install --help and the CLI reference page agree on available flags [devx] -
[recommended] Experimental flags reference missing the canvas flag in the Available flags table at
docs/src/content/docs/reference/experimental.md:170
docs/src/content/docs/reference/experimental.md lists every registered flag but canvas is absent despite being registered in experimental.py.
Proof (missing at):docs/src/content/docs/reference/experimental.md-- proves: apm experimental list output and the experimental flags reference page agree on available flags [devx] -
[recommended] apm unpack CLI reference missing --trust-canvas-extensions in its options table at
docs/src/content/docs/reference/cli/unpack.md:28
pack.py adds --trust-canvas-extensions to unpack_cmd but the unpack reference page has no entry.
Proof (missing at):docs/src/content/docs/reference/cli/unpack.md-- proves: apm unpack --help and the CLI reference page agree on available flags [devx] -
[recommended] apm install silently accepts --trust-canvas-extensions when the canvas feature flag is off, giving the user no feedback that the flag did nothing at
src/apm_cli/integration/canvas_integrator.py:164
canvas_integrator returns empty before any trust_canvas check when the flag is off. apm unpack (pack.py:713-718) correctly emits a two-case diagnostic; apm install should be consistent.
Suggested: Before the early return at line 164, add: if not is_enabled('canvas') and trust_canvas: emit a warning telling the user that --trust-canvas-extensions has no effect until 'apm experimental enable canvas' is run.
Proof (missing at):tests/unit/integration/test_canvas_integrator.py-- proves: apm install --trust-canvas-extensions emits a hint when the canvas feature flag is off [devx] -
[nit] [experimental] prefix on --trust-canvas-extensions help text is inconsistent with the sibling --trust-transitive-mcp flag at
src/apm_cli/commands/install.py:926 -
[nit] pack.md plugin bundle output format section does not list extensions/ alongside the other plugin-native subdirs at
docs/src/content/docs/reference/cli/pack.md:104
Supply Chain Security Expert
-
[recommended] No integration-with-fixtures test for the canvas trust gate in the full install pipeline
All gate tests are unit-level (monkeypatched config cache, tmp_path fixtures, no real CLI invocation). For a surface that deploys executable code, APM's tier-floor matrix requires integration-with-fixtures. A future refactor mis-threading trust_canvas or is_first_party would not be caught until runtime.
Suggested: Add a tests/integration/ fixture-backed test that: (a) runs apm install without --trust-canvas-extensions against a dep shipping a canvas and asserts .github/extensions/ is absent, (b) re-runs with --trust-canvas-extensions and asserts the file is deployed.
Proof (missing at):tests/integration/-- proves: Dependency canvas is blocked by default and deploys only with explicit operator opt-in across the full CLI surface [secure-by-default,governed-by-policy] -
[recommended] is_canvas_bundle_path false-negative: crafted bundle lockfiles can route canvas content through non-standard paths that evade the gate at
src/apm_cli/integration/canvas_integrator.py:66
is_canvas_bundle_path returns True only when 'extensions' is (a) the first path component or (b) immediately after a dot-dir. A bundle lockfile with entries like 'src/extensions/evil/extension.mjs' returns False, bypassing the block in unpacker.py:167-173 and services.py:724-728.
Suggested: Also flag any path whose final filename is 'extension.mjs', OR document the narrow scope and add a test for the False case.
Proof (missing at):tests/unit/integration/test_canvas_integrator.py::test_is_canvas_bundle_path
('src/extensions/evil/extension.mjs', False) # not currently in parametrize list -
[recommended] shutil.copyfile in _deploy_bundle follows destination symlinks, creating a TOCTOU path-escape window at
src/apm_cli/integration/canvas_integrator.py:358
shutil.copyfile(src, dest) follows dest if it is a symlink to a regular file (dest.is_file() returns True for symlink-to-file). The ensure_path_within check runs at planning time against the unresolved dest. A symlink placed between planning and copy could redirect a canvas file write outside canvas_root.
Suggested: Add a per-file symlink check immediately before the copy: if dest.is_symlink(): warn and skip. Or add a comment accepting the TOCTOU risk (attacker needs pre-existing write access). -
[nit] ensure_path_within(canvas_root.parent.resolve() / name, extensions_dir.resolve()) is logically equivalent to 'always True' and should be commented as defense-in-depth at
src/apm_cli/integration/canvas_integrator.py:298 -
[nit] shutil.copyfile vs shutil.copy2 choice is security-intentional (avoids setuid/setgid bits) but undocumented at
src/apm_cli/integration/canvas_integrator.py:358
Suggested: Add comment: 'Intentionally copyfile (content only) rather than copy2: avoids propagating setuid/setgid bits or special modes from package-supplied canvas source files.'
OSS Growth Hacker
-
[recommended] No starter template for canvas authoring -- first-use experience requires filling in values without a working default at
docs/src/content/docs/integrations/canvas.md:39
The canvas.md 'Author a canvas' section shows a directory tree but no file contents for even a minimal extension.mjs. templates/ has hello-world and policy starters but nothing for canvas.
Suggested: Add templates/canvas-starter/ with a minimal extension.mjs (5-line hello-world canvas) and a matching apm.yml that declares the copilot target. Link it from canvas.md. -
[recommended] CHANGELOG entry omits the session-reload requirement -- will generate 'my canvas is not showing up' support noise at
CHANGELOG.md:12
The CHANGELOG entry describes what the primitive does but omits that a freshly deployed canvas is not picked up mid-session and requires a reload or restart.
Suggested: Append to the canvas bullet: 'After install, reload your Copilot session (/clear or restart) -- Copilot CLI discovers extensions at session start only.' -
[recommended] No launch narrative for APM's first native Copilot-CLI-extension primitive -- new surface area with no story angle
Canvas is APM's first primitive that extends the Copilot CLI itself. No README mention, no quickstart callout, no repostable one-liner. The CHANGELOG entry reads as an engineer's note; nothing in it is repostable by a Copilot power user.
Suggested: Prepare a one-liner for social, add a one-sentence mention in README under 'What APM manages', and queue a quickstart callout for when the feature exits experimental. -
[nit] canvas.md description is factual but not conversion-oriented for doc site searchers at
docs/src/content/docs/integrations/canvas.md:3 -
[nit] CHANGELOG entry leaves 'canvas' unexplained for users who have not seen the create-canvas skill at
CHANGELOG.md:12
Auth Expert
-
[recommended] 'root/local' comment in request.py and context.py overstates first-party scope for canvas at
src/apm_cli/install/request.py:69
InstallRequest line 69 and InstallContext lines 67-68 document the first-party exemption as applying to 'root/local' canvases. In practice, only the root project's .apm/ directory receives is_first_party=True (via integrate_local_content). Local-path dependencies declared in apm.yml (path: ../my-pkg) flow through integrate_package_primitives with is_first_party defaulting to False, so their canvases ARE subject to the trust gate. The word 'local' will mislead maintainers.
Suggested: Change 'first-party (root/local)' to 'first-party (root project .apm/)' in both request.py and context.py. -
[nit] integrate_local_bundle trust gate comment could note the absence of is_first_party semantics for offline bundles at
src/apm_cli/install/services.py:724
Doc Writer
-
[recommended] canvas.md cross-link to security.md delivers neither the promised content nor an accurate security picture at
docs/src/content/docs/integrations/canvas.md:110
canvas.md line 110-112 links to security.md for 'the trust rationale'. Two problems: (1) security.md has zero canvas content. (2) security.md states 'No arbitrary code execution. APM does not execute scripts.' Canvas is exactly that: executable Node.js deployed by APM to .github/extensions/. An enterprise auditor gets an actively false picture.
Suggested: Remove the dangling cross-link from canvas.md; add a canvas exception callout to security.md under 'What APM does NOT do'. -
[recommended] '/clear' session-reload hint in Install section is likely incorrect and will mislead users at
docs/src/content/docs/integrations/canvas.md:65
The /clear slash command clears conversation context but does not restart the Copilot CLI process or re-trigger extension discovery. canvas.md itself states discovery happens at session START, which /clear cannot trigger.
Suggested: Replace 'reload your Copilot session (/clear) or restart it' with 'start a new Copilot CLI session (exit and relaunch)'. -
[recommended] CHANGELOG canvas entry missing contributor attribution for @sergio-sisternes-epam at
CHANGELOG.md:17
changelog.instructions.md rule: 'Credit external contributors inline: -- by@username(#PR_NUMBER).' Every other externally-authored entry includes the credit.
Suggested: Change the closing of the canvas bullet to: '...stays blocked until you pass--trust-canvas-extensions. (by @sergio-sisternes-epam, feat(install): experimental Copilot canvas extensions #1689)' -
[recommended] canvas.md Scope and limitations omits the missing policy-file control, which matters to the governance audience at
docs/src/content/docs/integrations/canvas.md:101
Teams using apm-policy.yml to govern trust surfaces would reasonably expect a new trust-gated primitive to be controllable via policy. The omission should be disclosed up-front.
Suggested: Add: 'No policy-file control yet. Canvas trust is controlled only by the --trust-canvas-extensions CLI flag; governing it via apm-policy.yml is planned but not part of this experimental release.' -
[nit] Missing blank line between the Fixed bullet and ### Changed heading in CHANGELOG [Unreleased] at
CHANGELOG.md:26
Test Coverage Expert
- [recommended] Canvas drift-exclusion logic in drift.py has no regression-trap test at
src/apm_cli/install/drift.py:589
_canvas_deploy_prefixes (drift.py:589-605) and its application in diff_scratch_against_project (drift.py:638-646) exclude canvas deploy paths from the orphan/modified check in 'apm audit --check drift'. grep for '_canvas_deploy_prefixes|drift.*canvas|canvas.*orphan' in tests/ returned zero results at any tier. The surface is cross-module, placing the tier floor at integration-with-fixtures.
Suggested: Add to tests/unit/install/test_drift.py a test that asserts deployed canvas paths are not reported as orphaned or drift findings after apm install deploys them.
Proof (missing at):tests/unit/install/test_drift.py::test_canvas_paths_excluded_from_drift_orphan_check
assert not any('.github/extensions' in f.path for f in findings)
-- proves: apm audit --check drift does not report a deployed canvas extension as orphaned after apm install deploys it [devx]
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
pypi.org
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "pypi.org"See Network Configuration for more information.
Generated by PR Review Panel for issue #1689 · sonnet46 28.9M · ◷
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fb52e02 to
ff5e1a3
Compare
Panel Review Response (aecdca8)Verdict: ship_with_followups -- agreed. All foldable findings addressed in commit aecdca8. Folded (14 items)
Deferred (post-merge follow-ups)
|
Add an experimental, Copilot-only "canvas" primitive so a package can ship a GitHub Copilot CLI extension under .apm/extensions/<name>/ and have `apm install` / `apm pack` deploy it verbatim to .github/extensions/<name>/ where Copilot CLI discovers it in-session. Two independent gates protect the surface: - Experimental feature flag `canvas` (feature availability, not security): the primitive type is inert unless the operator runs `apm experimental enable canvas`. - Trust gate for dependency-provided canvases (executable extension.mjs code): blocked by default, opt-in via `--trust-canvas-extensions`, mirroring `--trust-transitive-mcp`. First-party (root/local) canvases deploy freely once the flag is on. First-party status is decided by the call path (the integrate_local_content signal), never inferred from a package-name string a dependency could spoof. The flag + trust gates are enforced on every path that materialises canvas files -- the CanvasIntegrator, the offline local-bundle install, and `apm unpack` -- so a vendored bundle cannot smuggle executable canvas code past either gate. Canvas deploys are atomic per bundle (plan -> collision check -> adopt-if-identical -> copy) and never overwrite a pre-existing directory even under --force. Copilot-only, project-scope MVP: the canvas PrimitiveMapping lives solely on the copilot target; user scope and non-Copilot targets are unsupported. Drift replay deliberately omits canvases (deferred follow-up). After a canvas deploys, install hints that the Copilot session must be reloaded (/clear) or restarted to pick it up. Docs: new integrations/canvas.md page, primitives-and-targets matrix row, and apm-usage skill updates (authoring, governance, install flag). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… scope) Extend the experimental Copilot canvas primitive to deploy at user scope so a canvas is available in every Copilot session, not just one repo. Copilot CLI scans both `<git-root>/.github/extensions/` and `~/.copilot/extensions/`; APM now targets the latter on `apm install --global`. Bounded, leak-safe slice: only dependency-provided canvases deploy globally (the real "package + install --global" UX), which reuses the existing dependency-lockfile persistence and the existing user-scope sync_for_target prune path. First-party root `.apm/extensions/` at user scope stays blocked because the user-scope lockfile pipeline does not track it (would leak an executable bundle on uninstall). Security: a global canvas has full-account blast radius, so `--trust-canvas-extensions` is always required at user scope. A non-default `$COPILOT_HOME` is refused (APM only honors the default `~/.copilot`, which keeps the lockfile path home-relative and prunable). - targets.py: drop "canvas" from copilot `unsupported_user_primitives` so the mapping survives `for_scope(user_scope=True)` (root_dir=.copilot, resolved_deploy_root=None -> clean home-relative lockfile encoding). - canvas_integrator.py: replace the blanket user-scope no-op with scope-aware logic (COPILOT_HOME guard, first-party-at-user block, always-require-trust); add `_copilot_home_is_nondefault` helper. - install.py: note `--global` behaviour in `--trust-canvas-extensions` help. - Tests: user-scope dependency deploy, trust-required, first-party block, non-default COPILOT_HOME block, user-scope uninstall prune. - Docs + apm-usage skill + CHANGELOG updated. Refs #1691 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validation: canvas-only packages are no longer mis-flagged as empty. A warning notes they ship executable code gated by the canvas experimental flag and --trust-canvas-extensions. Audit: text-format output lists deployed canvas extension bundles as an informational note after the summary (lockfile-scan path only; machine-readable formats unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- find_canvas_bundles: reject symlinked base dir, verify resolved child stays within package_path (containment check). - sync_for_target: filter removed_paths through validate_deploy_path before cleanup_empty_parents to prevent traversal via tampered managed_files. - Collapse multi-line CHANGELOG entries to single-line bullets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- security.md: add canvas exception to 'no arbitrary code execution' claim - canvas.md: fix /clear hint, add policy-file limitation, fix cross-link - install.md: add --trust-canvas-extensions to policy table - experimental.md: add canvas flag to Available flags table - unpack.md: add --trust-canvas-extensions to options table - pack.md: add extensions/ to plugin bundle output section - canvas_integrator.py: pre-copy symlink TOCTOU guard, thread package_name into _plan_bundle_files, fix trust-block pkg duplication - request.py + context.py: fix 'root/local' comment to 'root project .apm/' - CHANGELOG.md: add contributor attribution to canvas entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
aecdca8 to
1982630
Compare
The rebase that merged main's hook-display helper with our canvas helpers pushed services.py to 1003 LOC, exceeding the architecture invariant test budget of 1000. Compact two helper docstrings to single-line form -- no behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 3 | 3 | CanvasIntegrator is a well-constructed BaseIntegrator extension; dual-gate design and atomic bundle pre-pass are sound; three gaps: missing verbose canvas total, partial-bundle TOCTOU, singular dispatch key |
| CLI Logging Expert | 0 | 4 | 2 | All strings are ASCII-clean; four UX gaps: 290-char help text, duplicated warning prefix, [i] on every audit list item, reload hint fires on adopt-only reinstalls |
| DevX UX Expert | 0 | 3 | 3 | Architecturally sound trust model; three UX gaps: silent skip on flag-off gives authors zero signal, canvas missing from CLI experimental reference doc, trust scope asymmetry undocumented |
| Supply Chain Security Expert | 1 | 2 | 2 | Two-gate model is sound and is_first_party is non-spoofable; one confirmed trust gate bypass via mixed-case Extensions/ on case-insensitive filesystems; fix is seg.lower() |
| OSS Growth Hacker | 0 | 4 | 2 | Moat-deepening primitive with strong narrative potential; four growth gaps: no end-to-end workflow demo, canvas absent from what-is-apm.md, no installed-canvas discoverability path, CHANGELOG entry is maintainer-facing |
| Auth Expert | -- | -- | -- | No auth surface touched; trust_canvas is a local-deployment bool passthrough with no auth or network components |
| Doc Writer | 0 | 4 | 4 | canvas.md is structurally sound; four substantive gaps: dead create-canvas link, silent no-op undisclosed, audit.md not updated, CHANGELOG PR number mismatch |
| Test Coverage Expert | 0 | 3 | 0 | All 10 unit scenarios covered; three gaps: install pipeline unit-only (integration-with-fixtures floor not met), _canvas_deploy_prefixes in drift.py has zero tests, lockfile round-trip for uninstall unverified |
| Performance Expert | 0 | 2 | 2 | Flag-OFF path is well-optimized (is_enabled is cached); two flag-ON gaps: bundle.rglob has no file count ceiling, adopt-detect reads full file content without size pre-screen |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Supply Chain Security Expert] (blocking-severity) Fix the case-folding defect in
is_canvas_bundle_path(): changeseg != 'extensions'toseg.lower() != 'extensions'atcanvas_integrator.py:91, apply the matching guard inintegrate_local_bundle, and verify against a fixture tarball with a capital-EExtensions/path on a case-insensitive filesystem. -- Confirmed trust gate bypass on macOS HFS+ and Windows NTFS; a tarball withExtensions/myext/extension.mjsdeploys unsigned executable Node.js code without--trust-canvas-extensions. The fix is one line; there is no architectural justification for deferring it. - [Test Coverage Expert] Add an integration-with-fixtures test for the canvas deploy pipeline: install a fixture canvas bundle, assert deployed paths exist under
.github/extensions/, assert those paths appear inapm.lock.yaml, and assert thatapm uninstallremoves them cleanly. -- All 10 unit scenarios pass but the APM install-pipeline floor requires integration-with-fixtures evidence. A new executable-code deployment path with no integration test is a regression trap that will surface as an incident, not a CI failure. - [Test Coverage Expert] Add tests for
_canvas_deploy_prefixesindrift.pycovering: (1) deployed canvas paths are excluded from the drift report, and (2) a path matching a canvas prefix pattern but not a canvas deployment is correctly flagged. -- Zero test coverage confirmed by grep acrosstests/. If this function regresses, everyapm installafter a canvas deploy will emit false drift across all primitive types -- a high-blast-radius silent failure. - [Doc Writer] Replace the dead
create-canvasskill link oncanvas.md:25with a concrete placeholder or forthcoming note, and correct the CHANGELOG PR number mismatch (second canvas entry attributes global install to Experimental canvas: support--globalinstall to~/.copilot/extensions/(user scope) #1691 instead of feat(install): experimental Copilot canvas extensions #1689). -- A broken link on the primary guide page signals incompleteness before a user reads a single line of code. The CHANGELOG mismatch is a durable audit trail error. Both are one-line fixes that belong in the same commit as the security fix. - [DevX UX Expert] Emit a diagnostic warning when canvas install is silently skipped because the experimental flag is off -- at minimum a
[!]line stating canvas bundles were found but not deployed because--experimental=canvasis not enabled. -- Silent no-op on flag-off is the single largest ergonomic gap for canvas authors. Without a signal, authors will open bugs or abandon the primitive after a failed first install that looks identical to a successful one.
Architecture
classDiagram
direction LR
class BaseIntegrator {
+link_resolver UnifiedLinkResolver
+check_collision(target_path, rel_path, managed_files, force) bool
+normalize_managed_files(managed_files) set
+is_content_identical_to_source(target, source) bool
+sync_remove_files(project_root, managed_files, prefix, targets) dict
+cleanup_empty_parents(paths, stop_at) None
+validate_deploy_path(rel, project_root, targets) bool
}
class CanvasIntegrator {
+find_canvas_bundles(package_path) list
+integrate_canvases_for_target(target, package_info, project_root) IntegrationResult
+sync_for_target(target, apm_package, project_root, managed_files) dict
-_deploy_bundle(bundle, extensions_dir, project_root) str
-_plan_bundle_files(bundle, canvas_root, project_root) list
-_validate_canvas_name(name) None
-_copilot_home_is_nondefault() bool
-_warn(diagnostics, message, package_name) None
}
class IntegrationResult {
+files_integrated int
+files_updated int
+files_skipped int
+target_paths list
+links_resolved int
+files_adopted int
}
class IntegratorBundle {
+prompt BaseIntegrator
+agent BaseIntegrator
+skill BaseIntegrator
+instruction BaseIntegrator
+command BaseIntegrator
+hook BaseIntegrator
+canvas BaseIntegrator or None
}
class TargetProfile {
+name str
+root_dir str
+primitives dict
+user_supported bool
+for_scope(user_scope) TargetProfile
+supports(primitive) bool
}
class PrimitiveMapping {
+subdir str
+extension str
+format_id str
+deploy_root str
}
class PrimitiveDispatch {
+integrator_class type
+integrate_method str
+sync_method str
+counter_key str
}
BaseIntegrator <|-- CanvasIntegrator : extends
CanvasIntegrator ..> IntegrationResult : produces
CanvasIntegrator ..> TargetProfile : reads
IntegratorBundle o-- CanvasIntegrator : canvas optional
TargetProfile *-- PrimitiveMapping : primitives
PrimitiveDispatch ..> CanvasIntegrator : instantiates
class CanvasIntegrator:::touched
class IntegratorBundle:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm install --trust-canvas-extensions\ncommands/install.py"] --> B["integrate_package_primitives\ninstall/services.py"]
B --> C{"integrators.canvas\nis None?"}
C -->|"None: legacy sites"| SKIP["no-op return"]
C -->|"CanvasIntegrator present"| D["integrate_canvases_for_target\ncanvas_integrator.py"]
D --> E{"is_enabled('canvas')\ncore/experimental.py"}
E -->|"flag off"| SKIP
E -->|"flag on"| F{"target.name == 'copilot'\nand mapping present?"}
F -->|"no"| SKIP
F -->|"yes"| G["find_canvas_bundles\n.apm/extensions/*/extension.mjs"]
G --> H{"scope == USER?"}
H -->|"user + nondefault COPILOT_HOME"| W1["warn: refuse nondefault COPILOT_HOME"]
H -->|"user + is_first_party"| W2["warn: refuse first-party user-scope"]
W1 --> SKIP
W2 --> SKIP
H -->|"project scope or dep user-scope"| I{"needs_trust = is_user or not is_first_party\ntrust_canvas?"}
I -->|"needs_trust and not trust_canvas"| W3["_emit_trust_block\ndiagnostics.warn bundles blocked"]
W3 --> SKIP
I -->|"trust satisfied"| J["for bundle in bundles\n_deploy_bundle()"]
J --> K["_validate_canvas_name\nregex + reserved Windows device names"]
K -->|"invalid"| SKIP2["return 'skipped'"]
K -->|"valid"| L["_plan_bundle_files\nbundle.rglob + ensure_path_within"]
L -->|"PathTraversalError"| SKIP2
L -->|"planned triples"| M{"non-file exists at dest?"}
M -->|"yes"| SKIP2
M -->|"no"| N{"unmanaged collision?\nnot in managed_files and not force"}
N -->|"yes"| SKIP2
N -->|"no"| O{"all files byte-identical?\nis_content_identical_to_source"}
O -->|"yes"| ADOPT["return 'adopted'\ntarget_paths populated"]
O -->|"no"| COPY["per-file: dest.parent.mkdir\nTOCTOU symlink check\nshutil.copyfile"]
COPY -->|"symlink detected mid-loop"| SKIP2
COPY -->|"all files written"| INT["return 'integrated'"]
INT --> RESULT["IntegrationResult\ncounter_key='canvases'"]
ADOPT --> RESULT
SKIP2 --> RESULT
sequenceDiagram
participant User
participant CLI as commands/install.py
participant Services as install/services.py
participant Canvas as CanvasIntegrator
participant Exp as core/experimental.py
participant FS as Filesystem
User->>CLI: apm install --trust-canvas-extensions
CLI->>Services: integrate_package_primitives(trust_canvas=True, is_first_party=False)
Services->>Canvas: integrate_canvases_for_target(target, trust_canvas=True, is_first_party=False)
Canvas->>Exp: is_enabled("canvas")
Exp-->>Canvas: True
Canvas->>FS: find_canvas_bundles(.apm/extensions/)
FS-->>Canvas: [dep_bundle]
Note over Canvas: needs_trust = True (dep, not first-party)<br/>trust_canvas = True -- gate passes
Canvas->>Canvas: _deploy_bundle(dep_bundle, extensions_dir)
Canvas->>Canvas: _plan_bundle_files() -> [(src, dest, rel)]
Canvas->>FS: shutil.copyfile src->dest per file
FS-->>Canvas: files written
Canvas-->>Services: IntegrationResult(files_integrated=1)
Services-->>CLI: canvases count recorded (per-package log)
CLI-->>User: canvas extension deployed to .github/extensions/
Recommendation
The canvas primitive is architecturally sound and strategically important, but it cannot merge with a confirmed trust gate bypass on macOS and Windows. The required rework is minimal: one line in canvas_integrator.py:91 (seg.lower() != 'extensions') plus a matching guard in integrate_local_bundle, verified against a capital-E Extensions/ fixture tarball. Once that fix lands, the dead link on canvas.md:25 and the CHANGELOG PR number mismatch should be corrected in the same commit to prevent a broken day-one doc experience. After those three changes, the PR is ready to merge. All remaining panel findings -- integration test gap for the deploy pipeline, _canvas_deploy_prefixes coverage, silent flag-off skip, help text length, primitive table omission in what-is-apm.md -- are tracked as post-merge follow-ups and are not merge blockers given the feature's explicit experimental gating.
Full per-persona findings
Python Architect
-
[recommended] canvas delta is never accumulated into ctx.total_canvases_integrated; finalize phase emits no verbose canvas count at
src/apm_cli/install/phases/integrate.py:594
phases/integrate.py accumulates total_hooks_integrated, total_commands_integrated, and others from per-package deltas, but the equivalent accumulation for 'canvases' is absent; neither the dep loop nor the root-project block calls deltas.get('canvases', 0), and ctx.total_canvases_integrated does not exist in context.py; finalize.py emits no verbose canvas count line.
Suggested: Add total_canvases_integrated: int = 0 to InstallContext (context.py). Accumulate deltas.get('canvases', 0) at integrate.py:594 and :609. Write back at :625. Add a verbose branch in finalize.py alongside hooks and commands messages. -
[recommended] _deploy_bundle TOCTOU guard fires mid-copy loop, leaving partial bundle on disk while reporting 'skipped' at
src/apm_cli/integration/canvas_integrator.py:399
The docstring states "the bundle is treated as a unit ... so a half-new/half-old executable extension is never produced." However, the TOCTOU symlink check (line 404) fires inside the per-file copy loop. If file N-1 has been written via shutil.copyfile and appended to target_paths, and file N then triggers the symlink guard, the function returns 'skipped' but those N-1 paths ARE in target_paths and enter the lockfile's deployed_files. The bundle counter says skipped while some files are deployed and lockfile-tracked.
Suggested: Track files_written inside the copy loop. On symlink detection, unlink each path in files_written before returning 'skipped'. Alternatively, copy to a temp directory inside extensions_dir and do an atomic rename of the whole canvas_root at the end. -
[recommended] Dispatch table key 'canvas' is singular; all other primitive keys are plural at
src/apm_cli/integration/dispatch.py:72
get_dispatch_table() returns keys: 'prompts', 'agents', 'commands', 'instructions', 'hooks', 'skills' (all plural) and 'canvas' (singular). The singular outlier creates a latent naming hazard for any code that iterates or generates the primitive name list.
Suggested: Rename dispatch key from 'canvas' to 'canvases' and update all callers: target.primitives dict in targets.py, _INTEGRATOR_KWARGS in services.py, string comparisons in services.py lines 161 and 377. -
[nit] _plan_bundle_files passes empty string instead of package_name to _warn on PathTraversalError at
src/apm_cli/integration/canvas_integrator.py:447
Suggested: Replace '' with package_name in the _warn call. -
[nit] _copilot_home_is_nondefault imports os inside the method body; os is always available at
src/apm_cli/integration/canvas_integrator.py:491
Suggested: Move 'import os' to the top-level imports block. -
[nit] _deploy_bundle lacks a return type annotation; the string-literal protocol is implicit at
src/apm_cli/integration/canvas_integrator.py:310
Suggested: Add 'from typing import Literal' and annotate as -> Literal['integrated', 'adopted', 'skipped'].
CLI Logging Expert
-
[recommended] install.py --trust-canvas-extensions help text is 290 chars and wraps to 7 lines in apm install --help at
src/apm_cli/commands/install.py
Click renders --help at 80 cols; the --trust-canvas-extensions option column consumes ~32 chars, leaving ~48 chars per help line. At 290 chars the text wraps to 7 lines, dwarfing every other flag including the peer --trust-transitive-mcp (89 chars, 2 lines). The pack.py sibling option gets this right at 70 chars.
Suggested: Replace the 290-char help with: '[experimental] Deploy canvas extensions from dependencies. Canvas extensions are executable Node code and are blocked by default. Requires the canvas experimental feature.' (~175 chars, 4 lines). -
[recommended] pack.py canvas_blocked warning duplicates a 67-char prefix across two if/else branches at
src/apm_cli/commands/pack.py
Both branches open with 'Blocked {N} canvas extension file(s): canvas extensions are ...' before diverging. A future edit to the shared prefix must be applied twice.
Suggested: Extract to a single logger.warning call with a conditional suffix string. -
[recommended] audit.py _render_canvas_note emits [i] symbol on every list continuation line at
src/apm_cli/commands/audit.py
logger.info(f' {root}', symbol='info') produces '[i] path/to/ext' for each item. With a header already bearing [i], every sub-item repeating the symbol is visual noise. Additionally, symbol='info' is the default for logger.info() so the explicit kwarg is redundant.
Suggested: Replace logger.info(f' {root}', symbol='info') with logger.tree_item(f' {root}') or logger.verbose_detail(f' {root}') for sub-items. -
[recommended] _emit_integration_hints fires canvas reload hint on adopt-only reinstalls, producing misleading guidance at
src/apm_cli/install/services.py
The condition is (info['files'] > 0 or info['adopted'] > 0). When info['files'] == 0 and info['adopted'] > 0, no new canvas code was deployed -- the canvas was already present and is already loaded in any active session. Emitting the reload hint on a pure-adopt run misleads the user into an unnecessary session restart.
Suggested: Change condition to: if prim_name == 'canvas' and info['files'] > 0 -- emit reload hint only when at least one canvas file was freshly written. -
[nit] _emit_trust_block interpolates CANVAS_MARKER ('extension.mjs') into user-facing prose at
src/apm_cli/integration/canvas_integrator.py
The diagnostic message reads 'canvas extensions are executable extension.mjs code'. The marker filename is an implementation detail. The pack.py warning correctly uses 'executable code' without the filename.
Suggested: Replace f'executable {CANVAS_MARKER} code' with 'executable Node.js code' in the diagnostics.warn() call. -
[nit] audit.py _render_canvas_note uses extension(s) plural hack instead of explicit singular/plural at
src/apm_cli/commands/audit.py
Suggested: noun = 'extension' if len(bundles) == 1 else 'extensions'; use f'{len(bundles)} executable canvas {noun} deployed (experimental, trust-gated):'.
DevX UX Expert
-
[recommended] Silent skip when feature flag is off gives canvas authors zero install signal at
src/apm_cli/integration/canvas_integrator.py
When is_enabled('canvas') is False the integrator returns an empty IntegrationResult with no warning, no note, and no skip-count contribution. A developer who authors .apm/extensions/my-canvas/extension.mjs and runs 'apm install --target copilot' without the experimental flag sees the install succeed with 0 canvases and no explanation. Compare: the trust-gate path correctly emits a named diagnostic with the opt-in flag.
Suggested: After the bundles-empty check and before the is_enabled guard return, emit a once-per-install diagnostics.info noting the count of found-but-skipped bundles and the enable command. Suppress if bundles is empty. -
[recommended] 'canvas' flag missing from docs/src/content/docs/reference/cli/experimental.md Available flags table
The 'canvas' flag is registered in experimental.py and appears in the long-form reference page, but is absent from the CLI command reference table. When a user runs 'apm experimental list' they see 'canvas'; when they consult the CLI doc they find nothing.
Suggested: Add a row to the Available flags table for 'canvas' with a one-line description and 'apm install --target copilot' as the usage example. -
[recommended] Trust scope asymmetry between --trust-canvas-extensions and --trust-transitive-mcp is undocumented in flag help at
src/apm_cli/commands/install.py
--trust-transitive-mcp gates only transitive (indirect) dependencies; direct dependencies are implicitly trusted. --trust-canvas-extensions gates ALL dependency canvases including direct dependencies. A developer who adds owner/canvas-pkg directly to apm.yml will be surprised when their explicitly-declared dependency's canvas is blocked.
Suggested: Revise help to: 'Deploy executable canvas extensions shipped by dependencies, including directly declared ones. Unlike --trust-transitive-mcp, this flag is required even for direct dependencies. First-party (root package) canvases deploy without it. Requires the canvas experimental feature.' -
[nit] --trust-canvas-extensions help text is four sentences vs --trust-transitive-mcp single line at
src/apm_cli/commands/install.py -
[nit] Session reload hint says 'restart' which is ambiguous; docs use 'exit and relaunch' at
src/apm_cli/install/services.py
Suggested: Change to: '|-- reload the Copilot session (/clear) or exit and relaunch Copilot CLI to load the canvas' -
[nit] apm audit canvas note does not disclose that canvas drift is not yet checked at
src/apm_cli/commands/audit.py
Suggested: After the bundle list, append a note that canvas drift detection is not yet implemented.
Supply Chain Security Expert
-
[blocking] Case-insensitive FS trust gate bypass via mixed-case Extensions/ directory in offline bundles at
src/apm_cli/integration/canvas_integrator.py:91
is_canvas_bundle_path() uses a literal string comparison: seg != 'extensions'. On macOS (HFS+) and Windows (NTFS), 'Extensions' and 'extensions' resolve to the same filesystem directory. A bundle crafted with path 'Extensions/myext/extension.mjs' causes is_canvas_bundle_path() to return False, so the file bypasses the trust filter and the Copilot-only target guard, deploying to all targets' default roots without operator consent. This is a direct bypass of the primary trust gate for executable Node.js code on the two dominant desktop platforms.
Suggested: In is_canvas_bundle_path(), change 'if seg != "extensions":' to 'if seg.lower() != "extensions":'. In integrate_local_bundle() in services.py, change the extensions guard comparison similarly. Consider rejecting paths where 'extensions' segment is not exactly lowercase on case-sensitive systems. -
[recommended] TOCTOU window between dest.is_symlink() check and shutil.copyfile() in _deploy_bundle at
src/apm_cli/integration/canvas_integrator.py:404
Between the symlink check and the copyfile call, an attacker with concurrent write access to the deploy directory can insert a symlink, causing shutil.copyfile to write content to the symlink's target outside canvas_root.
Suggested: Replace the is_symlink()+copyfile() pair with an atomic open using os.open(dest, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o666) and write the source bytes to the returned fd. -
[recommended] Bare except in _canvas_extension_names silently suppresses all errors, blinding authors to executable content in their packages at
src/apm_cli/models/validation.py:156
If the import fails or find_canvas_bundles raises an OSError, the function returns an empty list. The caller does not set has_primitives via canvas_names and does not emit the executable-code warning. Authors running 'apm validate' would see no warning about the gated executable code.
Suggested: Replace 'except Exception: return []' with 'except Exception as exc: import logging; logging.getLogger(name).debug("Canvas scan failed: %s", exc); return []'. -
[nit] Partial bundle orphan on mid-loop symlink guard: written files not recorded in target_paths, leaving untracked executable assets at
src/apm_cli/integration/canvas_integrator.py:399
Suggested: Before the per-file loop, snapshot the set of files about to be written. On any mid-loop bail-out, delete already-written files from that bundle (rollback). -
[nit] _copilot_home_is_nondefault uses resolve(strict=False) without a comment explaining the intentional fail-closed behavior at
src/apm_cli/integration/canvas_integrator.py:496
Suggested: Add a comment: '# strict=False is intentional: a non-existent or unresolvable COPILOT_HOME compares != default, so we return True (nondefault) and block global canvas deploy. Fail-closed behavior.'
OSS Growth Hacker
-
[recommended] The create-canvas to apm install end-to-end workflow is invoked but never demonstrated in docs
canvas.md says 'Canvases are typically produced by the create-canvas skill' but this is the single reference to create-canvas across the entire docs corpus. There is no link to the skill, no example extension.mjs, and no runnable sequence. This is the primary conversion story for the feature.
Suggested: Add a 'Full workflow' section to canvas.md with a minimal extension.mjs snippet, the create-canvas skill link, and the exact four-command sequence: scaffold, enable flag, install, reload Copilot. -
[recommended] what-is-apm.md primitive table omits canvas, hiding the capability from new users
docs/src/content/docs/concepts/what-is-apm.md lists eight rows: instructions, skills, prompts, agents, hooks, commands, plugins, MCP servers. Canvas is absent. This page is the canonical "What APM manages" reference.
Suggested: Add a 'Canvas extensions (experimental, Copilot-only)' row to the what-is-apm.md primitive table with a one-line description and a link to integrations/canvas.md. -
[recommended] No active discoverability path for installed canvases creates a silent failure mode for early adopters
canvas.md states 'Canvases are not yet shown by apm list/apm compile' but does not tell the user how to verify a canvas deployed successfully. Early adopters who encounter issues have no self-service diagnostic path documented.
Suggested: Add a 'Verify deployment' subsection to canvas.md listing three steps: check ls .github/extensions/, run apm audit to see the canvas note, reload the Copilot session. -
[recommended] CHANGELOG entry is maintainer-facing; the feature milestone has no shareable narrative hook
The CHANGELOG reads as implementation detail. This is the first time APM can distribute executable artifacts -- a real positioning moment. The entry should have a user-benefit headline sentence.
Suggested: Prepend a user-benefit headline: 'APM can now distribute GitHub Copilot CLI canvas extensions: author a canvas with create-canvas, ship it in an APM package, and consumers get it in their Copilot session with apm install and zero manual file copying.' -
[nit] Double-experimental signaling in canvas.md discourages early adopters most likely to become advocates (sidebar badge AND caution callout)
-
[nit] Community canvas contribution path is undocumented; producer ramp ends at first-party use
Auth Expert -- inactive
No auth surface touched. trust_canvas flag is a local-deployment feature flag with no bearing on authentication, credential resolution, or network authorization. Auth-expert activation was a false positive triggered by pipeline.py membership in the changed-file set.
Doc Writer
-
[recommended] create-canvas skill reference is a dead link to a non-existent artifact at
docs/src/content/docs/integrations/canvas.md:25
A search across all of src/, packages/, and .apm/ finds no skill named create-canvas. Under PROSE Safety Boundaries, planned features must use the :::note[Planned] callout or not be mentioned at all.
Suggested: Remove the sentence, or replace it with: ':::note[Planned] A create-canvas skill for scaffolding canvas bundles is planned but not yet available. :::' -
[recommended] Silent no-op when canvas flag is off is not disclosed; users will be confused when extension does not deploy at
docs/src/content/docs/integrations/canvas.md
canvas.md says 'With the flag off, .apm/extensions/ is ignored entirely.' but omits that the ignore is completely silent: no warning, no note, no skip-count. The trust-blocked case correctly emits a diagnostic; the feature-gate should disclose the silence.
Suggested: Add after the quoted sentence: 'No warning or diagnostic is emitted; canvas content is silently skipped. If an installed package ships a canvas that is not appearing in your Copilot session, verify the flag is on with apm experimental list.' -
[recommended] canvas.md limitations claim 'deployed at install only' but apm audit surfaces deployed canvas bundles -- inaccurate at
docs/src/content/docs/integrations/canvas.md:137
CHANGELOG line 23 and the source confirm: audit.py _render_canvas_note() reads apm.lock.yaml and emits an informational list of deployed canvas bundles in text format. The phrase 'deployed at install only' misrepresents the current state.
Suggested: Replace the 'No compile/list surfacing yet' bullet with: 'Not shown by apm list or apm compile. apm audit (text format) lists deployed canvas bundles recorded in the lockfile; this is the current discovery path until list/compile support ships.' -
[recommended] audit.md not updated despite apm audit gaining canvas-bundle surfacing in this PR at
docs/src/content/docs/reference/cli/audit.md
_render_canvas_note() is called in the text-format path of audit.py unconditionally. The audit.md reference page was not touched in this PR.
Suggested: Add a bullet to the Behavior section: 'Canvas note. When canvas experimental flag is on and canvas bundles are recorded in the project lockfile, the text-format output includes an informational list of deployed canvas bundle roots. Not shown in --format json, sarif, or markdown.' -
[nit] CHANGELOG second canvas entry attributes global install to Experimental canvas: support
--globalinstall to~/.copilot/extensions/(user scope) #1691 but the code ships in this PR (feat(install): experimental Copilot canvas extensions #1689) atCHANGELOG.md
Suggested: Verify whether global canvas install is intended to land in feat(install): experimental Copilot canvas extensions #1689 or Experimental canvas: support--globalinstall to~/.copilot/extensions/(user scope) #1691 and correct the attribution accordingly. -
[nit] Session restart hint 'exit and relaunch' is ambiguous for VS Code Copilot surface at
docs/src/content/docs/integrations/canvas.md
Suggested: Clarify hint to distinguish terminal CLI vs VS Code Copilot Chat context. -
[nit] Trust gate section re-explains content already in install.md flag table; cross-reference would be leaner at
docs/src/content/docs/integrations/canvas.md -
[nit] security.md canvas parenthetical makes the 'No arbitrary code execution' bullet harder to scan at
docs/src/content/docs/enterprise/security.md
Suggested: Break the canvas exception into a nested sub-bullet rather than an inline parenthetical.
Test Coverage Expert
-
[recommended] Canvas install pipeline is covered at unit tier only; install-pipeline floor requires integration-with-fixtures evidence
TestLocalBundleCanvasTrust::test_canvas_deployed_with_trust_and_flag uses click.testing.CliRunner (unit tier) and lives in tests/unit/install/. Zero hits for canvas tests in tests/integration/. The APM install-pipeline surface floor requires integration-with-fixtures evidence.
Proof (missing at integration-with-fixtures):tests/integration/test_canvas_install_e2e.py::test_canvas_deployed_via_cli_with_trust_and_flag-- proves: apm install --trust-canvas-extensions invoked as a real CLI process deploys canvas and exits 0 [secure-by-default, devx] -
[recommended] _canvas_deploy_prefixes in drift.py is new code with zero test coverage; regression would emit false drift on every apm install at
src/apm_cli/install/drift.py:589
grep confirmed zero hits for _canvas_deploy_prefixes in tests/. All calls to diff_scratch_against_project in integration tests pass targets=[], so the canvas exclusion branch is never reached.
Proof (missing):tests/integration/test_deps_models_phase3c.py::test_diff_scratch_canvas_paths_excluded_from_drift-- proves: apm install does not flag deployed canvas extension paths as orphaned drift [devx] -
[recommended] Canvas file paths not asserted in apm.lock.yaml after install; lockfile round-trip for canvas uninstall is unverified at
tests/unit/install/test_install_local_bundle.py
test_canvas_deployed_with_trust_and_flag asserts the canvas file exists on disk but does not assert it appears under deployed_files in the written apm.lock.yaml. Without a lockfile assertion, the apm uninstall pruning path is not proven end-to-end.
Proof (missing):tests/unit/install/test_install_local_bundle.py::TestLocalBundleCanvasTrust::test_canvas_paths_recorded_in_lockfile-- proves: apm install --trust-canvas-extensions writes canvas file paths to apm.lock.yaml so that apm uninstall can read and remove them [devx]
Performance Expert
-
[recommended] bundle.rglob("") in _plan_bundle_files has no file count ceiling at
src/apm_cli/integration/canvas_integrator.py:432
sorted(bundle.rglob("")) materializes ALL files in the bundle as a list before any processing. A canvas bundle that accidentally includes node_modules or a vite build cache could contain 10,000+ entries. 20,000+ syscalls at ~1ms each on a network share = 20 seconds of blocking install time per bundle. There is no early-exit on count before or inside the loop.
Suggested: After the sorted() call, check len(planned) > _MAX_CANVAS_FILES (e.g., 100) and return None (skip the bundle) if exceeded. Alternatively, enumerate with an early-exit inside the rglob loop. -
[recommended] Adopt-detect pass reads full file content without a size pre-screen at
src/apm_cli/integration/canvas_integrator.py:394
is_content_identical_to_source reads the entire file into memory via an os.read loop; there is no stat().st_size comparison before the read. For a canvas bundle where extension.mjs is a minified webpack bundle (500KB to 2MB is common), a warm re-install with no changes reads 2*bundle_total_bytes per file. For a 10-file bundle at 500KB average: 10MB of read I/O per re-install just for adopt detection.
Suggested: In is_content_identical_to_source (base_integrator.py), add a stat().st_size comparison as a pre-check before calling _read_bytes_no_follow. A size mismatch at ~50ns eliminates the full read for differing-size files. -
[nit] canvas_root.resolve() called O(N_files) times in _plan_bundle_files via ensure_path_within at
src/apm_cli/integration/canvas_integrator.py:442
canvas_root is constant across all files in a bundle, so canvas_root.resolve() executes N times redundantly.
Suggested: Resolve canvas_root once before the loop and pass the pre-resolved base into the containment check. -
[nit] Drift exclusion filter is O(n*m) but m is capped at 1 in practice (canvas mapped to copilot target only); no action needed now at
src/apm_cli/install/drift.py
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1689 · sonnet46 25.5M · ◷
Resolve 6 conflicts: CHANGELOG (keep canvas + ZIP/auth entries), astro.config (keep canvas + hermes sidebar items), primitives table (add Kiro column + canvas row), commands.md (merge Kiro/Hermes flags with --trust-canvas-extensions), package-authoring (keep canvas + sourceBase sections), test_unpacker (keep canvas trust + ZIP security tests). Refactor _deployed_path_entry to extract _try_dynamic_root helper, eliminating duplicated loop and keeping services.py under 1000 LOC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
is_canvas_bundle_path() used literal 'extensions' comparison, allowing a tarball with 'Extensions/myext/extension.mjs' (capital-E) to bypass the trust gate on macOS HFS+ and Windows NTFS. Apply seg.lower() in both canvas_integrator.py and the matching guard in services.py. Also fix dead create-canvas link on canvas.md and correct CHANGELOG global-install attribution from #1691 to #1689. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The refactor that extracted _try_dynamic_root from the inline loop inadvertently added a relative_to pre-check on the fallback path, preventing to_lockfile_path from running its own security validation. Add strict=True parameter to skip the pre-check on fallback, matching the original main branch behaviour where PathTraversalError propagates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
feat(install): experimental Copilot canvas extensions
TL;DR
A GitHub Copilot CLI canvas (a directory with an executable
extension.mjs, produced by thecreate-canvasskill) had no way to ship through APM — there was no primitive, no integrator, and no target mapping carrying it from a package into a consumer's.github/extensions/. This PR adds an experimental, Copilot-onlycanvasprimitive so a package can declare.apm/extensions/<name>/and haveapm install/apm packdeploy it verbatim to.github/extensions/<name>/, where Copilot CLI discovers it in-session. The surface is protected by two independent gates — an experimental feature flag and a dependency-trust opt-in for executable code — and is fail-closed on every path that materialises canvas files. It also supportsapm install --global, deploying a dependency-provided canvas to~/.copilot/extensions/<name>/so it is available in every Copilot session (user scope always requires--trust-canvas-extensions).Note
Experimental and opt-in twice over: nothing deploys unless the operator runs
apm experimental enable canvas, and a dependency's canvas additionally requires--trust-canvas-extensions. No existing install behaviour changes when the flag is off.Problem (WHY)
create-canvasskill emits a Copilot CLI extension, but APM has no canvas/extension primitive — instructions, prompts, agents, skills, hooks, commands, plugins, and MCP are the only known types. There is nowhere for a canvas to live or be routed..github/extensions/<name>/(project) and~/.copilot/extensions/<name>/(user). Without an integrator that targets those paths, a packaged canvas is never available in the session.extension.mjsis arbitrary executable Node code. Shipping one transitively via a dependency is a code-execution surface that must not deploy silently.Why these matter: an experimental flag alone is a feature-availability switch, not a security control — the experimental subsystem states flags "MUST NOT gate security-critical behaviour". Executable-code trust therefore needs its own gate, mirroring the existing
--trust-transitive-mcpboundary.Approach (WHAT)
canvasexperimental flag (feature availability; default off).--trust-canvas-extensionsinstall flag plumbed end-to-end (request → context → integrate phase), mirroringtrust_transitive_mcp.CanvasIntegrator(Copilot-only) with atomic per-bundle deploy, strict name validation, and containment checks.canvasroutes only on thecopilottarget. Project scope deploys to.github/extensions/;--globaldeploys a dependency-provided canvas to~/.copilot/extensions/. Non-Copilot targets stay hard-unsupported.apm install <bundle>andapm unpackpaths, so a vendored bundle cannot smuggle executable canvas code.Implementation (HOW)
src/apm_cli/integration/canvas_integrator.py(new) —CanvasIntegratorscans.apm/extensions/*/extension.mjs, validates each name, applies the trust gate for dependency sources, and deploys each bundle atomically (plan → directory-collision guard → unmanaged-collision skip → adopt-if-byte-identical → copy). At user scope it deploys only dependency-provided canvases, blocks first-party root bundles, and refuses a non-default$COPILOT_HOME.sync_for_targetis deliberately not flag-gated so uninstall always removes a previously-deployed canvas (project or user scope). Also exportsis_canvas_bundle_pathshared with the offline paths.src/apm_cli/integration/targets.py— canvas is no longer in the copilotunsupported_user_primitivesset, sofor_scope(user_scope=True)keeps the mapping withroot_dir=".copilot"andresolved_deploy_root=None, yielding a clean home-relative lockfile path (nocowork://scheme) that the user-scope uninstall can prune.src/apm_cli/install/services.py— dispatch threadstrust_canvasand an explicitis_first_partyinto the canvas call;integrate_local_contentpassesis_first_party=True; dependency/template/drift callers default toFalse.integrate_local_bundleenforces both gates and routesextensions/only to Copilot.src/apm_cli/bundle/unpacker.py—unpack_bundlegainstrust_canvasand acanvas_blockedcount; drops canvas paths unless both gates hold (fail closed).src/apm_cli/bundle/plugin_exporter.py— collectsextensions/into the pack soapm packround-trips a canvas (files stay inert until a consumer gates them).src/apm_cli/models/validation.py—_canvas_extension_nameshelper recognises canvas bundles so canvas-only packages are valid; a gated-executable warning is emitted for any package shipping canvas extensions.src/apm_cli/commands/audit.py—_deployed_canvas_bundles+_render_canvas_notelist deployed canvas extension bundles as an informational note in text-format audit output.core/experimental.py,commands/install.py,commands/pack.py,integration/dispatch.py,install/{context,request,pipeline,template,drift,phases/*}.py— flag registration, CLI flag, dispatch wiring, and pipeline plumbing.integrations/canvas.md, a matrix row inconcepts/primitives-and-targets.md, andapm-usageskill updates (authoring, governance, install flag).Diagrams
Legend: the two-gate decision every canvas deploy path runs; look first at the two diamonds — the experimental flag and the trust gate are independent, and a dependency must clear both.
flowchart TD A[Canvas found under extensions] --> B{canvas flag enabled?} B -->|no| S1[Skip silently: feature off] B -->|yes| C{first-party root or local?} C -->|yes| D[Deploy to .github/extensions] C -->|no, dependency| E{--trust-canvas-extensions set?} E -->|no| S2[Block: emit trust diagnostic] E -->|yes| D D --> F[Record deployed files in lockfile]Legend: the source-to-session paths across
install,--global, andpack; the project deploy target is.github/extensions/<name>/, the user-scope target is~/.copilot/extensions/<name>/, Copilot only.flowchart LR SRC[".apm/extensions/widget/extension.mjs"] -->|apm install| INT[CanvasIntegrator] SRC -->|apm pack| PACK[plugin_exporter] --> BUNDLE[bundle extensions tree] BUNDLE -->|apm install bundle| GATE[two-gate check] INT --> DEST[".github/extensions/widget/"] GATE --> DEST DEP["dependency canvas"] -->|apm install --global --trust-canvas-extensions| UDEST["~/.copilot/extensions/widget/"] DEST --> SESSION[Copilot CLI session after reload] UDEST --> SESSIONTrade-offs
--globaldeploys only dependency-provided canvases (the package +install --globalUX), which reuse the dependency-lockfile persistence and the user-scopesync_for_targetprune path. First-party root.apm/extensions/at user scope is blocked (the user-scope lockfile pipeline does not track it, so it would leak an executable bundle on uninstall), and a non-default$COPILOT_HOMEis refused (APM only honours the default~/.copilot). Full first-party /COPILOT_HOMEuser-scope support is a follow-up.Canvasdataclass /apm listsurfacing yet. Package validation recognises canvas extensions (canvas-only packages are valid; a gated-executable warning is emitted).apm audit(text format) lists deployed canvas bundles. Machine-readable audit formats (sarif/json/markdown) are unchanged.Benefits
create-canvasis now installable via APM with zero manual file copying, at project scope or globally via--global..github/extensions/or~/.copilot/extensions/without an explicit, named opt-in — fail-closed on the integrator, offline-bundle, and unpack paths alike, and user scope always requires trust._localsentinel bypass is closed); proven by a dedicated regression test.Validation
Full lint chain (all 7 checks) and the canvas + install suites are green; the
--globaladdition adds 5 user-scope tests, validated end-to-end with an isolatedHOME(install -> deploy, uninstall -> prune, no-trust -> block, non-defaultCOPILOT_HOME-> refuse). Validate/audit awareness adds 10 unit tests covering canvas-only package validity, gated-executable warning, bundle-root derivation, and render note.Scenario Evidence
tests/unit/integration/test_canvas_integrator.py::test_dispatch_dependency_named_local_is_not_first_partytests/unit/integration/test_canvas_integrator.py::test_dispatch_first_party_flag_deploys--trust-canvas-extensions+ flag deploys a dependency canvas to.github/extensions/tests/unit/install/test_install_local_bundle.py::TestLocalBundleCanvasTrust::test_canvas_deployed_with_trust_and_flagtests/unit/test_unpacker.py::TestUnpackCanvasTrust::test_canvas_blocked_when_trusted_but_flag_offapm packround-trips a canvas into the bundletests/unit/bundle/test_plugin_exporter_canvas.pyapm uninstallremoves a deployed canvastests/unit/integration/test_canvas_integrator.py::test_sync_only_removes_managed_subset--globaldeploys a trusted dependency canvas to~/.copilot/extensions/; first-party + non-default$COPILOT_HOMEare blockedtests/unit/integration/test_canvas_integrator.py::test_user_scope_dependency_deploys_with_trust,::test_user_scope_first_party_blocked,::test_user_scope_nondefault_copilot_home_blockedapm uninstall --globalprunes the user-scope canvastests/unit/integration/test_canvas_integrator.py::test_user_scope_sync_prunes_dependency_canvastests/unit/test_models_validation_rules.py::test_canvas_only_package_is_valid,::test_canvas_emits_gated_executable_warningapm audit(text) lists deployed canvas bundles as an informational notetests/unit/test_audit_scan_and_render.py::TestRenderCanvasNote::test_emits_info_when_bundles_foundHow to test
.apm/extensions/widget/extension.mjs, runapm installwith the flag off -- no.github/extensions/widget/is created (silent no-op).apm experimental enable canvas, thenapm install--.github/extensions/widget/extension.mjsappears and install hints to reload the Copilot session (/clear).apm install-- canvas is blocked with a trust diagnostic; re-run with--trust-canvas-extensions-- it deploys.apm pack, thenapm install <bundle>without trust -- canvas blocked; with--trust-canvas-extensionsand the flag on -- deploys.apm uninstallthe package --.github/extensions/widget/is removed.apm install <dep> --global --trust-canvas-extensions--~/.copilot/extensions/widget/appears;apm uninstall <dep> --globalremoves it. Without--trust-canvas-extensions, the global install is blocked..apm/extensions/widget/extension.mjs, validapm.yml, no other primitives) and run validation --is_valid=True, warning mentions--trust-canvas-extensions.apm audit-- text output lists the deployed canvas bundle(s) after the summary.Closes #1691
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com