Skip to content

fix(plugins): materialize plugins as real files instead of symlinks#2443

Open
katriendg wants to merge 7 commits into
mainfrom
fix/plugin-symlinks-windows
Open

fix(plugins): materialize plugins as real files instead of symlinks#2443
katriendg wants to merge 7 commits into
mainfrom
fix/plugin-symlinks-windows

Conversation

@katriendg

@katriendg katriendg commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Note to reviewers!

Please focus on the changed code files and not on the ./plugins folder contents to assess the completeness of the PR. The 3000+ file changes there are simply copies of the files now.
Due to the huge amount of changes, the UI is NOT showing some key file changes, namely these ones under scripts folder which are the main ones to review:

Plugin generation (source)

Plugin tests

Python discovery + tests (plugins-exclusion)

Description

The plugin generator used to materialize every packaged component as a git symbolic link on Linux and macOS, or as a text stub (a small file whose entire contents were a relative path) on Windows without Developer Mode. A follow-up pass then rewrote git index modes from 100644 to 120000 so those stubs would check out as real symlinks on other platforms. This kept git status clean, but it left the shipped plugin packages pointing at their sources rather than containing them.

Under the VS Code agent plugin packaging model, a plugin package has to be self-contained: every path a plugin.json declares, and every source the marketplace manifest indexes, must resolve to real content inside the plugin root. Symlinks and path-stub files do not survive packaging and installation intact.

That mismatch is what users hit in the field. On Windows the stubs shipped as literal relative-path text, so components arrived malformed. Through the agentPlugins install path, symlinked components did not resolve to real files inside the installed package, so the plugins were effectively empty. This PR replaces the link-and-repair strategy with straightforward file copies, so every generated artifact under plugins/ is a regular file, and adds marketplace validation that fails when a packaged plugin is missing real in-package content.

