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.
Files changed (61) hide show
  1. package/README.md +29 -24
  2. package/README.zh.md +29 -24
  3. package/client/client.js +1172 -159
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +14 -2
  6. package/lib/application/install-store.js +1 -1
  7. package/lib/application/mcp-service.js +185 -30
  8. package/lib/catalog/archive.js +15 -0
  9. package/lib/catalog/manifests.js +124 -13
  10. package/lib/catalog/scan-resolvers.js +117 -12
  11. package/lib/catalog/surfaces.js +73 -27
  12. package/lib/catalog/validate.js +205 -54
  13. package/lib/contracts/market.js +2 -0
  14. package/lib/index.js +6 -2
  15. package/lib/model/layouts.js +34 -0
  16. package/lib/model/preset-source.js +8 -0
  17. package/lib/routes.js +31 -2
  18. package/lib/runtime/agent-role-router.js +176 -51
  19. package/lib/runtime/host-locale.js +6 -12
  20. package/lib/runtime/mcp-client/connection.js +3 -2
  21. package/lib/runtime/mcp-client/oauth.js +82 -14
  22. package/lib/runtime/mcp-client/tools.js +2 -1
  23. package/lib/runtime/mcp-config.js +178 -34
  24. package/lib/runtime/mcp-direct-config.js +49 -6
  25. package/lib/runtime/mcp-mounts.js +15 -3
  26. package/lib/runtime/mcp-overrides.js +131 -1
  27. package/lib/runtime/mcp-redaction.js +8 -0
  28. package/lib/runtime/mcp-status.js +21 -5
  29. package/lib/runtime/server-config.js +49 -8
  30. package/lib/runtime/subagent-catalog.js +62 -22
  31. package/lib/runtime/tool-registry-observer.js +8 -4
  32. package/lib/types/application/catalog.d.ts +10 -1
  33. package/lib/types/application/install-store.d.ts +1 -1
  34. package/lib/types/application/mcp-service.d.ts +32 -3
  35. package/lib/types/application/queries.d.ts +6 -1
  36. package/lib/types/catalog/manifests.d.ts +6 -2
  37. package/lib/types/catalog/surfaces.d.ts +14 -4
  38. package/lib/types/catalog/validate.d.ts +72 -11
  39. package/lib/types/contracts/market.d.ts +67 -0
  40. package/lib/types/contracts/mcp-status.d.ts +11 -0
  41. package/lib/types/model/layouts.d.ts +20 -0
  42. package/lib/types/model/preset-source.d.ts +18 -0
  43. package/lib/types/model/types.d.ts +37 -1
  44. package/lib/types/runtime/agent-role-router.d.ts +90 -16
  45. package/lib/types/runtime/host-locale.d.ts +1 -1
  46. package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
  47. package/lib/types/runtime/mcp-config.d.ts +75 -13
  48. package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
  49. package/lib/types/runtime/mcp-overrides.d.ts +44 -0
  50. package/lib/types/runtime/mcp-redaction.d.ts +6 -0
  51. package/lib/types/runtime/server-config.d.ts +26 -1
  52. package/lib/types/runtime/subagent-catalog.d.ts +14 -2
  53. package/lib/types/runtime/tool-registry-observer.d.ts +5 -3
  54. package/package.json +8 -8
  55. package/schemas/1.0.0/mcp.schema.json +0 -22
  56. package/schemas/1.1.0/mcp.schema.json +120 -0
  57. package/schemas/1.1.0/plugin.schema.json +65 -0
  58. package/schemas/README.md +25 -19
  59. package/schemas/agent-plugins/spec.md +23 -9
  60. package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
  61. package/schemas/com.deepseek.harness/spec.md +52 -0
@@ -1,12 +1,17 @@
1
1
  /**
2
- * agent-plugins.org v1.0.0 validation: vendored JSON Schemas plus the
3
- * specification's semantic filesystem rules.
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/1.0.0/` and are bundled into the package —
6
- * the spec forbids retrieving a schema while loading a plugin. Schema
7
- * validation is one failure boundary (per server / per manifest); the
8
- * semantic pass then enforces §4 path containment, `$schema` recognition, and
9
- * the `${PLUGIN_ROOT}` / `${PLUGIN_DATA}` expansion the spec makes mandatory.
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 SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas', '1.0.0');
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
- export const PLUGIN_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json';
22
- export const MCP_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json';
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 the two vendored schemas. */
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 name of ['plugin.schema.json', 'mcp.schema.json']) {
29
- const schemaText = await readFile(join(SCHEMAS_DIR, name), 'utf8');
30
- instance.addSchema(JSON.parse(schemaText));
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 joined for surfacing. */
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 => `${error.instancePath === '' ? 'root' : error.instancePath} ${error.message ?? 'invalid'}`);
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
- /** Whether a `$schema` value selects the local v1.0.0 ruleset. */
46
- export function isRecognizedSchema(value) {
47
- return value === PLUGIN_SCHEMA_ID || value === MCP_SCHEMA_ID;
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
- /** Validate a plugin manifest per the vendored schema plus `$schema` recognition. */
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
- const errors = [];
82
- if (typeof raw !== 'object' || raw === null)
83
- return ['manifest is not a JSON object'];
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
- if (!isRecognizedSchema(record['$schema'])) {
86
- errors.push(`unrecognized $schema ${JSON.stringify(record['$schema'])}; this manager supports agent-plugins 1.0.0 only`);
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.push(...(await validateAgainstSchema(PLUGIN_SCHEMA_ID, raw)));
89
- return errors;
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
- * Parse and validate an `mcp.json` document: schema first, then §4 path
93
- * containment for `command` and `cwd` of every stdio server.
94
- * @returns the validated config plus per-problem strings; a failed `$schema`
95
- * or schema body drops the whole file, while per-server path problems
96
- * invalidate only their server.
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
- if (!isRecognizedSchema(record['$schema'])) {
107
- return { errors: [`unrecognized mcp.json $schema ${JSON.stringify(record['$schema'])}; this manager supports agent-plugins 1.0.0 only`] };
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
- const schemaErrors = await validateAgainstSchema(MCP_SCHEMA_ID, raw);
110
- if (schemaErrors.length > 0)
111
- return { errors: schemaErrors };
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}'))
@@ -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
- await catalog.mergeSources(config.sources ?? []);
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
- ], (key, params) => hostLocale.t(key, params)), 'dsh-agent-plugins-market: agent role routing');
191
+ ]), 'dsh-agent-plugins-market: agent role routing');
188
192
  });
189
193
  ctx.inject(['agents'], hostCtx => {
190
194
  hostCtx.effect(() => {
@@ -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
- sendJson(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
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 () => {