dsh-agent-plugins-market 0.7.1 → 0.7.2
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 +29 -24
- package/README.zh.md +29 -24
- package/client/client.js +1172 -159
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +14 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +185 -30
- package/lib/catalog/archive.js +15 -0
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +2 -0
- package/lib/index.js +6 -2
- package/lib/model/layouts.js +34 -0
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +31 -2
- package/lib/runtime/agent-role-router.js +176 -51
- package/lib/runtime/host-locale.js +6 -12
- package/lib/runtime/mcp-client/connection.js +3 -2
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +178 -34
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/tool-registry-observer.js +8 -4
- package/lib/types/application/catalog.d.ts +10 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +32 -3
- package/lib/types/application/queries.d.ts +6 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +67 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/model/layouts.d.ts +20 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +90 -16
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/tool-registry-observer.d.ts +5 -3
- package/package.json +8 -8
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
package/lib/catalog/validate.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-plugins.org v1
|
|
3
|
-
* specification's semantic
|
|
2
|
+
* agent-plugins.org v1 validation: vendored JSON Schemas per supported
|
|
3
|
+
* specification version plus the specification's semantic rules.
|
|
4
4
|
*
|
|
5
|
-
* The schemas live under `schemas
|
|
6
|
-
* the spec forbids retrieving a schema while loading a plugin
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The schemas live under `schemas/<version>/` and are bundled into the
|
|
6
|
+
* package — the spec forbids retrieving a schema while loading a plugin
|
|
7
|
+
* (§5.2, §7.2.1). Validation follows the specification's failure boundaries:
|
|
8
|
+
* manifest unknown top-level fields and the whole `extensions` subtree are
|
|
9
|
+
* reported and ignored (§5.2, §8.1) while any other manifest violation is
|
|
10
|
+
* fatal; `mcp.json` file-level violations disable MCP for the plugin (§7.2.2
|
|
11
|
+
* rule 2) and per-server violations skip only their server (§7.2.2 rule 3).
|
|
12
|
+
* The semantic pass then enforces §4 path containment, §7.2.1 remote URL and
|
|
13
|
+
* header rules, §9.2 placeholder discipline, and the `${PLUGIN_ROOT}` /
|
|
14
|
+
* `${PLUGIN_DATA}` expansion the spec makes mandatory.
|
|
10
15
|
*/
|
|
11
16
|
import { readFile, realpath } from 'node:fs/promises';
|
|
12
17
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -16,35 +21,62 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
21
|
import Ajv2020Default from 'ajv/dist/2020.js';
|
|
17
22
|
import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
|
|
18
23
|
import { isWithin } from './paths.js';
|
|
19
|
-
const
|
|
24
|
+
const SUPPORTED_VERSIONS = [
|
|
25
|
+
{ version: '1.0.0', pluginSchemaId: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', mcpSchemaId: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json' },
|
|
26
|
+
// 1.1.0 is explicitly recognized as compatible: its two published schemas
|
|
27
|
+
// differ from 1.0.0 only in the version string (§5.2 allows mapping
|
|
28
|
+
// explicitly recognized compatible versions onto one implementation).
|
|
29
|
+
{ version: '1.1.0', pluginSchemaId: 'https://agent-plugins.org/schemas/1.1.0/plugin.schema.json', mcpSchemaId: 'https://agent-plugins.org/schemas/1.1.0/mcp.schema.json' }
|
|
30
|
+
];
|
|
31
|
+
const SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas');
|
|
20
32
|
const Ajv2020Class = Ajv2020Default;
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
const BASELINE = SUPPORTED_VERSIONS[0];
|
|
34
|
+
/** The canonical plugin manifest schema identifier of the baseline release. */
|
|
35
|
+
export const PLUGIN_SCHEMA_ID = BASELINE.pluginSchemaId;
|
|
36
|
+
/** The canonical MCP configuration schema identifier of the baseline release. */
|
|
37
|
+
export const MCP_SCHEMA_ID = BASELINE.mcpSchemaId;
|
|
38
|
+
/** The spec version a recognized `$schema` id selects, or undefined when unrecognized. */
|
|
39
|
+
export function recognizedSpecVersion(schemaId) {
|
|
40
|
+
if (typeof schemaId !== 'string')
|
|
41
|
+
return undefined;
|
|
42
|
+
return SUPPORTED_VERSIONS.find(version => version.pluginSchemaId === schemaId || version.mcpSchemaId === schemaId);
|
|
43
|
+
}
|
|
44
|
+
/** Whether a `$schema` value selects a locally supported ruleset. */
|
|
45
|
+
export function isRecognizedSchema(value) {
|
|
46
|
+
return recognizedSpecVersion(value) !== undefined;
|
|
47
|
+
}
|
|
23
48
|
let ajvPromise;
|
|
24
|
-
/** Lazily build one Ajv instance holding
|
|
49
|
+
/** Lazily build one Ajv instance holding every vendored schema. */
|
|
25
50
|
async function ajv() {
|
|
26
51
|
ajvPromise ??= (async () => {
|
|
27
|
-
const instance = new Ajv2020Class({ strict: false });
|
|
28
|
-
for (const
|
|
29
|
-
const
|
|
30
|
-
|
|
52
|
+
const instance = new Ajv2020Class({ strict: false, allErrors: true });
|
|
53
|
+
for (const version of SUPPORTED_VERSIONS) {
|
|
54
|
+
for (const name of ['plugin.schema.json', 'mcp.schema.json']) {
|
|
55
|
+
const schemaText = await readFile(join(SCHEMAS_DIR, version.version, name), 'utf8');
|
|
56
|
+
instance.addSchema(JSON.parse(schemaText));
|
|
57
|
+
}
|
|
31
58
|
}
|
|
32
59
|
return instance;
|
|
33
60
|
})();
|
|
34
61
|
return ajvPromise;
|
|
35
62
|
}
|
|
36
|
-
/** Validate one JSON document against a vendored schema; errors
|
|
63
|
+
/** Validate one JSON document against a vendored schema; structured errors. */
|
|
37
64
|
export async function validateAgainstSchema(schemaId, document) {
|
|
38
65
|
const instance = await ajv();
|
|
39
66
|
const validate = instance.getSchema(schemaId);
|
|
40
67
|
if (validate === undefined)
|
|
41
|
-
return [`no bundled validator for ${schemaId}`];
|
|
68
|
+
return [{ keyword: 'schema', instancePath: '', message: `no bundled validator for ${schemaId}` }];
|
|
42
69
|
validate(document);
|
|
43
|
-
return (validate.errors ?? []).map(error =>
|
|
70
|
+
return (validate.errors ?? []).map(error => ({
|
|
71
|
+
keyword: error.keyword ?? 'schema',
|
|
72
|
+
instancePath: error.instancePath ?? '',
|
|
73
|
+
message: error.message ?? 'invalid',
|
|
74
|
+
...(error.params === undefined ? {} : { params: error.params })
|
|
75
|
+
}));
|
|
44
76
|
}
|
|
45
|
-
/**
|
|
46
|
-
export function
|
|
47
|
-
return
|
|
77
|
+
/** Render structured errors as one diagnostic string per error for surfacing. */
|
|
78
|
+
export function formatSchemaErrors(errors) {
|
|
79
|
+
return errors.map(error => `${error.instancePath === '' ? 'root' : error.instancePath} ${error.message}`);
|
|
48
80
|
}
|
|
49
81
|
/**
|
|
50
82
|
* Spec §4 containment: a plugin-relative path must begin with `./` and, after
|
|
@@ -76,39 +108,124 @@ export async function pathContainmentError(pluginRoot, value) {
|
|
|
76
108
|
}
|
|
77
109
|
return undefined;
|
|
78
110
|
}
|
|
79
|
-
|
|
111
|
+
const EXTENSIONS_PATH = '/extensions';
|
|
112
|
+
function isExtensionPath(instancePath) {
|
|
113
|
+
return instancePath === EXTENSIONS_PATH || instancePath.startsWith(`${EXTENSIONS_PATH}/`);
|
|
114
|
+
}
|
|
115
|
+
/** Validate a plugin manifest against its declared spec version's schema and split the verdict. */
|
|
80
116
|
export async function validatePluginManifest(raw) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
117
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
118
|
+
return { fatal: [{ keyword: 'type', instancePath: '', message: 'manifest is not a JSON object' }], ignored: [] };
|
|
119
|
+
}
|
|
84
120
|
const record = raw;
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
const version = recognizedSpecVersion(record['$schema']);
|
|
122
|
+
if (version === undefined) {
|
|
123
|
+
return {
|
|
124
|
+
fatal: [{ keyword: 'const', instancePath: '', message: `unrecognized $schema ${JSON.stringify(record['$schema'])}; supported: agent-plugins 1.0.0, 1.1.0` }],
|
|
125
|
+
ignored: []
|
|
126
|
+
};
|
|
87
127
|
}
|
|
88
|
-
errors
|
|
89
|
-
|
|
128
|
+
const errors = await validateAgainstSchema(version.pluginSchemaId, raw);
|
|
129
|
+
const fatal = [];
|
|
130
|
+
const ignored = [];
|
|
131
|
+
for (const error of errors) {
|
|
132
|
+
if (isExtensionPath(error.instancePath))
|
|
133
|
+
ignored.push(error);
|
|
134
|
+
else if (error.keyword === 'additionalProperties' && error.instancePath === '') {
|
|
135
|
+
const name = typeof error.params?.additionalProperty === 'string' ? error.params.additionalProperty : 'unknown field';
|
|
136
|
+
ignored.push({ ...error, message: `unknown top-level field "${name}" is not part of the portable manifest; ignored` });
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
fatal.push(error);
|
|
140
|
+
}
|
|
141
|
+
return { fatal, ignored };
|
|
90
142
|
}
|
|
143
|
+
const KNOWN_MCP_TRANSPORTS = new Set(['stdio', 'streamable-http', 'sse']);
|
|
91
144
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
* §7.2.1 remote URL rules: absolute http/https, no user information, no
|
|
146
|
+
* fragment, HTTPS everywhere except loopback hosts.
|
|
147
|
+
*/
|
|
148
|
+
export function remoteUrlError(value) {
|
|
149
|
+
let url;
|
|
150
|
+
try {
|
|
151
|
+
url = new URL(value);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return `url "${value}" is not an absolute URL`;
|
|
155
|
+
}
|
|
156
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
157
|
+
return `url "${value}" must use http or https`;
|
|
158
|
+
if (url.username !== '' || url.password !== '')
|
|
159
|
+
return `url "${value}" must not contain user information`;
|
|
160
|
+
if (url.hash !== '')
|
|
161
|
+
return `url "${value}" must not contain a fragment`;
|
|
162
|
+
if (url.protocol === 'http:') {
|
|
163
|
+
const host = url.hostname;
|
|
164
|
+
const loopback = host === 'localhost' || host === '::1' || host === '[::1]' || host.endsWith('.localhost') || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
165
|
+
if (!loopback)
|
|
166
|
+
return `url "${value}" must use https (http is allowed only for loopback)`;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* §7.2.1 header rules: valid field names, and a duplicate name under
|
|
172
|
+
* different casing makes the entry invalid.
|
|
173
|
+
*/
|
|
174
|
+
export function headersError(headers) {
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
177
|
+
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name))
|
|
178
|
+
return `header name ${JSON.stringify(name)} is not a valid HTTP header field name`;
|
|
179
|
+
if (typeof value !== 'string')
|
|
180
|
+
return `header ${JSON.stringify(name)} must have a string value`;
|
|
181
|
+
const lower = name.toLowerCase();
|
|
182
|
+
if (seen.has(lower))
|
|
183
|
+
return `header "${name}" is declared more than once under different casing`;
|
|
184
|
+
seen.add(lower);
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Parse and validate an `mcp.json` document.
|
|
190
|
+
*
|
|
191
|
+
* Strict mode applies the two-level failure boundary of §7.2.2: a file-level
|
|
192
|
+
* violation (bad `$schema`, version mismatch with the manifest, non-object
|
|
193
|
+
* `mcpServers`) disables MCP for the whole plugin, while a per-server
|
|
194
|
+
* violation skips only that server. Lenient mode keeps the native-client
|
|
195
|
+
* behavior: no `$schema` requirement, unknown transports skipped per server.
|
|
196
|
+
*
|
|
197
|
+
* @returns the validated config plus per-problem strings; `config` stays
|
|
198
|
+
* defined whenever at least the file shell was valid, even when some
|
|
199
|
+
* servers were skipped.
|
|
97
200
|
*/
|
|
98
|
-
const KNOWN_MCP_TRANSPORTS = new Set(['stdio', 'streamable-http', 'sse']);
|
|
99
201
|
export async function validateMcpJson(pluginRoot, raw, options) {
|
|
100
202
|
if (typeof raw !== 'object' || raw === null)
|
|
101
203
|
return { errors: ['mcp.json is not a JSON object'] };
|
|
102
204
|
const record = raw;
|
|
103
205
|
const errors = [];
|
|
104
206
|
const strict = options?.strict !== false;
|
|
207
|
+
const packageRules = strict && options?.packageRules !== false;
|
|
208
|
+
let version;
|
|
105
209
|
if (strict) {
|
|
106
|
-
|
|
107
|
-
|
|
210
|
+
version = recognizedSpecVersion(record['$schema']);
|
|
211
|
+
if (version === undefined) {
|
|
212
|
+
return { errors: [`unrecognized mcp.json $schema ${JSON.stringify(record['$schema'])}; supported: agent-plugins 1.0.0, 1.1.0`] };
|
|
108
213
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
if (options?.manifestVersion !== undefined && options.manifestVersion !== version.version) {
|
|
215
|
+
return {
|
|
216
|
+
errors: [
|
|
217
|
+
`mcp.json declares agent-plugins ${version.version} while plugin.json declares ${options.manifestVersion}; the versions must match (§10.1) and MCP is disabled for this plugin`
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// File-level shape only: `$schema`, `mcpServers`, unknown top-level
|
|
222
|
+
// fields. Server entries are validated per server below (§7.2.2 rule 3).
|
|
223
|
+
const shellErrors = (await validateAgainstSchema(version.mcpSchemaId, raw)).filter(error => !error.instancePath.startsWith('/mcpServers/'));
|
|
224
|
+
// User-owned data may carry keys this client does not know; they ride along
|
|
225
|
+
// rather than failing the file.
|
|
226
|
+
const fatal = packageRules ? shellErrors : shellErrors.filter(error => error.keyword !== 'additionalProperties');
|
|
227
|
+
if (fatal.length > 0)
|
|
228
|
+
return { errors: formatSchemaErrors(fatal) };
|
|
112
229
|
}
|
|
113
230
|
// Lenient native-client files may omit the `mcpServers` wrapper and put the
|
|
114
231
|
// server map at the top level (Claude Code `.mcp.json` shorthand).
|
|
@@ -125,6 +242,27 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
125
242
|
errors.push(`server "${name}": not an object`);
|
|
126
243
|
continue;
|
|
127
244
|
}
|
|
245
|
+
if (strict && packageRules) {
|
|
246
|
+
// §7.2.1 exposes `#/$defs/server` so each entry can be validated
|
|
247
|
+
// independently, preserving §7.2.2 rule 3: a violation skips only its
|
|
248
|
+
// own server. Validate the bare entry against that ref directly —
|
|
249
|
+
// errors are then rooted at the entry itself (`/env`, `` for required).
|
|
250
|
+
// User-owned data goes straight to the per-transport checks below: its
|
|
251
|
+
// closed-set shape is deliberately not the package one.
|
|
252
|
+
const serverErrors = await validateAgainstSchema(`${version.mcpSchemaId}#/$defs/server`, value);
|
|
253
|
+
if (serverErrors.length > 0) {
|
|
254
|
+
const rendered = new Set();
|
|
255
|
+
for (const error of serverErrors) {
|
|
256
|
+
if (error.keyword === 'oneOf' || error.keyword === 'required')
|
|
257
|
+
continue;
|
|
258
|
+
rendered.add(`${error.instancePath === '' ? '' : `${error.instancePath} `}${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
if (rendered.size === 0)
|
|
261
|
+
rendered.add('does not match any supported server variant');
|
|
262
|
+
errors.push(...[...rendered].map(problem => `server "${name}": ${problem || 'invalid server entry'}`));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
128
266
|
const server = value;
|
|
129
267
|
// Claude Code dialects: `http`/`streamable-http` are the same remote
|
|
130
268
|
// transport, and `local` or a missing `type` (with `command`) mean stdio.
|
|
@@ -138,6 +276,28 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
138
276
|
continue;
|
|
139
277
|
}
|
|
140
278
|
if (type !== 'stdio') {
|
|
279
|
+
if (strict) {
|
|
280
|
+
if (typeof server.url !== 'string') {
|
|
281
|
+
errors.push(`server "${name}": remote servers require a url`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const urlProblem = remoteUrlError(server.url);
|
|
285
|
+
if (urlProblem !== undefined) {
|
|
286
|
+
errors.push(`server "${name}": ${urlProblem}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (server.headers !== undefined) {
|
|
290
|
+
if (typeof server.headers !== 'object' || server.headers === null) {
|
|
291
|
+
errors.push(`server "${name}": headers must be an object`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const headerProblem = headersError(server.headers);
|
|
295
|
+
if (headerProblem !== undefined) {
|
|
296
|
+
errors.push(`server "${name}": ${headerProblem}`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
141
301
|
valid[name] = { ...server, type };
|
|
142
302
|
continue;
|
|
143
303
|
}
|
|
@@ -147,8 +307,11 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
147
307
|
errors.push(`server "${name}": stdio servers require a command`);
|
|
148
308
|
continue;
|
|
149
309
|
}
|
|
310
|
+
if (packageRules && /\$\{/.test(stdioServer.command)) {
|
|
311
|
+
problems.push(`command ${JSON.stringify(stdioServer.command)} must not contain placeholders (§9.2: command is resolved as one token, not expanded)`);
|
|
312
|
+
}
|
|
150
313
|
const command = stdioServer.command.replace(/^\$\{([A-Z_]+)\}\//, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? './' : match));
|
|
151
|
-
if (options?.pathMode !== 'project' && command.includes('/')) {
|
|
314
|
+
if (packageRules && options?.pathMode !== 'project' && command.includes('/')) {
|
|
152
315
|
if (!command.startsWith('./')) {
|
|
153
316
|
problems.push(`command "${command}" must be a bare executable name or a plugin-relative path beginning with "./"`);
|
|
154
317
|
}
|
|
@@ -162,7 +325,7 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
162
325
|
if (declaredCwd !== undefined && typeof declaredCwd !== 'string') {
|
|
163
326
|
problems.push('cwd must be a string');
|
|
164
327
|
}
|
|
165
|
-
else if (options?.pathMode !== 'project' && declaredCwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => declaredCwd.startsWith(`\${${name}}`))) {
|
|
328
|
+
else if (packageRules && options?.pathMode !== 'project' && declaredCwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => declaredCwd.startsWith(`\${${name}}`))) {
|
|
166
329
|
// `.` is the Codex dialect spelling for the plugin root.
|
|
167
330
|
const cwd = declaredCwd === '.' ? './' : declaredCwd;
|
|
168
331
|
const reason = await pathContainmentError(pluginRoot, cwd);
|
|
@@ -178,18 +341,6 @@ export async function validateMcpJson(pluginRoot, raw, options) {
|
|
|
178
341
|
}
|
|
179
342
|
return { config: { schema: typeof record['$schema'] === 'string' ? record['$schema'] : 'native-client', servers: valid }, errors };
|
|
180
343
|
}
|
|
181
|
-
/** Expand `${PLUGIN_ROOT}`, `${PLUGIN_DATA}`, and `${NAME}` (process env) in one string.
|
|
182
|
-
* Claude Code aliases `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` and
|
|
183
|
-
* `${NAME:-default}` fallbacks are honored for unset/empty env vars. */
|
|
184
|
-
export function expandPlaceholders(value, pluginRoot, pluginData, env = process.env) {
|
|
185
|
-
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => {
|
|
186
|
-
if (PLUGIN_ROOT_VARIABLES.has(name))
|
|
187
|
-
return pluginRoot;
|
|
188
|
-
if (PLUGIN_DATA_VARIABLES.has(name))
|
|
189
|
-
return pluginData;
|
|
190
|
-
return env[name] || fallback || '';
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
344
|
/** Resolve an expanded plugin-relative cwd to an absolute path. */
|
|
194
345
|
export function resolveCwd(value, pluginRoot, pluginData) {
|
|
195
346
|
if (value.startsWith('${PLUGIN_DATA}'))
|
package/lib/contracts/market.js
CHANGED
|
@@ -6,6 +6,7 @@ export const MARKET_ROUTES = {
|
|
|
6
6
|
overview: `${MARKET_API_PREFIX}overview`,
|
|
7
7
|
mcpStatus: `${MARKET_API_PREFIX}mcp-status`,
|
|
8
8
|
addMcpServer: `${MARKET_API_PREFIX}mcp-servers/add`,
|
|
9
|
+
importMcpServers: `${MARKET_API_PREFIX}mcp-servers/import`,
|
|
9
10
|
lspStatus: `${MARKET_API_PREFIX}lsp-status`,
|
|
10
11
|
lspServers: `${MARKET_API_PREFIX}lsp-servers`,
|
|
11
12
|
addLspServer: `${MARKET_API_PREFIX}lsp-servers/add`,
|
|
@@ -29,6 +30,7 @@ export const MARKET_ROUTES = {
|
|
|
29
30
|
mcpOverrides: `${MARKET_API_PREFIX}mcp-overrides`,
|
|
30
31
|
setMcpOverride: `${MARKET_API_PREFIX}set-mcp-override`,
|
|
31
32
|
setMcpServerEnabled: `${MARKET_API_PREFIX}set-mcp-server-enabled`,
|
|
33
|
+
setMcpServerTool: `${MARKET_API_PREFIX}set-mcp-server-tool`,
|
|
32
34
|
mcpRetry: `${MARKET_API_PREFIX}mcp-retry`,
|
|
33
35
|
mcpReauthorize: `${MARKET_API_PREFIX}mcp-reauthorize`,
|
|
34
36
|
mcpBackend: `${MARKET_API_PREFIX}mcp-backend`,
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createUserPanelStores } from './runtime/user-panels.js';
|
|
|
21
21
|
import { SourceAutoUpdater } from './runtime/source-auto-update.js';
|
|
22
22
|
import { UserPanelSkillProvider } from './runtime/user-panels.js';
|
|
23
23
|
import { UserCommandMountRegistry } from './runtime/user-commands.js';
|
|
24
|
+
import { presetSourceRef } from './model/preset-source.js';
|
|
24
25
|
export const name = 'dsh-agent-plugins-market';
|
|
25
26
|
export const inject = ['skills', 'commands'];
|
|
26
27
|
export async function apply(ctx, config = {}) {
|
|
@@ -144,7 +145,10 @@ export async function apply(ctx, config = {}) {
|
|
|
144
145
|
};
|
|
145
146
|
const catalog = new Catalog({ userRoot, dataRoot, agentsRoot, onChanged, ports, ...(config.git === undefined ? {} : { git: config.git }) });
|
|
146
147
|
await catalog.load();
|
|
147
|
-
|
|
148
|
+
// Configured seeds first, then the record this plugin presets: the market
|
|
149
|
+
// lists the first-party collection on the first open, and the ordinary
|
|
150
|
+
// refresh path clones it. Registration performs no network access.
|
|
151
|
+
await catalog.mergeSources([...(config.sources ?? []), presetSourceRef()]);
|
|
148
152
|
const resources = createPanelResources(catalog, panels);
|
|
149
153
|
runtime.setMcpOverridesProvider(async () => catalog.allMcpOverrides(await catalog.enabledUserSuites()));
|
|
150
154
|
runtime.lsp.setDirectProvider(async () => (await loadLspServers(agentsRoot)).servers);
|
|
@@ -184,7 +188,7 @@ export async function apply(ctx, config = {}) {
|
|
|
184
188
|
hostCtx.effect(() => mountAgentRoleTool(hostCtx, async (parent) => [
|
|
185
189
|
...(await resources.agents.list(true)).map(entry => ({ ...entry, title: entry.name, name: entry.id ?? entry.name })),
|
|
186
190
|
...(await projectAgentRoles(catalog, parent))
|
|
187
|
-
]
|
|
191
|
+
]), 'dsh-agent-plugins-market: agent role routing');
|
|
188
192
|
});
|
|
189
193
|
ctx.inject(['agents'], hostCtx => {
|
|
190
194
|
hostCtx.effect(() => {
|
package/lib/model/layouts.js
CHANGED
|
@@ -12,6 +12,22 @@ export const PLUGIN_LAYOUTS = [
|
|
|
12
12
|
];
|
|
13
13
|
/** Kimi Code's primary spelling precedes its compatibility spelling within that dialect. */
|
|
14
14
|
export const MANIFEST_ALIASES = { kimi: ['kimi.plugin.json'] };
|
|
15
|
+
/**
|
|
16
|
+
* This client's Agent Plugins extension namespace (§8): manifest data rides
|
|
17
|
+
* `plugin.json` `extensions` under this key, and file-based extensions live
|
|
18
|
+
* in the top-level directory named exactly this. The namespace contract
|
|
19
|
+
* version is declared inside the manifest data.
|
|
20
|
+
*/
|
|
21
|
+
export const EXTENSION_NAMESPACE = 'com.deepseek.harness';
|
|
22
|
+
/** Namespace contract versions this manager implements. */
|
|
23
|
+
export const NAMESPACE_SCHEMA_VERSIONS = new Set(['1.0.0']);
|
|
24
|
+
/** Namespace directory locations, relative to the plugin root. */
|
|
25
|
+
export const NAMESPACE_DIR = EXTENSION_NAMESPACE;
|
|
26
|
+
export const NAMESPACE_HOOKS_FILE = joinNamespace(NAMESPACE_DIR, 'hooks', 'hooks.json');
|
|
27
|
+
export const NAMESPACE_LSP_FILE = joinNamespace(NAMESPACE_DIR, 'lsp.json');
|
|
28
|
+
function joinNamespace(...parts) {
|
|
29
|
+
return parts.join('/');
|
|
30
|
+
}
|
|
15
31
|
/** Equivalent plugin-owned path variables; these never resolve as credentials. */
|
|
16
32
|
export const PLUGIN_ROOT_VARIABLES = new Set(['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'ZCODE_PLUGIN_ROOT', 'QODER_PLUGIN_ROOT']);
|
|
17
33
|
export const PLUGIN_DATA_VARIABLES = new Set(['PLUGIN_DATA', 'CLAUDE_PLUGIN_DATA', 'ZCODE_PLUGIN_DATA', 'QODER_PLUGIN_DATA']);
|
|
@@ -56,3 +72,21 @@ export const PROJECT_LAYOUTS = [
|
|
|
56
72
|
];
|
|
57
73
|
/** Marketplace catalogs follow manifest layout order; the shared root catalog is the fallback. */
|
|
58
74
|
export const MARKETPLACE_PATHS = [...PLUGIN_LAYOUTS.flatMap(layout => layout.marketplaces), 'marketplace.json'];
|
|
75
|
+
/**
|
|
76
|
+
* Inline top-level manifest keys this manager reads for the portable v1
|
|
77
|
+
* dialect. The specification's closed manifest schema carries none of them
|
|
78
|
+
* (§5.2, §6.1), so under `agent-plugin-v1` they are reported as unknown
|
|
79
|
+
* top-level fields and ignored; every other dialect keeps reading them.
|
|
80
|
+
*/
|
|
81
|
+
export const V1_IGNORED_MANIFEST_KEYS = [
|
|
82
|
+
'skills',
|
|
83
|
+
'commands',
|
|
84
|
+
'agents',
|
|
85
|
+
'hooks',
|
|
86
|
+
'mcpServers',
|
|
87
|
+
'lspServers',
|
|
88
|
+
'skillInstructions',
|
|
89
|
+
'systemPrompt',
|
|
90
|
+
'systemPromptPath',
|
|
91
|
+
'sessionStart'
|
|
92
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Source id the preset record registers under. */
|
|
2
|
+
export const PRESET_SOURCE_ID = 'dsh-agent-plugins';
|
|
3
|
+
/** Repository the preset record points at. */
|
|
4
|
+
export const PRESET_SOURCE_URL = 'https://github.com/Sivan757/dsh-agent-plugins.git';
|
|
5
|
+
/** The preset registration, as the market stores it. */
|
|
6
|
+
export function presetSourceRef() {
|
|
7
|
+
return { id: PRESET_SOURCE_ID, url: PRESET_SOURCE_URL, kind: 'git' };
|
|
8
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -39,7 +39,14 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
39
39
|
sendJson(response, 200, { ok: true, ...value });
|
|
40
40
|
}
|
|
41
41
|
catch (error) {
|
|
42
|
-
|
|
42
|
+
// A rejection that names its fields travels with them, so the editor
|
|
43
|
+
// can place each reason beside the input it belongs to.
|
|
44
|
+
const fields = error.fields;
|
|
45
|
+
sendJson(response, 400, {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: error instanceof Error ? error.message : String(error),
|
|
48
|
+
...(Array.isArray(fields) ? { fields } : {})
|
|
49
|
+
});
|
|
43
50
|
}
|
|
44
51
|
})();
|
|
45
52
|
}
|
|
@@ -77,7 +84,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
77
84
|
throw new Error('invalid service kind');
|
|
78
85
|
if (typeof body.id !== 'string' || body.id === '')
|
|
79
86
|
throw new Error('missing service id');
|
|
80
|
-
await manager.saveServerConfig(body.kind, body.id, body.config);
|
|
87
|
+
await manager.saveServerConfig(body.kind, body.id, body.config, body.policy);
|
|
81
88
|
return {};
|
|
82
89
|
});
|
|
83
90
|
post(MARKET_ROUTES.addMcpServer, async (body) => {
|
|
@@ -86,6 +93,13 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
86
93
|
await manager.addMcpServer(body.name, body.config);
|
|
87
94
|
return {};
|
|
88
95
|
});
|
|
96
|
+
// Paste import: many services in one write. Each entry reports its own
|
|
97
|
+
// outcome, so one rejected definition never fails the whole paste.
|
|
98
|
+
post(MARKET_ROUTES.importMcpServers, async (body) => {
|
|
99
|
+
const overwrite = body['overwrite'] === true;
|
|
100
|
+
const result = await manager.importMcpServers(body['servers'], overwrite);
|
|
101
|
+
return { imported: result.imported, skipped: result.skipped };
|
|
102
|
+
});
|
|
89
103
|
get(MARKET_ROUTES.lspStatus, async (_request, response) => {
|
|
90
104
|
sendJson(response, 200, await manager.lspStatus());
|
|
91
105
|
});
|
|
@@ -269,6 +283,21 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
269
283
|
await manager.setMcpServerEnabled(suiteKey, serverKey, enabled);
|
|
270
284
|
return {};
|
|
271
285
|
});
|
|
286
|
+
// Allow or deny one tool of a declared MCP server. Addressed by the same
|
|
287
|
+
// source-qualified suite id the status rows carry; the denial lands in the
|
|
288
|
+
// override record, so the suite's own mcp.json stays source-owned.
|
|
289
|
+
post(MARKET_ROUTES.setMcpServerTool, async (body) => {
|
|
290
|
+
const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
|
|
291
|
+
const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
|
|
292
|
+
const tool = textField(body['tool'] ?? '', 'MCP tool name');
|
|
293
|
+
const enabled = body['enabled'];
|
|
294
|
+
if (tool === '')
|
|
295
|
+
throw new Error('missing MCP tool name');
|
|
296
|
+
if (typeof enabled !== 'boolean')
|
|
297
|
+
throw new Error('missing boolean enabled');
|
|
298
|
+
await manager.setMcpServerToolEnabled(suiteKey, serverKey, tool, enabled);
|
|
299
|
+
return {};
|
|
300
|
+
});
|
|
272
301
|
// Manual MCP reconcile: retries failed mounts and clears residual tools
|
|
273
302
|
// without touching any catalog state.
|
|
274
303
|
post(MARKET_ROUTES.mcpRetry, async () => {
|