Migrate plugins and canvas extensions to Agent Plugins spec - #2546
Migrate plugins and canvas extensions to Agent Plugins spec#2546aaronpowell wants to merge 8 commits into
Conversation
- Add \ to all 69 curated plugin manifests - Migrate all 18 extension manifests: add \, move logo into �xtensions.com.github.copilot.logo namespace, remove top-level logo and string �xtensions: '.' - Update eng/validate-plugins.mjs: require \, validate namespace-keyed extensions object for canvas extensions, widen name pattern to allow dots (spec §5.5, max 64 chars) - Update eng/materialize-plugins.mjs: emit spec-clean served manifests (only spec fields: \, name, version, description, author, homepage, repository, license, keywords, extensions) - Update eng/generate-website-data.mjs: read logo from namespace with fallback to top-level logo for compatibility - Update eng/create-plugin.mjs: scaffold emits \ - Add .github/workflows/validate-plugins.yml: blocking CI for PRs touching plugins/** or extensions/** - Add spec compliance check to external plugin quality gates: non-blocking warnings with ✅/⚠️ /🛑 emoji legend - Update AGENTS.md: document new extension manifest shape, add \ to plugin checklist Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
- Move all extension plugin manifests from extensions/<name> to plugins/<name> - Keep extensions/<name> as reusable source only - Remove standalone extension discovery from marketplace and website plugin catalogs - Auto-bundle same-name extension sources during materialization - Add build-only extensions.json references for sharing extensions across plugins - Remove x-awesome-copilot extension metadata support - Update validation and contributor documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
- Add repo-local skill for creating canvas extension sources - Generate spec-compliant plugin manifests under plugins/ - Support registering reusable extensions with multiple plugins - Remove guidance for extension-local plugin manifests and custom fields Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
- Use each extension ID as its manifest namespace key - Update validation and website generation to resolve extension-specific namespaces - Upsert plugin validation PR comments using the existing repository pattern Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
There was a problem hiding this comment.
Pull request overview
Migrates curated plugins and canvas extensions to Agent Plugins v1.0.0, centralizing installable manifests under plugins/ and updating validation, materialization, marketplace, website, and contribution tooling.
Changes:
- Adds v1.0.0 schemas and namespace-based canvas metadata.
- Materializes reusable extension sources through
extensions.json. - Adds internal/external validation workflows, documentation, and scaffolding guidance.
Show a summary per file
| File | Description |
|---|---|
plugins/work-hub/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/where-was-i/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/visual-pr/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/typespec-m365-copilot/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/typescript-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/token-pacman/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/tiny-tool-town-submitter/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/testing-automation/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/technical-spike/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/swift-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/structured-autonomy/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/software-engineering-team/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/skill-image-gen/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/site-studio/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/security-best-practices/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/salesforce-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/rust-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/rug-agentic-workflow/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/ruby-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/roundup/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/repo-actions-hub/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/release-notes-showcase/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/react19-upgrade/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/react18-upgrade/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/python-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/project-planning/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/project-documenter/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/power-platform-mcp-connector-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/power-platform-architect/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/power-bi-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/power-apps-code-apps/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/php-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/phoenix/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/pcf-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/partners/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/ospo-sponsorship/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/oracle-to-postgres-migration-expert/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/openapi-to-application-python-fastapi/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/openapi-to-application-nodejs-nestjs/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/openapi-to-application-java-spring-boot/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/openapi-to-application-go/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/openapi-to-application-csharp-dotnet/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/noob-mode/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/napkin/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/mcp-m365-copilot/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/kotlin-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/java-modernization-studio/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/java-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/java-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/go-mcp-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/gesture-review/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/gem-team/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/frontend-web-dev/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/flowstudio-power-automate/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/feedback-themes/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/fastah-ip-geo-tools/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/eyeball/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/ember/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/edge-ai-tasks/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/doublecheck/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/diagram-viewer/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/devops-oncall/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/dataverse-sdk-for-python/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/database-data-management/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/csharp-dotnet-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/copilot-sdk/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/convert-to-md/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/context-matic/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/context-engineering/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/connector-namespaces/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/color-orb/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/cms-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/clojure-interactive-programming/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/chromium-control-canvas/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/cast-imaging/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/backlog-swipe-triage/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/azure-cloud-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/aws-cloud-development/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/awesome-copilot/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/automate-this/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/arize-ax/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/arch/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/arcade-canvas/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/apng-studio/.github/plugin/plugin.json |
Migrates canvas manifest. |
plugins/ai-team-orchestration/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/acreadiness-cockpit/.github/plugin/plugin.json |
Adds v1 schema. |
plugins/accessibility-kanban/.github/plugin/plugin.json |
Migrates canvas manifest. |
eng/validate-plugins.mjs |
Updates internal manifest and extension validation. |
eng/materialize-plugins.mjs |
Materializes reusable extensions and served manifests. |
eng/generate-website-data.mjs |
Sources extension metadata from plugins. |
eng/generate-marketplace.mjs |
Removes standalone extension catalog generation. |
eng/external-plugin-quality-gates.mjs |
Adds non-blocking spec checks. |
eng/external-plugin-pr-quality-gates.mjs |
Reports external spec status. |
eng/external-plugin-intake.mjs |
Formats quality-gate statuses and warnings. |
eng/create-plugin.mjs |
Adds schema to generated manifests. |
docs/README.plugins.md |
Adds migrated canvas plugins to documentation. |
CONTRIBUTING.md |
Documents the new extension model. |
AGENTS.md |
Updates repository development guidance. |
.github/workflows/validate-plugins.yml |
Adds plugin validation and PR comments. |
.github/workflows/external-plugin-pr-quality-gates.yml |
Reports spec compliance in PR checks. |
.github/workflows/external-plugin-intake.yml |
Updates intake fallback statuses. |
.github/workflows/external-plugin-command-router.yml |
Updates routed intake fallback statuses. |
.github/skills/create-canvas-extension/SKILL.md |
Adds canvas-extension scaffolding guidance. |
.github/plugin/marketplace.json |
Redirects canvas entries to plugin sources. |
Review details
Suppressed comments (18)
plugins/accessibility-kanban/.github/plugin/plugin.json:19
accessibility-kanbanis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/apng-studio/.github/plugin/plugin.json:19apng-studiois not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/backlog-swipe-triage/.github/plugin/plugin.json:19backlog-swipe-triageis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/chromium-control-canvas/.github/plugin/plugin.json:20chromium-control-canvasis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/color-orb/.github/plugin/plugin.json:19color-orbis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/connector-namespaces/.github/plugin/plugin.json:19connector-namespacesis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/diagram-viewer/.github/plugin/plugin.json:19diagram-vieweris not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/feedback-themes/.github/plugin/plugin.json:19feedback-themesis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/arcade-canvas/.github/plugin/plugin.json:19arcade-canvasis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/gesture-review/.github/plugin/plugin.json:19gesture-reviewis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/java-modernization-studio/.github/plugin/plugin.json:20java-modernization-studiois not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/release-notes-showcase/.github/plugin/plugin.json:19release-notes-showcaseis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/repo-actions-hub/.github/plugin/plugin.json:19repo-actions-hubis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/site-studio/.github/plugin/plugin.json:19site-studiois not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/tiny-tool-town-submitter/.github/plugin/plugin.json:19tiny-tool-town-submitteris not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/token-pacman/.github/plugin/plugin.json:19token-pacmanis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/where-was-i/.github/plugin/plugin.json:19where-was-iis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
plugins/work-hub/.github/plugin/plugin.json:19work-hubis not a reverse-domain extension namespace. Use the documented Copilot namespace so this manifest matches Agent Plugins v1.0.0 semantics and the scaffolding guidance.
- Files reviewed: 104/104 changed files
- Comments generated: 11
- Review effort level: Balanced
| return candidate; | ||
| } | ||
|
|
||
| function inspectAgentPluginSpecCompliance(pluginRoot) { |
| "keywords", | ||
| "extensions", | ||
| ]); | ||
| const AGENT_PLUGIN_NAME_PATTERN = /^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/; |
| } | ||
| if (!/^[a-z0-9-]+$/.test(name)) { | ||
| errors.push("name must contain only lowercase letters, numbers, and hyphens"); | ||
| if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) { |
| function validateSchema(parsed) { | ||
| if (parsed["$schema"] !== AGENT_PLUGINS_SCHEMA) { | ||
| return `$schema must be "${AGENT_PLUGINS_SCHEMA}"`; |
| if (!fs.existsSync(readmePath) && !isExtensionPlugin) { | ||
| errors.push("missing required file: README.md"); | ||
| } |
| paths: | ||
| - "plugins/**" | ||
| - "extensions/**" |
| - name: Post failure summary on PR | ||
| if: failure() && steps.validate.outcome == 'failure' |
|
|
||
| | Name | Description | Items | Tags | | ||
| | ---- | ----------- | ----- | ---- | | ||
| | [accessibility-kanban](../plugins/accessibility-kanban/README.md) | Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work. | 0 items | accessibility, github-issues, issue-triage, kanban-board, planning-workflow, status-tracking | |
| 'All internal plugins and extensions must include:', | ||
| '- `"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"` in `plugin.json`', | ||
| '- A valid `name`, `description`, and `version`', | ||
| '- For **extensions**: `extensions["<extension-id>"].logo` set to `"assets/preview.png"`', |
- Adopt com.github.copilot for all canvas extension manifests - Require the namespace during validation and website generation - Update extension scaffolding guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
There was a problem hiding this comment.
Review details
Suppressed comments (7)
eng/external-plugin-quality-gates.mjs:168
inspectAgentPluginSpecComplianceis declared inside theforblock offindPluginJson, so it is not in scope whererunExternalPluginQualityGatescalls it. Every external-plugin quality run that reaches this call will throw aReferenceErrorand be reported as an infrastructure failure instead of running any gates. Move this function to module scope afterfindPluginJson.
function inspectAgentPluginSpecCompliance(pluginRoot) {
eng/validate-plugins.mjs:24
- This pattern does not enforce the v1.0.0 prohibition on consecutive
--or.., so names such asfoo--barandfoo..barpass internal validation even though the referenced schema rejects them. Use the schema's negative lookahead here.
if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
eng/external-plugin-quality-gates.mjs:24
- The dots are over-escaped in this regex literal:
\\.\\.matches a backslash-containing sequence rather than two literal dots. As a result, an external manifest namedfoo..baris incorrectly reported as spec-compliant. Use\.\.in the regex literal, matching the v1.0.0 schema pattern.
const AGENT_PLUGIN_NAME_PATTERN = /^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
eng/validate-plugins.mjs:217
- Extension plugins are now exempted from the repository-wide README requirement, but the documentation generator unconditionally links every plugin to
../plugins/${dir}/README.md(eng/update-readme.mjs:795). The migrated extension plugin folders have no README, so all newly generated links indocs/README.plugins.mdare broken. Keep README mandatory for plugin directories and add the missing extension-plugin READMEs, or update the generator to support a real alternative target.
if (!fs.existsSync(readmePath) && !isExtensionPlugin) {
.github/workflows/validate-plugins.yml:50
- This failure guidance names the namespace as
<extension-id>, but validation and every migrated manifest require the fixed client namespacecom.github.copilot. Following the posted remediation would still fail validation; show the actual key.
'- For **extensions**: `extensions["<extension-id>"].logo` set to `"assets/preview.png"`',
docs/README.plugins.md:30
- The migrated canvas plugin contains one extension, but this generated row reports
0 items; the same incorrect count appears on every newly added canvas-plugin row. Update the README generator to include the plugin's implicit/self extension (and anyextensions.jsonreferences), then regenerate this file so catalog counts remain accurate.
| [accessibility-kanban](../plugins/accessibility-kanban/README.md) | Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work. | 0 items | accessibility, github-issues, issue-triage, kanban-board, planning-workflow, status-tracking |
eng/materialize-plugins.mjs:169
- The served manifest declares
assets/preview.png, but materialization copies the extension toplugins/<plugin>/extensions/<name>/, leaving the declared asset absent from the plugin root (the migrated plugin directories currently contain only.github). Installed canvas plugins will therefore reference a logo path that is not packaged at that location. Copy the standalone extension's preview into the manifest-relativeassets/preview.pngpath while retaining the extension source underextensions/.
totalExtensions++;
- Files reviewed: 104/104 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Resolve upstream changes while preserving Agent Plugins migration, reusable canvas extension bundling, and external plugin quality reporting. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
🔒 PR Risk Scan ResultsScanned 114 changed file(s).
|
There was a problem hiding this comment.
Review details
Suppressed comments (5)
eng/external-plugin-quality-gates.mjs:186
inspectAgentPluginSpecComplianceis declared inside theforblock offindPluginJson, so it is out of scope at the call on line 990. Every valid external-plugin run reaches that call, throws aReferenceError, and is reported as an infrastructure failure. Move this function to module scope.
function inspectAgentPluginSpecCompliance(pluginRoot) {
eng/materialize-plugins.mjs:1
- The new extension-copy path only places
assets/preview.pngunderextensions/<name>/assets, while the served manifest still declaresextensions.com.github.copilot.logoas root-relativeassets/preview.png. The source plugin directories do not contain that root asset (for example,plugins/apng-studio/assets/preview.pngis absent), so published extension plugins will have a broken logo. Copy each extension's preview asset to the plugin root during materialization, as the removed implementation did.
#!/usr/bin/env nodeimport fs from "fs";import path from "path";import { ROOT_FOLDER } from "./constants.mjs";const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");/** * Recursively copy a directory. */function copyDirRecursive(src, dest) { fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyDirRecursive(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } }}/** * Resolve a plugin-relative path to the repo-root source file. * * ./agents/foo.md → ROOT/agents/foo.agent.md * ./skills/baz/ → ROOT/skills/baz/ */function resolveSource(relPath) { const basename = path.basename(relPath, ".md"); if (relPath.startsWith("./agents/")) { return path.join(ROOT_FOLDER, "agents", `${basename}.agent.md`); } if (relPath.startsWith("./skills/")) { // Strip trailing slash and get the skill folder name const skillName = relPath.replace(/^\.\/skills\//, "").replace(/\/$/, ""); return path.join(ROOT_FOLDER, "skills", skillName); } if (relPath.startsWith("./extensions/")) { const extensionName = relPath.replace(/^\.\/extensions\//, "").replace(/\/$/, ""); return path.join(ROOT_FOLDER, "extensions", extensionName); } return null;}function readExtensionReferences(pluginPath, pluginName) { const referencesPath = path.join(pluginPath, ".github/plugin/extensions.json"); let references = []; if (fs.existsSync(referencesPath)) { try { references = JSON.parse(fs.readFileSync(referencesPath, "utf8")); } catch (err) { throw new Error(`Failed to parse ${referencesPath}: ${err.message}`); } } if (!Array.isArray(references) || references.some((name) => typeof name !== "string")) { throw new Error(`${referencesPath} must contain an array of extension names`); } const names = new Set(references); if (fs.existsSync(path.join(EXTENSIONS_DIR, pluginName, "extension.mjs"))) { names.add(pluginName); } return [...names].sort();}function materializePlugins() { console.log("Materializing plugin files...\n"); if (!fs.existsSync(PLUGINS_DIR)) { console.error(`Error: Plugins directory not found at ${PLUGINS_DIR}`); process.exit(1); } const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name) .sort(); let totalAgents = 0; let totalSkills = 0; let totalExtensions = 0; let warnings = 0; let errors = 0; for (const dirName of pluginDirs) { const pluginPath = path.join(PLUGINS_DIR, dirName); const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json"); if (!fs.existsSync(pluginJsonPath)) { continue; } let metadata; try { metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); } catch (err) { console.error(`Error: Failed to parse ${pluginJsonPath}: ${err.message}`); errors++; continue; } const pluginName = metadata.name || dirName; // Process agents if (Array.isArray(metadata.agents)) { for (const relPath of metadata.agents) { const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src)) { console.warn(` ⚠ ${pluginName}: Source not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "")); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); totalAgents++; } } // Process skills if (Array.isArray(metadata.skills)) { for (const relPath of metadata.skills) { const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) { console.warn(` ⚠ ${pluginName}: Source directory not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, "")); copyDirRecursive(src, dest); totalSkills++; } } // Process extension references from the build-only extensions.json file. const extensionRefs = readExtensionReferences(pluginPath, pluginName); for (const extensionName of extensionRefs) { const relPath = `./extensions/${extensionName}`; const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown extension path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) { console.warn(` ⚠ ${pluginName}: Extension source directory not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, "")); copyDirRecursive(src, dest); totalExtensions++; } // Emit a spec-compliant served manifest for the marketplace branch. // Source manifests keep composition fields (agents and skills) // for build tooling. The served manifest retains only Agent Plugins v1.0.0 fields // so the runtime uses conventional directory discovery for all content. const SPEC_FIELDS = new Set(["$schema", "name", "version", "description", "author", "homepage", "repository", "license", "keywords", "extensions"]); const AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"; const served = { "$schema": AGENT_PLUGINS_SCHEMA }; for (const [key, val] of Object.entries(metadata)) { if (SPEC_FIELDS.has(key) && key !== "$schema") { served[key] = val; } } fs.writeFileSync(pluginJsonPath, JSON.stringify(served, null, 2) + "\n", "utf8"); const counts = []; if (metadata.agents?.length) counts.push(`${metadata.agents.length} agents`); if (metadata.skills?.length) counts.push(`${metadata.skills.length} skills`); if (extensionRefs.length) counts.push(`${extensionRefs.length} extensions`); if (counts.length) { console.log(`✓ ${pluginName}: ${counts.join(", ")}`); } } console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`); if (warnings > 0) { console.log(`${warnings} warning(s).`); } if (errors > 0) { console.error(`${errors} error(s).`); process.exit(1); }}materializePlugins();
eng/validate-plugins.mjs:218
- Exempting extension plugins from the README requirement leaves the newly migrated plugin directories with only plugin.json. The generated
docs/README.plugins.mdnevertheless links toplugins/<id>/README.md(for example accessibility-kanban and apng-studio), producing broken documentation links and violating the plugin layout documented in AGENTS.md:325-327. Keep the README requirement and add the corresponding plugin READMEs.
if (!fs.existsSync(readmePath) && !isExtensionPlugin) {
.github/workflows/validate-plugins.yml:50
- The failure guidance names
extensions["<extension-id>"], but validation requires the literalcom.github.copilotnamespace. Following this message would still fail validation; show the exact required key.
'- For **extensions**: `extensions["<extension-id>"].logo` set to `"assets/preview.png"`',
.github/plugin/marketplace.json:310
- This adds a second
connector-namespacesmarketplace entry even though the external entry immediately above already publishes version 1.2.0, andplugins/connector-namespacesdoes not exist. Consumers may resolve the duplicate stale 1.1.0 entry to a nonexistent source; remove this generated duplicate.
{
"name": "connector-namespaces",
"source": "plugins/connector-namespaces",
"description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.",
"version": "1.1.0"
},
- Files reviewed: 110/110 changed files
- Comments generated: 3
- Review effort level: Balanced
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { | ||
| materializePlugins(); | ||
| } | ||
| #!/usr/bin/env nodeimport fs from "fs";import path from "path";import { ROOT_FOLDER } from "./constants.mjs";const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");/** * Recursively copy a directory. */function copyDirRecursive(src, dest) { fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyDirRecursive(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } }}/** * Resolve a plugin-relative path to the repo-root source file. * * ./agents/foo.md → ROOT/agents/foo.agent.md * ./skills/baz/ → ROOT/skills/baz/ */function resolveSource(relPath) { const basename = path.basename(relPath, ".md"); if (relPath.startsWith("./agents/")) { return path.join(ROOT_FOLDER, "agents", `${basename}.agent.md`); } if (relPath.startsWith("./skills/")) { // Strip trailing slash and get the skill folder name const skillName = relPath.replace(/^\.\/skills\//, "").replace(/\/$/, ""); return path.join(ROOT_FOLDER, "skills", skillName); } if (relPath.startsWith("./extensions/")) { const extensionName = relPath.replace(/^\.\/extensions\//, "").replace(/\/$/, ""); return path.join(ROOT_FOLDER, "extensions", extensionName); } return null;}function readExtensionReferences(pluginPath, pluginName) { const referencesPath = path.join(pluginPath, ".github/plugin/extensions.json"); let references = []; if (fs.existsSync(referencesPath)) { try { references = JSON.parse(fs.readFileSync(referencesPath, "utf8")); } catch (err) { throw new Error(`Failed to parse ${referencesPath}: ${err.message}`); } } if (!Array.isArray(references) || references.some((name) => typeof name !== "string")) { throw new Error(`${referencesPath} must contain an array of extension names`); } const names = new Set(references); if (fs.existsSync(path.join(EXTENSIONS_DIR, pluginName, "extension.mjs"))) { names.add(pluginName); } return [...names].sort();}function materializePlugins() { console.log("Materializing plugin files...\n"); if (!fs.existsSync(PLUGINS_DIR)) { console.error(`Error: Plugins directory not found at ${PLUGINS_DIR}`); process.exit(1); } const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name) .sort(); let totalAgents = 0; let totalSkills = 0; let totalExtensions = 0; let warnings = 0; let errors = 0; for (const dirName of pluginDirs) { const pluginPath = path.join(PLUGINS_DIR, dirName); const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json"); if (!fs.existsSync(pluginJsonPath)) { continue; } let metadata; try { metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); } catch (err) { console.error(`Error: Failed to parse ${pluginJsonPath}: ${err.message}`); errors++; continue; } const pluginName = metadata.name || dirName; // Process agents if (Array.isArray(metadata.agents)) { for (const relPath of metadata.agents) { const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src)) { console.warn(` ⚠ ${pluginName}: Source not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "")); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); totalAgents++; } } // Process skills if (Array.isArray(metadata.skills)) { for (const relPath of metadata.skills) { const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) { console.warn(` ⚠ ${pluginName}: Source directory not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, "")); copyDirRecursive(src, dest); totalSkills++; } } // Process extension references from the build-only extensions.json file. const extensionRefs = readExtensionReferences(pluginPath, pluginName); for (const extensionName of extensionRefs) { const relPath = `./extensions/${extensionName}`; const src = resolveSource(relPath); if (!src) { console.warn(` ⚠ ${pluginName}: Unknown extension path format: ${relPath}`); warnings++; continue; } if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) { console.warn(` ⚠ ${pluginName}: Extension source directory not found: ${src}`); warnings++; continue; } const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, "")); copyDirRecursive(src, dest); totalExtensions++; } // Emit a spec-compliant served manifest for the marketplace branch. // Source manifests keep composition fields (agents and skills) // for build tooling. The served manifest retains only Agent Plugins v1.0.0 fields // so the runtime uses conventional directory discovery for all content. const SPEC_FIELDS = new Set(["$schema", "name", "version", "description", "author", "homepage", "repository", "license", "keywords", "extensions"]); const AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"; const served = { "$schema": AGENT_PLUGINS_SCHEMA }; for (const [key, val] of Object.entries(metadata)) { if (SPEC_FIELDS.has(key) && key !== "$schema") { served[key] = val; } } fs.writeFileSync(pluginJsonPath, JSON.stringify(served, null, 2) + "\n", "utf8"); const counts = []; if (metadata.agents?.length) counts.push(`${metadata.agents.length} agents`); if (metadata.skills?.length) counts.push(`${metadata.skills.length} skills`); if (extensionRefs.length) counts.push(`${extensionRefs.length} extensions`); if (counts.length) { console.log(`✓ ${pluginName}: ${counts.join(", ")}`); } } console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`); if (warnings > 0) { console.log(`${warnings} warning(s).`); } if (errors > 0) { console.error(`${errors} error(s).`); process.exit(1); }}materializePlugins(); No newline at end of file |
|
|
||
| // Run the script | ||
| generateMarketplace(); | ||
| #!/usr/bin/env nodeimport fs from "fs";import path from "path";import { ROOT_FOLDER } from "./constants.mjs";import { readExternalPlugins } from "./external-plugin-validation.mjs";const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json");/** * Read plugin metadata from plugin.json file * @param {string} pluginDir - Path to plugin directory * @returns {object|null} - Plugin metadata or null if not found */function readPluginMetadata(pluginDir) { const pluginJsonPath = path.join(pluginDir, ".github/plugin", "plugin.json"); if (!fs.existsSync(pluginJsonPath)) { console.warn(`Warning: No plugin.json found for ${path.basename(pluginDir)}`); return null; } try { const content = fs.readFileSync(pluginJsonPath, "utf8"); return JSON.parse(content); } catch (error) { console.error(`Error reading plugin.json for ${path.basename(pluginDir)}:`, error.message); return null; }}function collectLocalPluginsFromRoot(rootDir, sourcePrefix, includeEntry = () => true) { if (!fs.existsSync(rootDir)) { return []; } const entries = fs.readdirSync(rootDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .filter(entry => includeEntry(entry.name)) .map(entry => entry.name) .sort(); const plugins = []; for (const dirName of entries) { const pluginPath = path.join(rootDir, dirName); const metadata = readPluginMetadata(pluginPath); if (!metadata) { continue; } plugins.push({ name: metadata.name, source: `${sourcePrefix}/${dirName}`, description: metadata.description, version: metadata.version || "1.0.0" }); } return plugins;}/** * Generate marketplace.json from plugin directories */function generateMarketplace() { console.log("Generating marketplace.json..."); if (!fs.existsSync(PLUGINS_DIR)) { console.error(`Error: Plugins directory (${PLUGINS_DIR}) was not found`); process.exit(1); } const plugins = collectLocalPluginsFromRoot(PLUGINS_DIR, "plugins"); console.log(`Found ${plugins.length} local plugin manifests`); // Read external plugins and merge as-is const { plugins: externalPlugins, errors: externalErrors, warnings: externalWarnings } = readExternalPlugins({ localPluginNames: plugins.map((plugin) => plugin.name), policy: "marketplace", }); externalWarnings.forEach((warning) => console.warn(`Warning: ${warning}`)); if (externalErrors.length > 0) { externalErrors.forEach((error) => console.error(`Error: ${error}`)); console.error("Error: external.json contains invalid entries"); process.exit(1); } if (externalPlugins.length > 0) { console.log(`\nFound ${externalPlugins.length} external plugins`); for (const ext of externalPlugins) { plugins.push(ext); console.log(`✓ Added external plugin: ${ext.name}`); } } // Sort all plugins by name (case-insensitive) plugins.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })); // Create marketplace.json structure const marketplace = { name: "awesome-copilot", metadata: { description: "Community-driven collection of GitHub Copilot plugins, agents, prompts, and skills", version: "1.0.0" }, owner: { name: "GitHub", email: "copilot@github.com" }, plugins: plugins }; // Ensure directory exists const marketplaceDir = path.dirname(MARKETPLACE_FILE); if (!fs.existsSync(marketplaceDir)) { fs.mkdirSync(marketplaceDir, { recursive: true }); } // Write marketplace.json fs.writeFileSync(MARKETPLACE_FILE, JSON.stringify(marketplace, null, 2) + "\n"); console.log(`\n✓ Successfully generated marketplace.json with ${plugins.length} plugins (${plugins.length - externalPlugins.length} local, ${externalPlugins.length} external)`); console.log(` Location: ${MARKETPLACE_FILE}`);}// Run the scriptgenerateMarketplace(); No newline at end of file |
| const sourceUrl = String(entry?.source_tree_url || ''); | ||
| const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository'); | ||
| const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator; |
Use root plugin.json manifests and namespaced extension directories throughout local tooling, validation, generation, and contributor documentation. Restore materialize-plugins.mjs line breaks so the source remains readable in GitHub. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
There was a problem hiding this comment.
Review details
Suppressed comments (9)
eng/external-plugin-quality-gates.mjs:186
- This function is declared inside the
forblock infindPluginJson, so it is block-scoped and unavailable torunExternalPluginQualityGatesat line 990. Every quality-gate run that reaches the new spec check throwsReferenceError, is caught as an infrastructure error, and never runs the remaining gates. Move this declaration to module scope after closingfindPluginJson.
function inspectAgentPluginSpecCompliance(pluginRoot) {
.github/workflows/external-plugin-pr-quality-gates.yml:300
- These values come from external submission metadata, and
source.refmay legally contain Markdown table delimiters. Removing the existing escaping lets a ref such asv1|...corrupt or inject content into the bot's PR comment. Keep the GitHub URL allowlist and table-cell escaping that are already defined above.
const sourceUrl = String(entry?.source_tree_url || '');
const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository');
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
.github/workflows/validate-plugins.yml:50
- This failure guidance names the namespace as
<extension-id>, but validation requires the literalcom.github.copilotnamespace. Users following the error message will produce another invalid manifest; showextensions["com.github.copilot"].logoinstead.
'- For **extensions**: `extensions["<extension-id>"].logo` set to `"assets/preview.png"`',
eng/external-plugin-quality-gates.mjs:25
- The escaped dots are incorrect for a regex literal:
\\.\\.matches backslash-containing text rather than two literal periods, so names such asa..bincorrectly pass this new compliance check. Use\.\.in the regex literal, matching the existing canonical validation ineng/external-plugin-validation.mjs:103-105.
const AGENT_PLUGIN_NAME_PATTERN = /^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
.github/skills/create-canvas-extension/SKILL.md:43
- This scaffold still places
plugin.jsonunder the removed.github/plugin/layout, contradicting the root path required later in this same skill and by the migrated tooling. Following the skill would create an invalid extension plugin.
plugins/<extension-id>/
├── .github/
│ └── plugin/
│ └── plugin.json
└── README.md
plugins/the-workshop/.github/plugin/extensions.json:3
- This build-only mapping is never read: repository search finds no consumer for
.github/plugin/extensions.json, whilematerialize-plugins.mjs:50-64reads extension references fromplugin.json. That contradicts the PR description's mapping-file design and means future mappings created according to that design will be ignored. Either wire this file into materialization/validation or remove it and document the manifest field as the source of truth.
eng/update-readme.mjs:718 - Switching this reader to root manifests now includes extension-only plugins, but the table generator at lines 789-792 still counts only agents, commands, and skills. Consequently every newly listed canvas plugin is reported as
0 itemsindocs/README.plugins.mddespite shipping an extension. Count extension references (including a matching extension source) and regenerate the documentation.
const jsonPath = path.join(pluginDir, "plugin.json");
.github/skills/create-canvas-extension/SKILL.md:86
- The instruction says to update
plugin.jsonbut then describes its entire contents as a JSON array of IDs. An agent following this literally would overwrite the required manifest object with an invalid array. Show theextensions.com.github.copilot.directoriesproperty inside a manifest object, including the required./extensions/path prefix.
Its contents must be a sorted JSON array of extension IDs:
```json
[
"<extension-id>"
]
docs/README.plugins.md:30
- This and the other newly added canvas rows link to
plugins/<id>/README.md, but these migrated plugin directories do not contain those files (for example,plugins/accessibility-kanban/README.mdis absent). The generated catalog therefore introduces broken links. Either add the required plugin READMEs or make the generator link to an existing extension README/fallback before regenerating this table.
| [accessibility-kanban](../plugins/accessibility-kanban/README.md) | Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work. | 0 items | accessibility, github-issues, issue-triage, kanban-board, planning-workflow, status-tracking |
- Files reviewed: 115/115 changed files
- Comments generated: 1
- Review effort level: Balanced
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { | ||
| materializePlugins(); | ||
| } | ||
| materializePlugins(); |
Pull Request Checklist
npm startand verified thatREADME.mdis up to date.mainbranch for this pull request.Description
This PR migrates curated plugins and canvas extensions to the Agent Plugins v1.0.0 manifest shape and removes the old standalone-extension model. Canvas implementations remain reusable sources under
extensions/, while all shippable manifests live underplugins/.The build now materializes reusable extensions into any plugin that references them through build-only
.github/plugin/extensions.jsonfiles, without adding custom fields toplugin.json. Canvas extension manifests use the GitHub Copilot client namespacecom.github.copilot, and marketplace/website generation no longer treatsextensions/as a separate installable catalog.It also adds blocking internal validation, non-blocking external spec compliance reporting, a repo-local canvas extension scaffolding skill, and stable PR-comment updates for validation failures.
Validation
npm run plugin:validatenpm run buildnpm run website:datanpm run skill:validateType of Contribution
Additional Notes
The source manifests retain build-time composition information needed to materialize agents and skills; served marketplace manifests are stripped to Agent Plugins spec fields. Canvas extension source can be reused by multiple plugins without duplicating the implementation.
By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.