@dshforge/agent-plugins 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -2
- package/cordis.patch.yml +5 -0
- package/lib/client.js +771 -0
- package/lib/discovery.js +191 -0
- package/lib/index.js +45 -0
- package/lib/install.js +199 -0
- package/lib/mcp.js +186 -0
- package/lib/paths.js +47 -0
- package/lib/rpc.js +147 -0
- package/lib/skills.js +54 -0
- package/lib/validation.js +218 -0
- package/package.json +65 -4
- package/schemas/1.0.0/mcp.schema.json +120 -0
- package/schemas/1.0.0/plugin.schema.json +65 -0
package/lib/rpc.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { clientRequestSchema } from '@deepseek-ai/dsh-client-connection'
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { resolveExistingInside } from './paths.js'
|
|
5
|
+
import { errorMessage } from './validation.js'
|
|
6
|
+
|
|
7
|
+
/** Build the catalog-only Connection RPC handler without exposing config values. */
|
|
8
|
+
export function createAgentPluginsRpcHandler(catalog, { install } = {}) {
|
|
9
|
+
const readCatalog = () => typeof catalog === 'function' ? catalog() : catalog
|
|
10
|
+
return async (endpoint, payload, signal, peer) => {
|
|
11
|
+
if (peer.principal.kind !== 'owner') {
|
|
12
|
+
return failure('agent-plugins/forbidden', 'Agent Plugins catalog is available to owner peers only')
|
|
13
|
+
}
|
|
14
|
+
if (signal.aborted) return failure('agent-plugins/cancelled', 'Agent Plugins catalog request was cancelled')
|
|
15
|
+
if (endpoint === 'catalog') return { ok: true, value: projectCatalog(readCatalog()) }
|
|
16
|
+
if (endpoint === 'files') return listPluginDirectory(readCatalog(), payload)
|
|
17
|
+
if (endpoint === 'install' && install !== undefined) {
|
|
18
|
+
if (typeof payload?.spec !== 'string') return failure('agent-plugins/invalid-request', 'install requires a Git spec')
|
|
19
|
+
try {
|
|
20
|
+
return { ok: true, value: await install(payload.spec, signal) }
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return failure(typeof error?.code === 'string' && error.code.startsWith('agent-plugins/')
|
|
23
|
+
? error.code
|
|
24
|
+
: 'agent-plugins/install-failed', errorMessage(error))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return failure('agent-plugins/not-found', `Unknown Agent Plugins endpoint ${JSON.stringify(endpoint)}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function listPluginDirectory(catalog, payload) {
|
|
32
|
+
if (typeof payload?.root !== 'string' || typeof payload.path !== 'string') {
|
|
33
|
+
return failure('agent-plugins/invalid-request', 'files requires root and path')
|
|
34
|
+
}
|
|
35
|
+
const plugin = catalog.plugins.find(entry => entry.directoryName === payload.root)
|
|
36
|
+
if (!plugin || plugin.status !== 'ready') return failure('agent-plugins/not-found', 'Plugin directory was not found')
|
|
37
|
+
const relative = payload.path
|
|
38
|
+
if (relative.includes('\0') || relative.includes('\\') || relative.startsWith('/') || relative.split('/').includes('..')) {
|
|
39
|
+
return failure('agent-plugins/invalid-path', 'Directory path must stay within the plugin root')
|
|
40
|
+
}
|
|
41
|
+
const directory = join(plugin.root, relative)
|
|
42
|
+
let resolved
|
|
43
|
+
try {
|
|
44
|
+
resolved = await resolveExistingInside(plugin.root, directory)
|
|
45
|
+
if (!(await stat(resolved)).isDirectory()) {
|
|
46
|
+
return failure('agent-plugins/not-directory', 'Requested path is not a directory')
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return failure('agent-plugins/invalid-path', errorMessage(error))
|
|
50
|
+
}
|
|
51
|
+
let entries
|
|
52
|
+
try {
|
|
53
|
+
entries = await readdir(resolved, { withFileTypes: true })
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return failure('agent-plugins/unavailable', errorMessage(error))
|
|
56
|
+
}
|
|
57
|
+
const rows = entries
|
|
58
|
+
.filter(entry => !entry.isSymbolicLink() && (entry.isDirectory() || entry.isFile()))
|
|
59
|
+
.map(entry => ({
|
|
60
|
+
name: entry.name,
|
|
61
|
+
kind: entry.isDirectory() ? 'directory' : 'file',
|
|
62
|
+
path: relative ? `${relative}/${entry.name}` : entry.name,
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === 'directory' ? -1 : 1)
|
|
65
|
+
return { ok: true, value: { path: relative, entries: rows.slice(0, 200), truncated: rows.length > 200 } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Register the catalog over the existing authenticated Connection fetch bridge. */
|
|
69
|
+
export function registerAgentPluginsRpc(ctx, catalog, options) {
|
|
70
|
+
const handler = createAgentPluginsRpcHandler(catalog, options)
|
|
71
|
+
return ctx.get('webServer').register({
|
|
72
|
+
kind: 'prefix',
|
|
73
|
+
path: '/agent-plugins',
|
|
74
|
+
handler: async (request, response) => {
|
|
75
|
+
const admission = ctx.connection.admit(request)
|
|
76
|
+
if ('rejection' in admission) {
|
|
77
|
+
response.writeHead(admission.rejection).end(admission.rejection === 401 ? 'unauthorized' : 'forbidden')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (request.method !== 'POST') {
|
|
81
|
+
response.writeHead(404).end('not found')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const endpoint = new URL(request.url, 'http://localhost').pathname.slice('/agent-plugins/'.length)
|
|
85
|
+
if (!['catalog', 'files', 'install'].includes(endpoint)) {
|
|
86
|
+
response.writeHead(404).end('not found')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
let message
|
|
90
|
+
try {
|
|
91
|
+
message = clientRequestSchema.parse(await readJson(request))
|
|
92
|
+
} catch {
|
|
93
|
+
response.writeHead(400).end('invalid client-request message')
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (message.method !== endpoint) {
|
|
97
|
+
response.writeHead(400).end('method does not match endpoint')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const controller = new AbortController()
|
|
101
|
+
response.on('close', () => controller.abort())
|
|
102
|
+
const result = await handler(endpoint, message.payload, controller.signal, admission.peer)
|
|
103
|
+
response.writeHead(200, { 'content-type': 'application/json' })
|
|
104
|
+
response.end(JSON.stringify({ type: 'server-response', rpcId: message.rpcId, result }))
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readJson(request) {
|
|
110
|
+
const chunks = []
|
|
111
|
+
let size = 0
|
|
112
|
+
for await (const chunk of request) {
|
|
113
|
+
size += chunk.byteLength
|
|
114
|
+
if (size > 1024 * 1024) {
|
|
115
|
+
request.destroy()
|
|
116
|
+
throw new Error('request body exceeds 1 MiB')
|
|
117
|
+
}
|
|
118
|
+
chunks.push(chunk)
|
|
119
|
+
}
|
|
120
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function failure(code, message) {
|
|
124
|
+
return { ok: false, error: { code, message, details: {} } }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Keep transport config useful to the UI without exposing env or header values. */
|
|
128
|
+
function projectCatalog(catalog) {
|
|
129
|
+
return {
|
|
130
|
+
...catalog,
|
|
131
|
+
plugins: catalog.plugins.map(plugin => ({
|
|
132
|
+
...plugin,
|
|
133
|
+
mcpServers: plugin.mcpServers.map(server => ({
|
|
134
|
+
name: server.name,
|
|
135
|
+
type: server.type,
|
|
136
|
+
...server.type === 'stdio' ? {
|
|
137
|
+
command: server.command,
|
|
138
|
+
args: server.args ?? [],
|
|
139
|
+
...server.cwd === undefined ? {} : { cwd: server.cwd },
|
|
140
|
+
} : {
|
|
141
|
+
url: server.url,
|
|
142
|
+
headerNames: Object.keys(server.headers ?? {}),
|
|
143
|
+
},
|
|
144
|
+
})),
|
|
145
|
+
})),
|
|
146
|
+
}
|
|
147
|
+
}
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BUNDLED_SKILL_RANK } from '@deepseek-ai/dsh-skill'
|
|
2
|
+
import { discoverAgentPlugins, loadAgentSkill } from './discovery.js'
|
|
3
|
+
import { INVALID_AGENT_PLUGIN, isMissingPath } from './validation.js'
|
|
4
|
+
|
|
5
|
+
const PROVIDER = 'agent-plugins'
|
|
6
|
+
const INVOCATION = { modelInvocable: true, userInvocable: true }
|
|
7
|
+
|
|
8
|
+
/** Create a strict Agent Plugins v1 skill provider with on-demand body loading. */
|
|
9
|
+
export function createAgentSkillProvider({ pluginsDirectory }) {
|
|
10
|
+
return {
|
|
11
|
+
name: PROVIDER,
|
|
12
|
+
async list(options = {}) {
|
|
13
|
+
options.signal?.throwIfAborted()
|
|
14
|
+
const catalog = await discoverAgentPlugins({ pluginsDirectory })
|
|
15
|
+
options.signal?.throwIfAborted()
|
|
16
|
+
return catalog.plugins.flatMap(plugin => plugin.skills.map(skill => ({
|
|
17
|
+
name: skill.name,
|
|
18
|
+
description: skill.description,
|
|
19
|
+
path: skill.path,
|
|
20
|
+
provider: PROVIDER,
|
|
21
|
+
source: PROVIDER,
|
|
22
|
+
rank: BUNDLED_SKILL_RANK,
|
|
23
|
+
invocation: INVOCATION,
|
|
24
|
+
resourceBase: { kind: 'directory', path: skill.directory },
|
|
25
|
+
locator: {
|
|
26
|
+
pluginRoot: plugin.root,
|
|
27
|
+
skillDirectory: skill.directory,
|
|
28
|
+
packageDirectory: skill.packageDirectory,
|
|
29
|
+
},
|
|
30
|
+
...skill.metadata === undefined ? {} : { metadata: skill.metadata },
|
|
31
|
+
})))
|
|
32
|
+
},
|
|
33
|
+
async get(candidate, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const document = await loadAgentSkill({ ...candidate.locator, signal: options.signal })
|
|
36
|
+
if (document === undefined) return undefined
|
|
37
|
+
return {
|
|
38
|
+
name: document.name,
|
|
39
|
+
description: document.description,
|
|
40
|
+
path: candidate.path,
|
|
41
|
+
provider: PROVIDER,
|
|
42
|
+
source: PROVIDER,
|
|
43
|
+
invocation: INVOCATION,
|
|
44
|
+
resourceBase: candidate.resourceBase,
|
|
45
|
+
content: document.content,
|
|
46
|
+
...document.metadata === undefined ? {} : { metadata: document.metadata },
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (isMissingPath(error) || error.code === INVALID_AGENT_PLUGIN) return undefined
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020.js'
|
|
2
|
+
import { validateHeaderName, validateHeaderValue } from 'node:http'
|
|
3
|
+
import { isIP } from 'node:net'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
const pluginSchema = require('../schemas/1.0.0/plugin.schema.json')
|
|
8
|
+
const mcpSchema = require('../schemas/1.0.0/mcp.schema.json')
|
|
9
|
+
|
|
10
|
+
export const PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'
|
|
11
|
+
export const MCP_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'
|
|
12
|
+
export const INVALID_AGENT_PLUGIN = 'AGENT_PLUGIN_INVALID'
|
|
13
|
+
|
|
14
|
+
const MANIFEST_FIELDS = new Set([
|
|
15
|
+
'$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository',
|
|
16
|
+
'license', 'keywords', 'extensions',
|
|
17
|
+
])
|
|
18
|
+
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
19
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true })
|
|
20
|
+
const validateManifestSchema = ajv.compile(pluginSchema)
|
|
21
|
+
ajv.addSchema(mcpSchema)
|
|
22
|
+
const validateMcpRootSchema = ajv.compile({
|
|
23
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
$schema: { const: MCP_SCHEMA },
|
|
27
|
+
mcpServers: { type: 'object' },
|
|
28
|
+
},
|
|
29
|
+
required: ['$schema', 'mcpServers'],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
})
|
|
32
|
+
const validateMcpServerSchema = ajv.getSchema(`${MCP_SCHEMA}#/$defs/server`)
|
|
33
|
+
|
|
34
|
+
/** Create one validation failure suitable for isolated component reporting. */
|
|
35
|
+
export function invalidAgentPlugin(message, path) {
|
|
36
|
+
const error = new Error(path ? `${path}: ${message}` : message)
|
|
37
|
+
error.code = INVALID_AGENT_PLUGIN
|
|
38
|
+
error.path = path
|
|
39
|
+
return error
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isPlainObject(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isMissingPath(error) {
|
|
47
|
+
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function errorMessage(error) {
|
|
51
|
+
return error instanceof Error ? error.message : String(error)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function schemaMessage(validate) {
|
|
55
|
+
return ajv.errorsText(validate.errors, { separator: '; ', maxErrors: 3 })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Validate and sanitize plugin.json, including the two non-fatal exceptions. */
|
|
59
|
+
export function validateManifest(value, path) {
|
|
60
|
+
if (!isPlainObject(value)) throw invalidAgentPlugin('plugin.json must contain an object', path)
|
|
61
|
+
const manifest = { ...value }
|
|
62
|
+
const diagnostics = []
|
|
63
|
+
for (const key of Object.keys(manifest)) {
|
|
64
|
+
if (MANIFEST_FIELDS.has(key)) continue
|
|
65
|
+
diagnostics.push({ component: 'manifest', path, message: `ignored unknown manifest field ${JSON.stringify(key)}` })
|
|
66
|
+
delete manifest[key]
|
|
67
|
+
}
|
|
68
|
+
if (Object.hasOwn(manifest, 'extensions') && !isPlainObject(manifest.extensions)) {
|
|
69
|
+
diagnostics.push({ component: 'manifest', path, message: 'ignored non-object extensions field' })
|
|
70
|
+
delete manifest.extensions
|
|
71
|
+
}
|
|
72
|
+
const extensions = manifest.extensions
|
|
73
|
+
if (extensions !== undefined) delete manifest.extensions
|
|
74
|
+
if (!validateManifestSchema(manifest)) {
|
|
75
|
+
throw invalidAgentPlugin(`invalid plugin.json: ${schemaMessage(validateManifestSchema)}`, path)
|
|
76
|
+
}
|
|
77
|
+
if (extensions !== undefined) manifest.extensions = extensions
|
|
78
|
+
return { manifest, diagnostics }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Validate the MCP document envelope without rejecting independent server entries. */
|
|
82
|
+
export function validateMcpDocument(value, manifestSchema, path) {
|
|
83
|
+
if (!validateMcpRootSchema(value)) {
|
|
84
|
+
throw invalidAgentPlugin(`invalid mcp.json: ${schemaMessage(validateMcpRootSchema)}`, path)
|
|
85
|
+
}
|
|
86
|
+
if (schemaVersion(value.$schema) !== schemaVersion(manifestSchema)) {
|
|
87
|
+
throw invalidAgentPlugin('plugin.json and mcp.json target different Agent Plugins versions', path)
|
|
88
|
+
}
|
|
89
|
+
if (!isPlainObject(value.mcpServers)) throw invalidAgentPlugin('mcpServers must be an object', path)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function schemaVersion(value) {
|
|
93
|
+
return /^https:\/\/agent-plugins\.org\/schemas\/([^/]+)\//.exec(value)?.[1]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Validate one MCP server entry and its transport-level URL/header semantics. */
|
|
97
|
+
export function validateMcpServer(name, value, path) {
|
|
98
|
+
if (!validateMcpServerSchema(value)) {
|
|
99
|
+
throw invalidAgentPlugin(`invalid MCP server ${JSON.stringify(name)}: ${schemaMessage(validateMcpServerSchema)}`, path)
|
|
100
|
+
}
|
|
101
|
+
if (value.type === 'stdio') {
|
|
102
|
+
if (!value.command.startsWith('./') && /[\\/]/.test(value.command)) {
|
|
103
|
+
throw invalidAgentPlugin(`MCP server ${JSON.stringify(name)} command must be a bare executable or ./ plugin-relative path`, path)
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
validateRemoteUrl(value.url, path)
|
|
108
|
+
validateHeaders(value.headers, path)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateRemoteUrl(value, path) {
|
|
112
|
+
let url
|
|
113
|
+
try {
|
|
114
|
+
url = new URL(value)
|
|
115
|
+
} catch {
|
|
116
|
+
throw invalidAgentPlugin('MCP endpoint must be an absolute HTTP(S) URL', path)
|
|
117
|
+
}
|
|
118
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
|
|
119
|
+
throw invalidAgentPlugin('MCP endpoint must be HTTP(S), without user information or a fragment', path)
|
|
120
|
+
}
|
|
121
|
+
if (url.protocol === 'http:' && !isLoopbackHostname(url.hostname)) {
|
|
122
|
+
throw invalidAgentPlugin('non-loopback MCP endpoints must use HTTPS', path)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isLoopbackHostname(hostname) {
|
|
127
|
+
if (hostname === 'localhost') return true
|
|
128
|
+
const normalized = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
|
|
129
|
+
const family = isIP(normalized)
|
|
130
|
+
return (family === 4 && normalized.startsWith('127.')) || (family === 6 && normalized === '::1')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validateHeaders(headers = {}, path) {
|
|
134
|
+
if (!isPlainObject(headers)) throw invalidAgentPlugin('MCP headers must be an object', path)
|
|
135
|
+
const names = new Set()
|
|
136
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
137
|
+
try {
|
|
138
|
+
validateHeaderName(name)
|
|
139
|
+
validateHeaderValue(name, value)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw invalidAgentPlugin(`invalid MCP header: ${errorMessage(error)}`, path)
|
|
142
|
+
}
|
|
143
|
+
const normalized = name.toLowerCase()
|
|
144
|
+
if (names.has(normalized)) throw invalidAgentPlugin(`duplicate MCP header ${JSON.stringify(name)}`, path)
|
|
145
|
+
names.add(normalized)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Parse and validate one SKILL.md document against the Agent Skills contract. */
|
|
150
|
+
export function parseSkillDocument(raw, expectedName, path) {
|
|
151
|
+
const { data, body } = parseFrontmatter(raw, path)
|
|
152
|
+
const name = requiredString(data, 'name', 64, path)
|
|
153
|
+
if (!SKILL_NAME.test(name) || name !== expectedName) {
|
|
154
|
+
throw invalidAgentPlugin(`skill name ${JSON.stringify(name)} must be kebab-case and match parent directory ${JSON.stringify(expectedName)}`, path)
|
|
155
|
+
}
|
|
156
|
+
const description = requiredString(data, 'description', 1024, path)
|
|
157
|
+
const result = {
|
|
158
|
+
name,
|
|
159
|
+
description,
|
|
160
|
+
content: body.trim(),
|
|
161
|
+
}
|
|
162
|
+
if (Object.hasOwn(data, 'license')) result.license = optionalString(data, 'license', undefined, path)
|
|
163
|
+
if (Object.hasOwn(data, 'compatibility')) result.compatibility = optionalString(data, 'compatibility', 500, path)
|
|
164
|
+
if (Object.hasOwn(data, 'allowed-tools')) result.allowedTools = optionalString(data, 'allowed-tools', undefined, path)
|
|
165
|
+
if (Object.hasOwn(data, 'metadata')) {
|
|
166
|
+
if (!isPlainObject(data.metadata) || Object.values(data.metadata).some(value => typeof value !== 'string')) {
|
|
167
|
+
throw invalidAgentPlugin('skill metadata must map strings to strings', path)
|
|
168
|
+
}
|
|
169
|
+
result.metadata = { ...data.metadata }
|
|
170
|
+
}
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function requiredString(data, key, maximum, path) {
|
|
175
|
+
const value = data[key]
|
|
176
|
+
if (typeof value !== 'string' || value.length < 1 || value.length > maximum) {
|
|
177
|
+
throw invalidAgentPlugin(`skill ${key} must be a string of 1-${maximum} characters`, path)
|
|
178
|
+
}
|
|
179
|
+
return value
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function optionalString(data, key, maximum, path) {
|
|
183
|
+
const value = data[key]
|
|
184
|
+
if (typeof value !== 'string' || value.length < 1 || (maximum !== undefined && value.length > maximum)) {
|
|
185
|
+
throw invalidAgentPlugin(`skill ${key} has an invalid string value`, path)
|
|
186
|
+
}
|
|
187
|
+
return value
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseFrontmatter(raw, path) {
|
|
191
|
+
const firstLineEnd = raw.indexOf('\n')
|
|
192
|
+
if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') {
|
|
193
|
+
throw invalidAgentPlugin('SKILL.md requires YAML frontmatter', path)
|
|
194
|
+
}
|
|
195
|
+
const start = firstLineEnd + 1
|
|
196
|
+
let lineStart = start
|
|
197
|
+
while (lineStart <= raw.length) {
|
|
198
|
+
const nextNewline = raw.indexOf('\n', lineStart)
|
|
199
|
+
const lineEnd = nextNewline < 0 ? raw.length : nextNewline
|
|
200
|
+
const line = raw.slice(lineStart, lineEnd).replace(/\r$/, '')
|
|
201
|
+
if (line === '---') {
|
|
202
|
+
const yamlText = raw.slice(start, lineStart)
|
|
203
|
+
let data
|
|
204
|
+
try {
|
|
205
|
+
data = parseYaml(yamlText)
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw invalidAgentPlugin(`invalid YAML frontmatter: ${errorMessage(error)}`, path)
|
|
208
|
+
}
|
|
209
|
+
if (!isPlainObject(data)) throw invalidAgentPlugin('skill frontmatter must be an object', path)
|
|
210
|
+
return { data, body: raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1) }
|
|
211
|
+
}
|
|
212
|
+
if (nextNewline < 0) break
|
|
213
|
+
lineStart = nextNewline + 1
|
|
214
|
+
}
|
|
215
|
+
throw invalidAgentPlugin('SKILL.md frontmatter has no closing delimiter', path)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
import { parse as parseYaml } from 'yaml'
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshforge/agent-plugins",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Agent Plugins v1 Skills and MCP support for DeepSeek Harness Web.",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/",
|
|
14
|
+
"schemas/",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node scripts/build.mjs",
|
|
20
|
+
"test": "node --test tests/*.test.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0",
|
|
24
|
+
"node": ">=24.9.0"
|
|
25
|
+
},
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
},
|
|
30
|
+
"client": {
|
|
31
|
+
"platform": "web",
|
|
32
|
+
"inject": [
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-settings-general",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
37
|
+
"@deepseek-ai/dsh-client-connection",
|
|
38
|
+
"@deepseek-ai/dsh-client-locale"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-util-workspace-path": ">=0.2.1-rc.3 <0.3.0",
|
|
44
|
+
"ajv": "^8.17.1",
|
|
45
|
+
"execa": "^9.6.0",
|
|
46
|
+
"yaml": "^2.4.2"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
50
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.3 <0.3.0",
|
|
51
|
+
"@deepseek-ai/dsh-mcp-client": ">=0.2.1-rc.3 <0.3.0",
|
|
52
|
+
"@deepseek-ai/dsh-skill": ">=0.2.1-rc.3 <0.3.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
56
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.3 <0.3.0",
|
|
57
|
+
"@deepseek-ai/dsh-mcp-client": ">=0.2.1-rc.3 <0.3.0",
|
|
58
|
+
"@deepseek-ai/dsh-skill": ">=0.2.1-rc.3 <0.3.0",
|
|
59
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.2.1-rc.3 <0.3.0",
|
|
60
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.3 <0.3.0",
|
|
61
|
+
"@modelcontextprotocol/client": "2.2.0",
|
|
62
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
63
|
+
"@modelcontextprotocol/server": "2.2.0",
|
|
64
|
+
"esbuild": "^0.25.0",
|
|
65
|
+
"zod": "4.4.3"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
4
|
+
"title": "Agent Plugins MCP Configuration",
|
|
5
|
+
"description": "Machine-readable schema for mcp.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"const": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
10
|
+
"description": "Canonical identifier of the MCP configuration schema for the Agent Plugins version targeted by this document."
|
|
11
|
+
},
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": {
|
|
15
|
+
"$ref": "#/$defs/server"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": ["$schema", "mcpServers"],
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"$defs": {
|
|
22
|
+
"server": {
|
|
23
|
+
"title": "MCP server",
|
|
24
|
+
"oneOf": [
|
|
25
|
+
{
|
|
26
|
+
"$ref": "#/$defs/stdioServer"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"$ref": "#/$defs/streamableHttpServer"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"$ref": "#/$defs/sseServer"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"stdioServer": {
|
|
37
|
+
"title": "stdio MCP server",
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"type": {
|
|
41
|
+
"const": "stdio"
|
|
42
|
+
},
|
|
43
|
+
"command": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
"description": "Executable token. Resolution rules are defined by the Agent Plugins specification."
|
|
47
|
+
},
|
|
48
|
+
"args": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"env": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"propertyNames": {
|
|
57
|
+
"not": {
|
|
58
|
+
"enum": ["PLUGIN_ROOT", "PLUGIN_DATA"]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"additionalProperties": {
|
|
62
|
+
"type": "string"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"cwd": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"pattern": "^(?:\\./|\\$\\{PLUGIN_ROOT\\}(?:/|$)|\\$\\{PLUGIN_DATA\\}(?:/|$))",
|
|
68
|
+
"description": "Plugin-relative, PLUGIN_ROOT-rooted, or PLUGIN_DATA-rooted working directory. Filesystem containment is validated separately."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": ["type", "command"],
|
|
72
|
+
"additionalProperties": false
|
|
73
|
+
},
|
|
74
|
+
"streamableHttpServer": {
|
|
75
|
+
"title": "Streamable HTTP MCP server",
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"type": {
|
|
79
|
+
"const": "streamable-http"
|
|
80
|
+
},
|
|
81
|
+
"url": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"minLength": 1,
|
|
84
|
+
"description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification."
|
|
85
|
+
},
|
|
86
|
+
"headers": {
|
|
87
|
+
"$ref": "#/$defs/headers"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"required": ["type", "url"],
|
|
91
|
+
"additionalProperties": false
|
|
92
|
+
},
|
|
93
|
+
"sseServer": {
|
|
94
|
+
"title": "Legacy HTTP+SSE MCP server",
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"type": {
|
|
98
|
+
"const": "sse"
|
|
99
|
+
},
|
|
100
|
+
"url": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"minLength": 1,
|
|
103
|
+
"description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification."
|
|
104
|
+
},
|
|
105
|
+
"headers": {
|
|
106
|
+
"$ref": "#/$defs/headers"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"required": ["type", "url"],
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
},
|
|
112
|
+
"headers": {
|
|
113
|
+
"title": "HTTP headers",
|
|
114
|
+
"type": "object",
|
|
115
|
+
"additionalProperties": {
|
|
116
|
+
"type": "string"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
4
|
+
"title": "Agent Plugins Manifest",
|
|
5
|
+
"description": "Machine-readable schema for plugin.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"const": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
10
|
+
"description": "Canonical identifier of the plugin manifest schema for the Agent Plugins version targeted by this document."
|
|
11
|
+
},
|
|
12
|
+
"name": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"maxLength": 64,
|
|
16
|
+
"pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$",
|
|
17
|
+
"description": "Human-readable plugin name."
|
|
18
|
+
},
|
|
19
|
+
"version": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"description": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"author": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"name": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"email": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"url": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
},
|
|
40
|
+
"homepage": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"license": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"keywords": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "string"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"extensions": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Client-specific manifest data keyed by reverse-domain extension namespace. Agent Plugins assigns no semantics to namespace object contents.",
|
|
58
|
+
"additionalProperties": {
|
|
59
|
+
"type": "object"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"required": ["$schema", "name"],
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
}
|