The bulk of the diff is the regenerated plugins/* tree*, where 3,146 previously-symlinked or stubbed entries become real files. The reviewable change is the six PowerShell source and test files described below, plus a set of CI exclusions covered in the CI and workflows section.

Because plugin packages are now materialized as real file copies (rather than symlinks) of already-validated .github/ sources, every recursive check began scanning duplicate content: spell check failed on the copies and CodeQL raised duplicate "clear-text logging" alerts against the copied mural/_cli_auth.py. Since everything under plugins/ is auto-generated and independently guaranteed by plugin validation, it should always be excluded from source-level checks.

Changes

Generation

  • Rewrote New-PluginLink in PluginHelpers.psm1 to copy the source file or directory tree into the plugin destination instead of creating a symbolic link or writing a text stub. It removes any existing destination first, so stale symlinks or files from earlier generations do not survive a re-run, and uses -LiteralPath consistently.
  • Removed Test-SymlinkCapability (the per-run symlink probe) and Repair-PluginSymlinkIndex (the 100644120000 git index rewrite) from PluginHelpers.psm1, along with their Export-ModuleMember entries.
  • Added Add-GeneratedFilesForCopiedTree, which walks a materialized tree and registers every copied file path into the generated-files set so orphan cleanup preserves the full tree rather than pruning nested copied content.
  • Dropped the -SymlinkCapable parameter from Write-PluginHookArtifact and Write-PluginDirectory. Hook script directories, skill directories, and shared directories now register their copied trees through the new helper.

Orchestration

  • Removed the run-level symlink-capability probe and the Repair-PluginSymlinkIndex post-pass from Invoke-PluginGeneration in Generate-Plugins.ps1.
  • Reworked orphan cleanup so it no longer special-cases symlink LinkType. It now builds an OrdinalIgnoreCase set of generated paths and preserves any directory that was generated or contains generated content, checking that prefix before removing empty directories bottom-up.
  • Updated doc comments and DryRun verbose output from link language to copy language.

Validation

  • Added Test-PluginPackageContent to Validate-Marketplace.ps1. It confirms each packaged plugin has a real in-package README.md and .github/plugin/plugin.json, parses that manifest, and verifies every declared agents, commands, skills, and hooks path resolves to real content inside the plugin root.
  • Wired the new check into Invoke-MarketplaceValidation, running per plugin once the directory-existence check passes, so a plugin that ships without real content now fails validation.

Tests

  • Removed the Test-SymlinkCapability and Repair-PluginSymlinkIndex suites from PluginHelpers.Tests.ps1. The New-PluginLink tests now assert real copied file content with no reparse point, plus a recursive directory copy that preserves nested files.
  • Inverted the symlink-capability expectation in Generate-Plugins.Tests.ps1 to assert that no "Symlink capability" messages are emitted during generation.
  • Added a New-TestPluginPackage helper in Validate-Marketplace.Tests.ps1 that builds real README and plugin.json packages, updated existing fixtures to use it, and added coverage for a missing README and a missing declared component path.
  • Added parallel plugins-exclusion cases in PythonLintHelpers.Tests.ps1 and Invoke-PythonTests.Tests.ps1, mirroring the existing node_modules exclusion tests.

CI and workflows

Excluded the generated plugins/ tree from source-level checks that would otherwise re-scan the copies:

  • Added plugins/** to the .cspell.json ignorePaths (fixes the failing spell-check), to the codecov.yml ignore list, and to the lint:links exclude set in package.json.
  • Added a new .github/codeql/codeql-config.yml with paths-ignore: plugins, wired into codeql-analysis.yml via config-file, to drop the duplicate code-scanning alerts.
  • Added a plugins guard to the recursive pyproject.toml discovery in pr-validation.yml, which transitively removes plugin copies from the python-tests, fuzz-tests, and pip-audit matrix jobs, and applied the same guard to the local Get-PythonSkill and Invoke-PythonTests.ps1 discovery helpers.

Node-project discovery needed no change since it is already scoped to .github/skills, and markdown linting was intentionally left as-is because the plugin:postprocess step formats the generated top-level markdown.

Related Issue(s)

Fixes #2409
Related to #1905 which reported malformed agent symlinks on Windows, but the root cause was the same: symlinks and path stubs do not survive packaging and installation intact. To be validated if GitHub Copilot CLI issues also solved, and close issue once we validate.

Type of Change

Select all that apply:

Code & Documentation:

  • Bug fix (non-breaking change fixing an issue)
  • New feature (non-breaking change adding functionality)
  • Breaking change (fix or feature causing existing functionality to change)
  • Documentation update

Infrastructure & Configuration:

  • GitHub Actions workflow
  • Linting configuration (markdown, PowerShell, etc.)
  • Security configuration
  • DevContainer configuration
  • Dependency update

AI Artifacts:

  • Reviewed contribution with prompt-builder agent and addressed all feedback
  • Copilot instructions (.github/instructions/*.instructions.md)
  • Copilot prompt (.github/prompts/*.prompt.md)
  • Copilot agent (.github/agents/*.agent.md)
  • Copilot skill (.github/skills/*/SKILL.md)
  • Copilot hook (.github/hooks/*/*.json)
  • Eval spec added/updated for changed AI artifacts (evals/)

Note for AI Artifact Contributors:

  • Agents: Research, indexing/referencing other project (using standard VS Code GitHub Copilot/MCP tools), planning, and general implementation agents likely already exist. Review .github/agents/ before creating new ones.
  • Skills: Must include both bash and PowerShell scripts. See Skills.
  • Model Versions: Only contributions targeting the latest Anthropic and OpenAI models will be accepted. Older model versions (e.g., GPT-3.5, Claude 3) will be rejected.
  • See Agents Not Accepted and Model Version Requirements.

Other:

  • Script/automation (.ps1, .sh, .py)
  • Other (please describe):

Testing

Ran the plugin PowerShell suites via npm run test:ps -- -TestPath "scripts/tests/plugins/" and regenerated the tree with npm run plugin:generate to confirm every artifact under plugins/* materializes as a regular file with no symbolic links or 120000 index entries. Marketplace validation (npm run lint:marketplace) now exercises the in-package content checks.

Checklist

Required Checks

  • Documentation is updated (if applicable)
  • Files follow existing naming conventions
  • Changes are backwards compatible (if applicable)
  • Tests added for new functionality (if applicable)

AI Artifact Contributions

  • Used /prompt-analyze to review contribution
  • Addressed all feedback from prompt-builder review
  • Verified contribution follows common standards and type-specific requirements

Required Automated Checks

The following validation commands must pass before merging:

  • Markdown linting: npm run lint:md
  • Spell checking: npm run spell-check
  • Frontmatter validation: npm run lint:frontmatter
  • Skill structure validation: npm run validate:skills
  • Link validation: npm run lint:md-links
  • PowerShell analysis: npm run lint:ps
  • Eval spec schema and coverage (if AI artifacts changed): npm run eval:lint:schema
  • Plugin freshness: npm run plugin:generate
  • Docusaurus tests: npm run docs:test

Security Considerations

  • This PR does not contain any sensitive or NDA information
  • Any new dependencies have been reviewed for security issues
  • Security-related scripts follow the principle of least privilege

Additional Notes

Removing Repair-PluginSymlinkIndex also drops the git hash-object and git update-index calls that the generator previously ran against generated paths, reducing the generator's surface area. Reviewers should focus on the six source and test files; the remaining diff is the regenerated plugins/* output* produced by npm run plugin:generate.

Replace the symlink-or-text-stub materialization in plugin generation
with copied files and directory trees so generated plugin packages are
self-contained and install on Windows without Developer Mode or
elevated privileges.

- Copy files and directory trees in New-PluginLink, copy hook sibling
  script directories, and track copied child files for orphan cleanup
- Remove Test-SymlinkCapability, Repair-PluginSymlinkIndex, and the
  -SymlinkCapable parameter and its generator orchestration
- Preserve generated shared directories during refresh cleanup
- Assert real in-package content in Validate-Marketplace.ps1
- Update plugin Pester tests for copy-based behavior
- Regenerate plugins/** with zero symlinks (mode 100644)
@katriendg
katriendg requested a review from a team as a code owner July 10, 2026 08:56

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Dependency Review Summary

The full dependency review summary was too large to display here (27673KB, limit is 1024KB).

Please download the artifact named "dependency-review-summary" to view the complete report.

View full job summary

@github-actions

Copy link
Copy Markdown
Contributor

Eval Execution

⚠️ No eval summary was produced.

@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@5c15a03). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #2443   +/-   ##
=======================================
  Coverage        ?   82.71%           
=======================================
  Files           ?      154           
  Lines           ?    20777           
  Branches        ?       13           
=======================================
  Hits            ?    17185           
  Misses          ?     3590           
  Partials        ?        2           
Flag Coverage Δ
docusaurus 94.38% <ø> (?)
pester 86.20% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot mentioned this pull request Jul 10, 2026
Plugin files under plugins/ are now real copies (not symlinks) of
already-validated .github/ sources, so recursive scanners were seeing
duplicate content: spell check failed and CodeQL flagged duplicate
alerts. Exclude the generated tree from checks that would otherwise
re-scan the copies.

- Add plugins/** to cspell ignorePaths (fixes failing spell-check)
- Add CodeQL config with paths-ignore: plugins, referenced from
  codeql-analysis.yml init (drops duplicate code-scanning alerts)
- Exclude plugins from CI Python project discovery in pr-validation
  (removes plugin copies from python-tests, fuzz-tests, and pip-audit)
- Exclude plugins from Get-PythonSkill and Invoke-PythonTests discovery
- Add plugins/** to codecov ignore and the lint:links exclude set
- Add parallel plugins-exclusion Pester cases mirroring node_modules
- convert remaining symlink stubs (mode 120000) to real file copies
- update modified plugin files to reflect source changes from main
- remove orphaned references files no longer present in sources

🔌 - Generated by Copilot
@rezatnoMsirhC
rezatnoMsirhC self-requested a review July 15, 2026 22:21

@rezatnoMsirhC rezatnoMsirhC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was eager to resolve merge conflicts and get this merged, but after so many changes resulting from npm run plugin:generate and the fact that my approval is no longer valid due to being the last pusher, we need one more fresh approval from someone else.

@jkim323

jkim323 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Was eager to resolve merge conflicts and get this merged, but after so many changes resulting from npm run plugin:generate and the fact that my approval is no longer valid due to being the last pusher, we need one more fresh approval from someone else.

@rezatnoMsirhC Agree and approved :)

@jkim323

jkim323 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Seems like we need to regenerate the plugin files locally and commit the result to fix the failed check....

@JoshLuedeman

Copy link
Copy Markdown

Independent Windows verification that this is the right fix, plus a note on the red checks.

Repro on main (Windows, Copilot CLI 1.0.72): Developer Mode on, global core.symlinks=true, and a symlink-creation probe that succeeds — yet copilot plugin install hve-core-all@hve-core still fails with Access is denied (os error 5), while a real-file plugin (fabric-collection) installs fine. So the documented prerequisites can't rescue the symlink approach; the real-file copies here are what's needed. This lines up with #1905 / #1844.

On the failing checks: the only substantive one is Plugin ValidationPlugins out of date. Run 'npm run plugin:generate' and commit. The branch looks up to date with main, and none of the .github/{agents,prompts,instructions,skills,hooks} or collections/ sources appear to differ from main — so this reads as a stale plugins/ regeneration gap rather than a rebase issue. A fresh regenerate on the branch (Linux/devcontainer, so line endings + modes match CI) should clear it:

npm ci
npm run plugin:generate
git add plugins && git commit -m "chore(plugins): regenerate after source sync" && git push

PR Validation Success is only red as a cascade of that (The following jobs did not pass: plugin-validation), and Review Dependencies isn't a required check (its submission step completed). Happy to help — I don't have write access to the branch, otherwise I'd push the regenerate myself.


🤖 Drafted by GitHub Copilot.

@JoshLuedeman

Copy link
Copy Markdown

@katriendg — this is approved and really close; the only substantive blocker is the Plugin Validation check (Plugins out of date. Run 'npm run plugin:generate' and commit.). The branch already looks up to date with main, so a quick regenerate on the branch should turn it green (and clear the cascading PR Validation Success):

npm ci
npm run plugin:generate
git add plugins && git commit -m "chore(plugins): regenerate after source sync" && git push

Could you push that when you get a chance? I'd gladly do it myself but don't have write access to the branch. Thanks for driving this one — it fixes the Windows install for us.


🤖 Drafted by GitHub Copilot.

@JoshLuedeman

Copy link
Copy Markdown

Heads-up from testing this branch on Windows: the real-file conversion here is a clear improvement, but there looks to be a second, separate Windows issue that this PR alone won't resolve for the larger bundles.

I checked out this branch and installed its plugins via a local marketplace (i.e. the real-file plugins/ from this PR — verified 0 symlinks). On Windows (Copilot CLI 1.0.72):

  • Installs cleanly: github, ado, rpi, coding-standards, gitlab, jira, data-science, installer — these are broken on main (symlink stubs), so this PR genuinely fixes them.
  • Still fails with Access is denied. (os error 5): design-thinking, security, project-planning, hve-core, hve-core-all.

The split correlates with plugin size, not content: jira (78 files) installs, but design-thinking (94 files, no hook, no Python skill) fails, and it's deterministic (3/3 retries). What I was able to rule out:

  • Symlinks — 0 across every plugin dir in the checkout.
  • The telemetry hookproject-planning / design-thinking / security have no hook and still fail.
  • Python skillsgitlab / jira contain a pyproject.toml skill and install fine.
  • Windows MAX_PATH — deepest install-target path is ~174 chars (well under 260).
  • The files themselvesrobocopy copies every failing plugin with 0 failures, and all files are readable with no reserved/odd names.

So it reads as a CLI-side plugin install issue that scales with file count, independent of the symlink fix in this PR. Net: this change is necessary (and fixes the smaller plugins), but on its own it isn't sufficient to make hve-core / hve-core-all installable via copilot plugin install on Windows — likely worth a separate issue against the CLI.

As a stopgap, a manual install works on Windows: copy the real-file plugin dir into ~/.copilot/installed-plugins/<marketplace>/<plugin> and register it in config.json + settings.json; copilot plugin list then loads it (77 agents / 75 commands / 51 skills for hve-core-all, no symlinks).


🤖 Drafted by GitHub Copilot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: HVE Core is not installed correctly via agentPlugins

6 participants