fix(review): validate skill roots at mount and isolate provider default roots

ds-review-bot round 1 on the repository-plugin runtime:
- a manifest-declared skill root absent or non-directory in the installed
  package now fails the plugin load (skill-local treats a missing root as
  legitimately empty, which silently mounted a skill-less plugin)
- includeDefaultRoots: false no longer inherits $DSH_BUNDLED_SKILL_DIR, so
  isolated repository providers see only their explicit roots
- prepared wrapper baseUrl schema requires the file: scheme, failing hostile
  URLs at the declared validation boundary
- preparedPath reuses format.ts's isOutside; SERVER_NAME_PATTERN is exported
  and pinned equal to dsh-mcp-client's, with the restatement justified (the
  prepare bin keeps a zod-only module graph); the unexplained `as never`
  cast now carries its schemastery rationale
- the import-free wrapper assertion also rejects dynamic import(
- the headless fixture wrapper is regenerated by the real prepareDshPlugin
  and a drift test pins fixture == generator output
- prepareDshPlugin JSDoc states the non-atomic publish repair contract
This commit is contained in:
Tianyi Cui
2026-08-01 21:16:23 +08:00
parent a0aed8a19f
commit 0664b25cd9
20 changed files with 175 additions and 36 deletions
@@ -820,6 +820,22 @@ describe('LocalSkillProvider', () => {
await ctx.plugin(SkillLocal, { watch: false })
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['env-bundled-skill', 'env-skill'])
// Isolated providers see only their explicit roots: the environment
// bundled root is a default root, so includeDefaultRoots: false must
// drop it — repository providers never re-claim the app's builtins.
const isolated = new Context()
await isolated.plugin(SkillService)
const customOnly = join(envHome, 'custom-only')
await writeSkill(customOnly, 'custom-isolated-skill', 'Custom isolated skill')
await isolated.plugin(SkillLocal, {
providerName: 'isolated',
includeDefaultRoots: false,
customSkillDirs: [customOnly],
watch: false,
})
expect((await isolated.skills.list()).map(skill => skill.name)).toEqual(['custom-isolated-skill'])
await isolated.fiber.dispose()
process.env.DSH_HOME = join(envHome, 'empty-dsh')
delete process.env.DSH_BUNDLED_SKILL_DIR
process.env.DSH_AGENTS_HOME = join(envHome, 'empty-agents')