mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(repository-plugin): load trusted package code
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Static repository-plugin preparation and prepared-manifest validation.
|
||||
* Trusted repository-package preparation and prepared-manifest validation.
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -12,21 +12,36 @@ import { parseMcpDocument } from './mcp.ts'
|
||||
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. */
|
||||
/** Loader builtin used by every generated repository wrapper. */
|
||||
export const REPOSITORY_PLUGIN_BUILTIN = 'dsh-repository-plugin'
|
||||
/** Exact host-owned command required by the repository package `prepack` lifecycle. */
|
||||
/** Host-owned command that repository package `prepack` lifecycles must invoke. */
|
||||
export const REPOSITORY_PLUGIN_PREPARE_COMMAND = 'dsh-plugin-prepare'
|
||||
|
||||
/**
|
||||
* Whether a package lifecycle declaration names the host preparation helper.
|
||||
* @param script - package-authored lifecycle command.
|
||||
* @returns true when the required helper command is present.
|
||||
*/
|
||||
export function hasRepositoryPrepareCommand(script: string): boolean {
|
||||
return script.includes(REPOSITORY_PLUGIN_PREPARE_COMMAND)
|
||||
}
|
||||
|
||||
const prepackSchema = z.string().min(1).refine(
|
||||
hasRepositoryPrepareCommand,
|
||||
{ message: `must invoke ${REPOSITORY_PLUGIN_PREPARE_COMMAND}` },
|
||||
)
|
||||
|
||||
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',
|
||||
entry: z.string().min(1).optional(),
|
||||
}).strict().refine(value => value.skills.length > 0 || value.mcpServers !== undefined || value.entry !== undefined, {
|
||||
message: 'declare at least one skill root, mcpServers file, or compiled entry',
|
||||
})
|
||||
const sourcePackageSchema = z.looseObject({
|
||||
name: z.string().min(1),
|
||||
scripts: z.looseObject({
|
||||
prepack: z.literal(REPOSITORY_PLUGIN_PREPARE_COMMAND),
|
||||
prepack: prepackSchema,
|
||||
}),
|
||||
dsh: sourceMetadataSchema,
|
||||
})
|
||||
@@ -34,6 +49,7 @@ const preparedManifestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
skills: z.array(z.string().min(1)),
|
||||
mcpServers: z.string().min(1).optional(),
|
||||
entry: z.string().min(1).optional(),
|
||||
}).strict()
|
||||
const preparedConfigSchema = z.object({
|
||||
// Wrappers pass import.meta.url, which is always file: for an installed
|
||||
@@ -43,11 +59,12 @@ const preparedConfigSchema = z.object({
|
||||
manifest: preparedManifestSchema,
|
||||
}).strict()
|
||||
|
||||
/** Static manifest embedded in the generated wrapper. */
|
||||
/** Prepared manifest embedded in the generated wrapper. */
|
||||
export interface PreparedPluginManifest {
|
||||
name: string
|
||||
skills: string[]
|
||||
mcpServers?: string
|
||||
entry?: string
|
||||
}
|
||||
|
||||
/** Untrusted generated-wrapper config accepted by the DSH-owned runtime builtin. */
|
||||
@@ -74,6 +91,7 @@ export function parsePreparedPluginConfig(value: unknown): PreparedPluginConfig
|
||||
name: result.data.manifest.name,
|
||||
skills: result.data.manifest.skills,
|
||||
...result.data.manifest.mcpServers === undefined ? {} : { mcpServers: result.data.manifest.mcpServers },
|
||||
...result.data.manifest.entry === undefined ? {} : { entry: result.data.manifest.entry },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -121,28 +139,49 @@ function wrapperSource(manifest: PreparedPluginManifest): string {
|
||||
...manifest.skills.length > 0 ? ['skills'] : [],
|
||||
...manifest.mcpServers === undefined ? [] : ['tools'],
|
||||
]
|
||||
const entryHelpers = manifest.entry === undefined ? [] : [
|
||||
'function unwrap(exports) {',
|
||||
' const value = exports?.default ?? exports',
|
||||
' return value?.__esModule ? (value.default ?? value) : value',
|
||||
'}',
|
||||
]
|
||||
const entryApply = manifest.entry === undefined ? [] : [
|
||||
' const repositoryPlugin = unwrap(await import(manifest.entry))',
|
||||
" await mount(ctx, repositoryPlugin, 'repository Plugin entry')",
|
||||
]
|
||||
return [
|
||||
'// Generated by dsh-plugin-prepare. Do not edit.',
|
||||
`const manifest = ${JSON.stringify(manifest)}`,
|
||||
'const FIBER_ACTIVE = 2',
|
||||
`export const name = ${JSON.stringify(manifest.name)}`,
|
||||
`export const inject = ${JSON.stringify(inject)}`,
|
||||
...entryHelpers,
|
||||
'async function mount(ctx, plugin, label, config) {',
|
||||
' const fiber = ctx.plugin(plugin, config)',
|
||||
' await fiber',
|
||||
' if (fiber.state !== FIBER_ACTIVE) {',
|
||||
' const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)',
|
||||
" throw new Error(`${label} did not activate (waiting for services: ${missing.join(', ') || 'unknown'})`)",
|
||||
' }',
|
||||
'}',
|
||||
'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 })',
|
||||
" await mount(ctx, runtime, 'repository Plugin runtime', { baseUrl: import.meta.url, manifest })",
|
||||
...entryApply,
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and package one `.dsh-plugin` directory into static assets plus a fixed wrapper.
|
||||
* Validate and package one `.dsh-plugin` directory into copied assets plus a generated wrapper.
|
||||
* Outputs are staged and committed by rename, but the final publish (remove
|
||||
* old outputs, rename assets, rename entry) is not one atomic step: a crash
|
||||
* mid-publish can leave assets without an entry or neither. Rerunning prepare
|
||||
* repairs the package; partial outputs are never importable as a plugin.
|
||||
* @param directory - `.dsh-plugin` package directory; defaults to the prepare process cwd.
|
||||
* @returns the generated static manifest.
|
||||
* @returns the generated prepared manifest.
|
||||
*/
|
||||
export async function prepareDshPlugin(directory: string = process.cwd()): Promise<PreparedPluginManifest> {
|
||||
const pluginDirectory = await realpath(resolve(directory))
|
||||
@@ -169,11 +208,17 @@ export async function prepareDshPlugin(directory: string = process.cwd()): Promi
|
||||
mcpSource = await sourcePath(pluginDirectory, sourceRoot, parsed.data.dsh.mcpServers, 'file')
|
||||
parseMcpDocument(await readFile(mcpSource, 'utf8'))
|
||||
}
|
||||
let entry: string | undefined
|
||||
if (parsed.data.dsh.entry !== undefined) {
|
||||
const entrySource = await sourcePath(pluginDirectory, pluginDirectory, parsed.data.dsh.entry, 'file')
|
||||
entry = `./${relative(pluginDirectory, entrySource).split(sep).join('/')}`
|
||||
}
|
||||
|
||||
const manifest: PreparedPluginManifest = {
|
||||
name: parsed.data.name,
|
||||
skills: skillSources.map((_, index) => `${PREPARED_ASSET_DIRECTORY}/skills/${index}`),
|
||||
...mcpSource === undefined ? {} : { mcpServers: `${PREPARED_ASSET_DIRECTORY}/.mcp.json` },
|
||||
...entry === undefined ? {} : { entry },
|
||||
}
|
||||
const staging = await mkdtemp(join(pluginDirectory, '.dsh-plugin-prepare-'))
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Restricted repository-plugin runtime for static skills and common MCP definitions.
|
||||
* Trusted repository-package runtime for code, skills, and common MCP definitions.
|
||||
* @module @deepseek-ai/dsh-repository-plugin
|
||||
*/
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { z } from 'zod'
|
||||
import {
|
||||
PREPARED_ENTRY_FILENAME,
|
||||
REPOSITORY_PLUGIN_PREPARE_COMMAND,
|
||||
hasRepositoryPrepareCommand,
|
||||
} from './format.ts'
|
||||
|
||||
// Value mirror: Cordis's const enum has no runtime object to import. Keep
|
||||
@@ -80,7 +81,10 @@ export async function createRepositoryPrepareCommand(): Promise<RepositoryPrepar
|
||||
const GITHUB_SOURCE_PATTERN = /^github:([^/\s#&]+)\/([^/\s#&]+)#([^\s#&]+)(?:&path:(\/[^\s&]+))?$/
|
||||
const installedPackageSchema = z.looseObject({
|
||||
scripts: z.looseObject({
|
||||
prepack: z.literal(REPOSITORY_PLUGIN_PREPARE_COMMAND),
|
||||
prepack: z.string().min(1).refine(
|
||||
hasRepositoryPrepareCommand,
|
||||
{ message: `must invoke ${REPOSITORY_PLUGIN_PREPARE_COMMAND}` },
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -127,7 +131,7 @@ async function assertInstalledPackageMetadata(directory: string): Promise<void>
|
||||
}
|
||||
const result = installedPackageSchema.safeParse(value)
|
||||
if (!result.success) {
|
||||
throw new Error(`installed DSH plugin package must declare scripts.prepack as ${JSON.stringify(REPOSITORY_PLUGIN_PREPARE_COMMAND)}:\n${z.prettifyError(result.error)}`)
|
||||
throw new Error(`installed DSH plugin package must declare a non-empty scripts.prepack that invokes ${JSON.stringify(REPOSITORY_PLUGIN_PREPARE_COMMAND)}:\n${z.prettifyError(result.error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user