feat: add static repository plugin format

This commit is contained in:
Tianyi Cui
2026-07-30 04:15:07 +08:00
parent fd4d369907
commit fa7051a9d1
35 changed files with 1310 additions and 27 deletions
@@ -0,0 +1,12 @@
#!/usr/bin/env node
/** Command-line entry that prepares the current `.dsh-plugin` package. @module */
import { prepareDshPlugin } from './format.ts'
try {
await prepareDshPlugin()
} catch (error) {
process.stderr.write(`dsh-plugin-prepare: ${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
}
@@ -0,0 +1,168 @@
/**
* Static repository-plugin preparation and prepared-manifest validation.
* @module
*/
import { cp, copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { z } from 'zod'
import { parseMcpDocument } from './mcp.ts'
/** Fixed module filename loaded from an installed prepared plugin package. */
export const PREPARED_ENTRY_FILENAME = 'dsh-plugin.mjs'
/** Fixed directory containing copied static plugin assets. */
export const PREPARED_ASSET_DIRECTORY = 'dsh-plugin-assets'
/** Loader builtin used by every generated import-free wrapper. */
export const REPOSITORY_PLUGIN_BUILTIN = 'dsh-repository-plugin'
const sourceMetadataSchema = z.object({
skills: z.array(z.string().min(1)).default([]),
mcpServers: z.string().min(1).optional(),
}).strict().refine(value => value.skills.length > 0 || value.mcpServers !== undefined, {
message: 'declare at least one skill root or mcpServers file',
})
const sourcePackageSchema = z.looseObject({
name: z.string().min(1),
dsh: sourceMetadataSchema,
})
const preparedManifestSchema = z.object({
name: z.string().min(1),
skills: z.array(z.string().min(1)),
mcpServers: z.string().min(1).optional(),
}).strict()
const preparedConfigSchema = z.object({
baseUrl: z.url(),
manifest: preparedManifestSchema,
}).strict()
/** Static manifest embedded in the generated wrapper. */
export interface PreparedPluginManifest {
name: string
skills: string[]
mcpServers?: string
}
/** Untrusted generated-wrapper config accepted by the DSH-owned runtime builtin. */
export interface PreparedPluginConfig {
baseUrl: string
manifest: PreparedPluginManifest
}
function formatZodError(label: string, error: z.ZodError): Error {
return new Error(`${label}:\n${z.prettifyError(error)}`)
}
/**
* Validate the config passed by an installed prepared wrapper.
* @param value - wrapper-provided value crossing the file/module boundary.
* @returns a detached typed config.
*/
export function parsePreparedPluginConfig(value: unknown): PreparedPluginConfig {
const result = preparedConfigSchema.safeParse(value)
if (!result.success) throw formatZodError('invalid prepared DSH plugin', result.error)
return {
baseUrl: result.data.baseUrl,
manifest: {
name: result.data.manifest.name,
skills: result.data.manifest.skills,
...result.data.manifest.mcpServers === undefined ? {} : { mcpServers: result.data.manifest.mcpServers },
},
}
}
function isOutside(root: string, candidate: string): boolean {
const path = relative(root, candidate)
/* v8 ignore next -- Different-drive Windows relative paths cannot be produced on POSIX coverage hosts. */
return path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)
}
async function sourcePath(pluginDirectory: string, sourceRoot: string, configured: string, kind: 'directory' | 'file'): Promise<string> {
if (isAbsolute(configured)) throw new Error(`DSH plugin asset path must be relative: ${JSON.stringify(configured)}`)
let path: string
try {
path = await realpath(resolve(pluginDirectory, configured))
} catch (cause) {
throw new Error(`DSH plugin asset does not exist: ${JSON.stringify(configured)}`, { cause })
}
if (isOutside(sourceRoot, path)) {
throw new Error(`DSH plugin asset escapes its plugin source root: ${JSON.stringify(configured)}`)
}
const info = await stat(path)
if (kind === 'directory' ? !info.isDirectory() : !info.isFile()) {
throw new Error(`DSH plugin asset is not a ${kind}: ${JSON.stringify(configured)}`)
}
return path
}
function wrapperSource(manifest: PreparedPluginManifest): string {
return [
'// Generated by dsh-plugin-prepare. Do not edit.',
`const manifest = ${JSON.stringify(manifest)}`,
`export const name = ${JSON.stringify(manifest.name)}`,
"export const inject = ['loader']",
'export async function apply(ctx) {',
` const runtime = ctx.loader.builtins[${JSON.stringify(REPOSITORY_PLUGIN_BUILTIN)}]`,
` if (runtime === undefined) throw new Error(${JSON.stringify(`missing Cordis builtin ${REPOSITORY_PLUGIN_BUILTIN}`)})`,
' await ctx.plugin(runtime, { baseUrl: import.meta.url, manifest })',
'}',
'',
].join('\n')
}
/**
* Validate and package one `.dsh-plugin` directory into static assets plus a fixed wrapper.
* @param directory - `.dsh-plugin` package directory; defaults to the prepare process cwd.
* @returns the generated static manifest.
*/
export async function prepareDshPlugin(directory: string = process.cwd()): Promise<PreparedPluginManifest> {
const pluginDirectory = await realpath(resolve(directory))
let packageValue: unknown
try {
packageValue = JSON.parse(await readFile(join(pluginDirectory, 'package.json'), 'utf8')) as unknown
} catch (cause) {
throw new Error(`failed to read DSH plugin package metadata in ${pluginDirectory}`, { cause })
}
const parsed = sourcePackageSchema.safeParse(packageValue)
if (!parsed.success) throw formatZodError('invalid package.json#dsh', parsed.error)
const sourceRoot = await realpath(dirname(pluginDirectory))
const skillSources: string[] = []
for (const configured of parsed.data.dsh.skills) {
const source = await sourcePath(pluginDirectory, sourceRoot, configured, 'directory')
if (!isOutside(source, pluginDirectory)) {
throw new Error(`DSH skill root cannot contain the .dsh-plugin package: ${JSON.stringify(configured)}`)
}
skillSources.push(source)
}
let mcpSource: string | undefined
if (parsed.data.dsh.mcpServers !== undefined) {
mcpSource = await sourcePath(pluginDirectory, sourceRoot, parsed.data.dsh.mcpServers, 'file')
parseMcpDocument(await readFile(mcpSource, 'utf8'))
}
const manifest: PreparedPluginManifest = {
name: parsed.data.name,
skills: skillSources.map((_, index) => `${PREPARED_ASSET_DIRECTORY}/skills/${index}`),
...mcpSource === undefined ? {} : { mcpServers: `${PREPARED_ASSET_DIRECTORY}/.mcp.json` },
}
const staging = await mkdtemp(join(pluginDirectory, '.dsh-plugin-prepare-'))
try {
const stagedAssets = join(staging, PREPARED_ASSET_DIRECTORY)
await mkdir(join(stagedAssets, 'skills'), { recursive: true })
await Promise.all(skillSources.map((source, index) => cp(source, join(stagedAssets, 'skills', String(index)), {
recursive: true,
force: false,
errorOnExist: true,
})))
if (mcpSource !== undefined) await copyFile(mcpSource, join(stagedAssets, '.mcp.json'))
await writeFile(join(staging, PREPARED_ENTRY_FILENAME), wrapperSource(manifest))
await rm(join(pluginDirectory, PREPARED_ASSET_DIRECTORY), { recursive: true, force: true })
await rm(join(pluginDirectory, PREPARED_ENTRY_FILENAME), { force: true })
await rename(stagedAssets, join(pluginDirectory, PREPARED_ASSET_DIRECTORY))
await rename(join(staging, PREPARED_ENTRY_FILENAME), join(pluginDirectory, PREPARED_ENTRY_FILENAME))
} finally {
await rm(staging, { recursive: true, force: true })
}
return manifest
}
@@ -0,0 +1,97 @@
/**
* Restricted repository-plugin runtime for static skills and common MCP definitions.
* @module @deepseek-ai/dsh-repository-plugin
*/
import { readFile } from 'node:fs/promises'
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Context } from 'cordis'
import type {} from '@cordisjs/plugin-loader'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as McpClient from '@deepseek-ai/dsh-mcp-client'
import {
REPOSITORY_PLUGIN_BUILTIN,
parsePreparedPluginConfig,
type PreparedPluginConfig,
} from './format.ts'
import { parseMcpDocument, resolveMcpServers } from './mcp.ts'
export {
PREPARED_ASSET_DIRECTORY,
PREPARED_ENTRY_FILENAME,
REPOSITORY_PLUGIN_BUILTIN,
prepareDshPlugin,
type PreparedPluginManifest,
} from './format.ts'
/** Cordis plugin name used by Loader diagnostics. */
export const name = 'repository-plugin'
/** Loader service required to register the fixed prepared-wrapper builtin. */
export const inject = ['loader']
function preparedPath(baseUrl: string, configured: string): string {
if (isAbsolute(configured)) throw new Error(`prepared DSH plugin path must be relative: ${JSON.stringify(configured)}`)
const directory = dirname(fileURLToPath(baseUrl))
const path = resolve(directory, configured)
const rel = relative(directory, path)
/* v8 ignore next -- Different-drive Windows relative paths cannot be produced on POSIX coverage hosts. */
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
throw new Error(`prepared DSH plugin path escapes its package: ${JSON.stringify(configured)}`)
}
return path
}
async function applyPrepared(ctx: Context, value: PreparedPluginConfig): Promise<void> {
const config = parsePreparedPluginConfig(value)
const directory = dirname(fileURLToPath(config.baseUrl))
const skillDirectories = config.manifest.skills.map(path => preparedPath(config.baseUrl, path))
const mcpConfigs = config.manifest.mcpServers === undefined
? []
: resolveMcpServers(
parseMcpDocument(await readFile(preparedPath(config.baseUrl, config.manifest.mcpServers), 'utf8')),
process.env,
directory,
).map(input => McpClient.Config(input as never))
await ctx.effect(async function* () {
if (skillDirectories.length > 0) {
const skills = ctx.plugin(SkillLocal, {
providerName: `repository:${config.manifest.name}`,
includeDefaultRoots: false,
customSkillDirs: skillDirectories,
watch: false,
})
await skills
yield skills.dispose
}
for (const mcpConfig of mcpConfigs) {
const mcp = ctx.plugin(McpClient, mcpConfig)
await mcp
yield mcp.dispose
}
}, `repository-plugin(${config.manifest.name})`)
}
const preparedRuntime = {
name: 'repository-plugin-runtime',
apply: applyPrepared,
}
/**
* Register the DSH-owned runtime as the Loader builtin used by fixed prepared wrappers.
* @param ctx - plugin context carrying the Loader service.
*/
export function apply(ctx: Context): void {
if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] !== undefined) {
throw new Error(`Loader builtin ${REPOSITORY_PLUGIN_BUILTIN} is already registered`)
}
ctx.effect(function* () {
ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] = preparedRuntime
yield () => {
if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] === preparedRuntime) {
Reflect.deleteProperty(ctx.loader.builtins, REPOSITORY_PLUGIN_BUILTIN)
}
}
}, 'repository-plugin Loader builtin')
}
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-repository-plugin`.
* @module @deepseek-ai/dsh-repository-plugin/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-repository-plugin'
/** Cordis companion plugin name. */
export const name = 'repository-plugin-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the package owns no service state; Loader fibers and the existing skill
* and MCP owners expose the authoritative lifecycle relationships for its composed children.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,145 @@
/**
* Parser for the common `.mcp.json` file consumed by prepared repository plugins.
* @module
*/
import { z } from 'zod'
const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/
const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
const PLACEHOLDER_PATTERN = /\$\{([^}]*)\}/g
const stringMap = z.record(z.string(), z.string())
const stdioServerSchema = z.object({
type: z.literal('stdio').optional(),
command: z.string().min(1),
args: z.array(z.string()).optional(),
env: stringMap.optional(),
}).strict()
const httpServerSchema = z.object({
type: z.literal('http'),
url: z.string().min(1),
headers: stringMap.optional(),
}).strict()
const documentSchema = z.object({
mcpServers: z.record(z.string(), z.union([stdioServerSchema, httpServerSchema])),
}).strict()
/** One supported server entry from the common `.mcp.json` format. */
export type McpServerDefinition = z.infer<typeof stdioServerSchema> | z.infer<typeof httpServerSchema>
/** Parsed common MCP document before process-environment expansion. */
export interface McpDocument {
mcpServers: Record<string, McpServerDefinition>
}
/** Resolved input handed to the existing `dsh-mcp-client` Config schema. */
export type ResolvedMcpServer =
| {
transport: 'stdio'
serverName: string
command: string
args: string[]
env: Record<string, string>
cwd: string
}
| {
transport: 'streamable-http'
serverName: string
url: string
headers: Record<string, string>
}
function assertTemplate(value: string, location: string): void {
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) {
const name = match[1] as string
if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
throw new Error(`${location} contains an unsupported environment placeholder ${JSON.stringify(match[0])}`)
}
}
if (value.replace(PLACEHOLDER_PATTERN, '').includes('${')) {
throw new Error(`${location} contains an unterminated environment placeholder`)
}
}
function visitStrings(serverName: string, definition: McpServerDefinition, visit: (value: string, location: string) => void): void {
if ('command' in definition) {
visit(definition.command, `mcpServers.${serverName}.command`)
definition.args?.forEach((value, index) => { visit(value, `mcpServers.${serverName}.args[${index}]`) })
Object.entries(definition.env ?? {}).forEach(([name, value]) => { visit(value, `mcpServers.${serverName}.env.${name}`) })
return
}
visit(definition.url, `mcpServers.${serverName}.url`)
Object.entries(definition.headers ?? {}).forEach(([name, value]) => { visit(value, `mcpServers.${serverName}.headers.${name}`) })
}
/**
* Parse and validate one common `.mcp.json` document without resolving environment values.
* @param content - UTF-8 JSON document.
* @returns the supported stdio and Streamable HTTP server definitions.
*/
export function parseMcpDocument(content: string): McpDocument {
let value: unknown
try {
value = JSON.parse(content) as unknown
} catch (cause) {
throw new Error('invalid .mcp.json: expected JSON', { cause })
}
const result = documentSchema.safeParse(value)
if (!result.success) throw new Error(`invalid .mcp.json:\n${z.prettifyError(result.error)}`)
for (const [serverName, definition] of Object.entries(result.data.mcpServers)) {
if (!SERVER_NAME_PATTERN.test(serverName)) {
throw new Error(`invalid .mcp.json: server name ${JSON.stringify(serverName)} must match [A-Za-z0-9_-]{1,32}`)
}
visitStrings(serverName, definition, assertTemplate)
}
return result.data
}
function expand(value: string, environment: NodeJS.ProcessEnv, location: string): string {
return value.replace(PLACEHOLDER_PATTERN, (_placeholder, name: string) => {
const replacement = environment[name]
if (replacement === undefined) throw new Error(`${location} requires missing environment variable ${name}`)
return replacement
})
}
function expandMap(values: Record<string, string> | undefined, environment: NodeJS.ProcessEnv, location: string): Record<string, string> {
return Object.fromEntries(Object.entries(values ?? {}).map(([name, value]) => [
name,
expand(value, environment, `${location}.${name}`),
]))
}
/**
* Resolve supported MCP definitions to inputs for the existing MCP client.
* @param document - validated common MCP document.
* @param environment - process environment used for exact `${NAME}` expansion.
* @param cwd - prepared plugin directory used for stdio child processes.
* @returns one existing-client config input per declared server.
*/
export function resolveMcpServers(document: McpDocument, environment: NodeJS.ProcessEnv, cwd: string): ResolvedMcpServer[] {
return Object.entries(document.mcpServers).map(([serverName, definition]) => {
if ('command' in definition) {
return {
transport: 'stdio',
serverName,
command: expand(definition.command, environment, `mcpServers.${serverName}.command`),
args: (definition.args ?? []).map((value, index) => expand(value, environment, `mcpServers.${serverName}.args[${index}]`)),
env: expandMap(definition.env, environment, `mcpServers.${serverName}.env`),
cwd,
}
}
const url = expand(definition.url, environment, `mcpServers.${serverName}.url`)
const protocol = new URL(url).protocol
if (protocol !== 'http:' && protocol !== 'https:') {
throw new Error(`mcpServers.${serverName}.url must use http or https`)
}
return {
transport: 'streamable-http',
serverName,
url,
headers: expandMap(definition.headers, environment, `mcpServers.${serverName}.headers`),
}
})
}