Fix external canvas extension validation (#2928)

* fix(intake): accept nested canvas extension entry points

Allow canvas entry points at any depth under the com.github.copilot namespace, including materialized extensions/<name> layouts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3eb2554c-9e4a-408c-92f5-36306c99d33b

* fix(intake): require named canvas extension directories

Require canvas entry points at com.github.copilot/<extension-name>/extension.mjs and reject flat or deeper layouts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3eb2554c-9e4a-408c-92f5-36306c99d33b

* fix: align external canvas plugin layout

Require canvas extensions under com.github.copilot/extensions/<extension>/extension.mjs, matching github-app discovery and reject legacy layouts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3eb2554c-9e4a-408c-92f5-36306c99d33b

* test: cover canvas logo validation, fix directory-mismatch message

Add intake regression tests for a missing/malformed com.github.copilot namespace and an incorrect logo path. Fix the quality gate to report a file-vs-directory mismatch instead of a generic missing-entry-point message when extension.mjs exists but is a directory.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3eb2554c-9e4a-408c-92f5-36306c99d33b

---------

Copilot-Session: 3eb2554c-9e4a-408c-92f5-36306c99d33b
This commit is contained in:
Aaron Powell
2026-09-03 19:14:23 +10:00
committed by GitHub
co-authored by Copilot App
parent 17cd9ee68d
commit 2ba72cd142
4 changed files with 211 additions and 131 deletions
+17 -22
View File
@@ -25,6 +25,8 @@ const AGENT_PLUGIN_ALLOWED_TOP_LEVEL_FIELDS = new Set([
]);
const AGENT_PLUGIN_NAME_PATTERN = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
const EXTERNAL_CANVAS_KEYWORD = "canvas";
const COPILOT_CLIENT_NAMESPACE = "com.github.copilot";
const COPILOT_EXTENSIONS_DIRECTORY = "extensions";
const INFRA_ERROR_PATTERNS = [
/\b401\b/,
@@ -804,33 +806,24 @@ function locateCanvasEntryPoint(repoDir, readRef, locator, extensionsDir) {
return { entryPoint: null, output: listing.output };
}
let flatIsBlob = false;
let flatIsTree = false;
let nestedEntryPoint = null;
let nestedEntryIsNotFile = false;
for (const entry of listing.entries) {
if (entry.name === "extension.mjs") {
if (entry.type === "blob") {
flatIsBlob = true;
} else if (entry.type === "tree") {
flatIsTree = true;
}
continue;
}
const segments = entry.name.split("/");
if (segments.length === 2 && segments[1] === "extension.mjs" && entry.type === "blob" && !nestedEntryPoint) {
nestedEntryPoint = toPosixPath(extensionsDir, segments[0], "extension.mjs");
if (segments.length === 2 && segments[1] === "extension.mjs" && !nestedEntryPoint) {
if (entry.type === "blob") {
nestedEntryPoint = toPosixPath(extensionsDir, segments[0], "extension.mjs");
} else {
nestedEntryIsNotFile = true;
}
}
}
if (flatIsBlob) {
return { entryPoint: toPosixPath(extensionsDir, "extension.mjs"), output: "" };
}
if (nestedEntryPoint) {
return { entryPoint: nestedEntryPoint, output: "" };
}
return { entryPoint: null, output: "", flatKindMismatch: flatIsTree };
return { entryPoint: null, output: "", kindMismatch: nestedEntryIsNotFile };
}
export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) {
@@ -854,8 +847,8 @@ export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) {
};
}
const extensionsDir = toPosixPath(normalizedPluginPath, "extensions");
const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs");
const namespaceDir = toPosixPath(normalizedPluginPath, COPILOT_CLIENT_NAMESPACE);
const extensionsDir = toPosixPath(namespaceDir, COPILOT_EXTENSIONS_DIRECTORY);
let hasFailure = false;
let hasInfraError = false;
@@ -901,11 +894,13 @@ export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) {
}
if (!extensionEntryCheck.entryPoint) {
hasFailure = true;
if (extensionEntryCheck.flatKindMismatch) {
messages.push(`- ${locator}: "${extensionEntryPoint}" must be a file.`);
if (extensionEntryCheck.kindMismatch) {
messages.push(
`- ${locator}: "${extensionsDir}/<extension>/extension.mjs" must be a file.`,
);
} else {
messages.push(
`- ${locator}: missing required canvas extension entry point "${extensionEntryPoint}" (or a nested "${extensionsDir}/<extension>/extension.mjs").`,
`- ${locator}: missing required canvas extension entry point "${extensionsDir}/<extension>/extension.mjs".`,
);
}
continue;