Accept nested extensions/<name>/extension.mjs in external-plugin canvas checks#2403
Accept nested extensions/<name>/extension.mjs in external-plugin canvas checks#2403tlmii wants to merge 3 commits into
Conversation
…as checks The external-plugin canvas structure check (quality gate) and intake validation both hardcoded a flat extensions/extension.mjs entry point, falsely rejecting the documented nested extensions/<name>/extension.mjs layout that installs and runs fine. Scan the extensions/ directory for a nested subfolder containing extension.mjs while still accepting the flat form for backward compatibility. Applied to both runCanvasStructureGate (git-object lookups) and validateCanvasPluginMetadata (Contents API), keeping them behaviorally aligned. Added regression coverage for both paths. Fixes #2402 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address multi-model review findings on the nested canvas extension fix: - Quality gate: enumerate extensions/ via 'git ls-tree -z' with spawnSync (NUL-delimited, untruncated) so large directories no longer drop the real entry past the 12KB output cap. - Intake: decouple the flat extensions/extension.mjs check from the directory listing, require an array listing (Array.isArray) before treating it as a directory, and surface an unverifiable (warning) result instead of a false rejection when the listing or a nested lookup hits a transient API error. - Add regression tests: nested entry beyond the legacy output cap (gate) and unverifiable/flat-still-accepted paths when the listing errors (intake). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates external-plugin canvas validation to support nested extension entry points.
Changes:
- Supports flat and nested
extension.mjslayouts. - Handles lookup failures as unverifiable.
- Adds regression tests for both validation paths.
Show a summary per file
| File | Description |
|---|---|
eng/external-plugin-quality-gates.mjs |
Adds untruncated nested entry-point discovery. |
eng/external-plugin-quality-gates.test.mjs |
Tests nested and large directory layouts. |
eng/external-plugin-intake.mjs |
Adds API-based nested entry-point discovery. |
eng/external-plugin-intake.test.mjs |
Tests intake acceptance and failure handling. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Medium
| } else if (Array.isArray(extensionContainerResponse.data)) { | ||
| extensionDirEntries = extensionContainerResponse.data; | ||
| canEnumerateExtensions = true; |
There was a problem hiding this comment.
Good catch — this is the same truncation class the git-based gate avoids. Reworked intake to fetch the release tree once via a recursive git/trees/<locator>?recursive=1 call and inspect the extensions/*/extension.mjs paths locally, so the full extensions/ subtree comes back in a single response rather than the Contents API's 1,000-entry-capped listing. When the tree returns truncated: true and no entry point is found among the returned entries, intake now reports it as unverifiable (a warning to re-run intake) instead of falsely rejecting. Fixed in 43032be.
Generated via Copilot (Claude Opus 4.8) on behalf of @tlmii
| for (const entry of entries) { | ||
| if (entry?.type !== "dir" || !entry?.name) { | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
Done — replaced the per-subdirectory Contents API loop with a single recursive git/trees/<locator>?recursive=1 fetch, then match the immediate extensions/*/extension.mjs paths in memory. It's now one request regardless of how many extension folders the repository has, so there's no per-folder latency or rate-limit pressure. Fixed in 43032be.
Generated via Copilot (Claude Opus 4.8) on behalf of @tlmii
…intake Address PR review: the Contents API caps directory listings at 1,000 entries and required one request per extension subfolder, so a nested entry beyond the cap could be falsely rejected (the same truncation class the git gate avoids) and large repos risked latency / rate-limit exhaustion. Replace the per-subfolder Contents API enumeration with one recursive 'git/trees/<locator>?recursive=1' fetch and inspect 'extensions/extension.mjs' and immediate 'extensions/<name>/extension.mjs' paths locally. A truncated tree without a located entry point is reported as unverifiable (warning) rather than rejected, and refs are normalized so 'refs/tags/<tag>' resolves as a tree-ish. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| } | ||
|
|
||
| const nestedEntryPoint = toPosixPath(extensionsDir, entry.name, "extension.mjs"); | ||
| const nestedCheck = checkPathExistsAtLocator(repoDir, locator, nestedEntryPoint, "blob"); |
| `submission: plugins tagged with "canvas" must include "extensions/extension.mjs" at ${releaseLocatorDescription}`, | ||
| ); | ||
| } else if (extensionEntryResponse.kind === "apiError") { | ||
| const releaseTreeResponse = await fetchGitHubTree(repo, normalizeTreeish(releaseLocator), token); |
Fixes #2402.
Problem
The external-plugin canvas-structure check required the canvas extension entry point to be a flat file at
<pluginRoot>/extensions/extension.mjs. But the endorsed convention (AGENTS.md → Canvas Extensions) nests each extension in its own subfolder:<pluginRoot>/extensions/<name>/extension.mjs. Plugins following the nested convention — which install and run fine (e.g. upgrade-agent in #2394, whose install smoke test passes) — were falsely reported as "missing required canvas extension entry point."This repo's own discovery helpers (
eng/generate-marketplace.mjs,eng/generate-website-data.mjs) already probe the nested path; only the two external-plugin checks disagreed.Fix
Both checks now locate the canvas entry point by accepting either the flat
extensions/extension.mjsor at least one immediate subfolder containingextension.mjs, kept behaviorally aligned:eng/external-plugin-quality-gates.mjs, git-object based): newlocateCanvasEntryPointchecks the flat blob first, then enumeratesextensions/children viagit ls-treeand probes each<name>/extension.mjs.eng/external-plugin-intake.mjs, Contents-API based): newlocateCanvasEntryPointViaApichecks the flat path, then walks theextensions/directory listing for a subdir withextension.mjs.A canvas plugin whose
extensions/contains noextension.mjsanywhere (flat or nested) still fails.eng/external-plugin-validation.mjsis unchanged.Adversarial review hardening
A multi-model review surfaced three issues, addressed here:
git ls-tree -zviaspawnSync(NUL-delimited, untruncated) instead of the 12 KB-cappedrunCommand, so largeextensions/directories no longer drop the real entry.Array.isArray) before it is treated as a directory; otherwise a "must be a directory" error is raised.Tests
eng/external-plugin-quality-gates.test.mjs: nested-subfolder pass, no-entry-anywhere fail, and a nested entry listed past the legacy output cap.eng/external-plugin-intake.test.mjs(new): nested accept, flat accept, no-entry reject, unverifiable-on-listing-error warning, and flat-still-accepted when the listing errors.All 11 tests pass; the new nested tests fail on the pre-fix code.
npm run build,npm run plugin:validate, andnpm run skill:validateall pass with no generated-file drift.