refactor(packages): merge timeout/ into guard/, rename cordis/ to self-modification/
git mv timeout-policy beside repeat-tool-guard (both are loop-hygiene policies on the tool-execution pipeline, and the timeout/ group name collided with util/timeout) and tool-cordis into self-modification/ (naming the role the framework name obscured). Merged/renamed group README triplets, tsconfig globs, generator sources, hierarchy tables, catalogs, and the timeout-policy design note's group references follow. Adds the fifth FIXME marker (dsh-timeout-guard, recorded as a suggestion to settle at resolution time). guard + self-modification suites: 197 passed.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Restricted repository-plugin runtime for static skills and common MCP definitions.
|
||||
* @module @deepseek-ai/dsh-repository-plugin
|
||||
*/
|
||||
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { dirname, isAbsolute, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@cordisjs/plugin-loader'
|
||||
import { RepositoryCache } from '@cordisjs/plugin-loader/repository'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import * as McpClient from '@deepseek-ai/dsh-mcp-client'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
REPOSITORY_PLUGIN_BUILTIN,
|
||||
isOutside,
|
||||
parsePreparedPluginConfig,
|
||||
type PreparedPluginConfig,
|
||||
} from './format.ts'
|
||||
import { parseMcpDocument, resolveMcpServers } from './mcp.ts'
|
||||
import {
|
||||
loadPreparedRepository,
|
||||
resolveRepositoryCacheDirectory,
|
||||
resolveRepositorySpecifier,
|
||||
} from './source.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']
|
||||
|
||||
/** Repository Plugin runtime and source-list configuration. */
|
||||
export interface Config {
|
||||
/** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
|
||||
repositories?: string[]
|
||||
/** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
|
||||
cacheDir?: string
|
||||
}
|
||||
|
||||
export const Config = z.object({
|
||||
repositories: z.array(z.string().min(1)).default([]),
|
||||
cacheDir: z.string().min(1).optional(),
|
||||
}).strict().default({ repositories: [] })
|
||||
|
||||
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)
|
||||
if (isOutside(directory, path)) {
|
||||
throw new Error(`prepared DSH plugin path escapes its package: ${JSON.stringify(configured)}`)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
async function preparedDirectory(baseUrl: string, configured: string): Promise<string> {
|
||||
const path = preparedPath(baseUrl, configured)
|
||||
// A manifest-declared skill root missing from the installed package (files/
|
||||
// .npmignore dropping generated outputs, a damaged cache entry) must fail
|
||||
// the plugin load: the skill provider treats an absent root as legitimately
|
||||
// empty, which would silently mount a skill-less plugin.
|
||||
let info
|
||||
try {
|
||||
info = await stat(path)
|
||||
} catch (cause) {
|
||||
throw new Error(`prepared DSH plugin skill root is missing from the installed package: ${JSON.stringify(configured)}`, { cause })
|
||||
}
|
||||
if (!info.isDirectory()) {
|
||||
throw new Error(`prepared DSH plugin skill root is not a directory: ${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 = await Promise.all(config.manifest.skills.map(path => preparedDirectory(config.baseUrl, path)))
|
||||
const mcpConfigs = config.manifest.mcpServers === undefined
|
||||
? []
|
||||
: resolveMcpServers(
|
||||
parseMcpDocument(await readFile(preparedPath(config.baseUrl, config.manifest.mcpServers), 'utf8')),
|
||||
process.env,
|
||||
directory,
|
||||
// Schemastery call signatures collapse the parameter to `never` under
|
||||
// NodeNext; ResolvedMcpServer is shaped for the Config union by design.
|
||||
).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 async function apply(ctx: Context, config: Config = {}): Promise<void> {
|
||||
if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] !== undefined) {
|
||||
throw new Error(`Loader builtin ${REPOSITORY_PLUGIN_BUILTIN} is already registered`)
|
||||
}
|
||||
const repositories = (config.repositories ?? []).map(resolveRepositorySpecifier)
|
||||
if (new Set(repositories).size !== repositories.length) {
|
||||
throw new Error('repository sources must resolve to unique exact specifiers')
|
||||
}
|
||||
const cache = new RepositoryCache(resolveRepositoryCacheDirectory(config.cacheDir))
|
||||
await ctx.effect(async 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)
|
||||
}
|
||||
}
|
||||
for (const repository of repositories) {
|
||||
const plugin = await loadPreparedRepository(ctx, cache, repository)
|
||||
yield plugin.dispose
|
||||
}
|
||||
}, 'repository-plugin runtime and sources')
|
||||
}
|
||||
Reference in New Issue
Block a user