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 4 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
…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"); |
There was a problem hiding this comment.
Good catch. Fixed in 437f916: locateCanvasEntryPoint now enumerates the extensions/ subtree with a single recursive git ls-tree -r -t -z and classifies the flat/nested entry points in memory, instead of spawning a git cat-file per immediate subfolder. Process count is now constant regardless of how many directories an untrusted repo packs under extensions/ — the 160-folder regression test dropped from ~10s to ~1.3s as a result.
Generated via Copilot (Claude Opus 4.8) on behalf of @tlmii
| `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); |
There was a problem hiding this comment.
Agreed — a root recursive fetch made verifiability depend on total repo size, so a large unrelated monorepo could deterministically return truncated: true and permanently block a valid plugin. Fixed in 437f916: intake now walks to the plugin's extensions directory one level at a time (resolveDirectoryTreeSha, bounded by path depth) to resolve its tree SHA, then fetches only that subtree recursively. A truncated result is now only possible for an implausibly large extensions directory itself, not the rest of the repo.
Generated via Copilot (Claude Opus 4.8) on behalf of @tlmii
Address reviewer feedback on unbounded scaling for untrusted/large repos: - Quality gate: replace the per-candidate-directory git cat-file spawns in locateCanvasEntryPoint with a single recursive git ls-tree over the extensions subtree, classifying flat/nested entry points in memory. Process count is now constant regardless of how many folders live under extensions/. - Intake: stop fetching the recursive git tree from the repo root (which a large unrelated monorepo can push past the Trees API truncation limit and never validate). Walk to the plugin's extensions directory one level at a time to resolve its tree SHA, then fetch only that subtree recursively, so verifiability depends on the plugin's own size, not the whole repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.