dsh-agent-plugins-market 0.7.0 → 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 +31 -26
- package/README.zh.md +31 -26
- package/client/client.js +29164 -1136
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +27 -2
- package/lib/application/details.js +13 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +191 -19
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +16 -1
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +3 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +32 -13
- package/lib/model/layouts.js +44 -1
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +43 -2
- package/lib/runtime/agent-role-router.js +203 -56
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +45 -15
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +9 -8
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +182 -35
- 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/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +24 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +19 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +38 -3
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +8 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -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 +77 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +30 -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 +96 -18
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- 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/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
- package/lib/types/runtime/user-panels.d.ts +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +23 -6
- 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/surfaces.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* thrown discovery.
|
|
11
11
|
*/
|
|
12
12
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
13
|
-
import { dirname, join } from 'node:path';
|
|
13
|
+
import { dirname, join, relative } from 'node:path';
|
|
14
14
|
import { parseSkillFrontmatter } from './skills-parse.js';
|
|
15
15
|
import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
|
|
16
|
-
import { validateMcpJson } from './validate.js';
|
|
16
|
+
import { validateMcpJson, pathContainmentError, recognizedSpecVersion } from './validate.js';
|
|
17
17
|
import { readManifest } from './manifests.js';
|
|
18
18
|
import { componentDocuments, componentPath, firstComponentFile, isUnknownArray } from './component-files.js';
|
|
19
19
|
/**
|
|
@@ -38,16 +38,29 @@ async function declaredSkillDirs(root, declared, errors) {
|
|
|
38
38
|
}
|
|
39
39
|
return dirs;
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Discover SKILL.md files under the suite's skills directory, up to 3 levels
|
|
43
|
+
* deep. Portable v1 suites (§7.1) instead discover exactly one level of
|
|
44
|
+
* `skills/` subdirectories each carrying a `SKILL.md` — the spec forbids
|
|
45
|
+
* recursive deeper search and root-level or flat skill files are outside the
|
|
46
|
+
* portable discovery shape.
|
|
47
|
+
*/
|
|
48
|
+
export async function discoverSkills(root, errors, declared, portable = false) {
|
|
43
49
|
const skills = [];
|
|
44
|
-
const rootSkill = join(root, 'SKILL.md');
|
|
45
|
-
const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
|
|
46
|
-
const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
|
|
47
|
-
if (rootParsed !== undefined)
|
|
48
|
-
skills.push(rootParsed);
|
|
49
50
|
const skillsDirs = await declaredSkillDirs(root, declared, errors);
|
|
50
|
-
if (
|
|
51
|
+
if (!portable) {
|
|
52
|
+
const rootSkill = join(root, 'SKILL.md');
|
|
53
|
+
const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
|
|
54
|
+
const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
|
|
55
|
+
if (rootParsed !== undefined)
|
|
56
|
+
skills.push(rootParsed);
|
|
57
|
+
if (declared === undefined) {
|
|
58
|
+
const fallback = join(root, 'skills');
|
|
59
|
+
if (await isDirectory(fallback))
|
|
60
|
+
skillsDirs.push(fallback);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (declared === undefined) {
|
|
51
64
|
const fallback = join(root, 'skills');
|
|
52
65
|
if (await isDirectory(fallback))
|
|
53
66
|
skillsDirs.push(fallback);
|
|
@@ -60,6 +73,25 @@ export async function discoverSkills(root, errors, declared) {
|
|
|
60
73
|
skills.push(skill);
|
|
61
74
|
};
|
|
62
75
|
for (const skillsDir of skillsDirs) {
|
|
76
|
+
if (portable) {
|
|
77
|
+
// §7.1: each immediate child directory containing `SKILL.md` is one
|
|
78
|
+
// skill; no deeper descendants are searched and flat files are not
|
|
79
|
+
// skills. Each candidate also passes a realpath containment check so a
|
|
80
|
+
// symlink pointing outside the plugin root is skipped (§4.1 boundary 3).
|
|
81
|
+
for (const child of await listChildDirs(skillsDir)) {
|
|
82
|
+
const skillFile = join(child, 'SKILL.md');
|
|
83
|
+
if (!(await isFile(skillFile)))
|
|
84
|
+
continue;
|
|
85
|
+
const reason = await pathContainmentError(root, `./${relative(root, skillFile).replace(/\\/g, '/')}`);
|
|
86
|
+
if (reason !== undefined) {
|
|
87
|
+
errors.push(`skill "${child.split(/[\\/]/).at(-1) ?? ''}": ${reason}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const name = child.split(/[\\/]/).at(-1) ?? '';
|
|
91
|
+
pushUnique(await parseOneSkill(skillFile, child, name, errors));
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
63
95
|
if (await isFile(skillsDir)) {
|
|
64
96
|
pushUnique(await parseOneSkill(skillsDir, dirname(skillsDir), '', errors));
|
|
65
97
|
continue;
|
|
@@ -145,23 +177,34 @@ async function parseOneSkill(file, directory, fallbackName, errors) {
|
|
|
145
177
|
}
|
|
146
178
|
return { ...verdict, directory };
|
|
147
179
|
}
|
|
148
|
-
/**
|
|
149
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning
|
|
182
|
+
* manifest's inline `mcpServers`. Portable v1 suites read only `mcp.json` at
|
|
183
|
+
* the plugin root (§7.2.1: no alternative core path, no inline declarations).
|
|
184
|
+
*/
|
|
185
|
+
export async function discoverMcp(root, errors, manifest, portable = false) {
|
|
150
186
|
const resolved = manifest ?? (await readManifest(root, errors, undefined));
|
|
151
187
|
const layout = resolved?.layout;
|
|
152
|
-
const names =
|
|
153
|
-
? ['
|
|
154
|
-
: layout === '
|
|
155
|
-
? ['.mcp.json'
|
|
156
|
-
: layout === '
|
|
157
|
-
? ['.mcp.json', '
|
|
158
|
-
: layout === '
|
|
159
|
-
? ['.mcp.json', 'mcp.json']
|
|
160
|
-
: layout === '
|
|
161
|
-
? ['mcp.json']
|
|
162
|
-
:
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
const names = portable
|
|
189
|
+
? ['mcp.json']
|
|
190
|
+
: layout === 'zcode'
|
|
191
|
+
? ['.mcp.json']
|
|
192
|
+
: layout === 'qoder'
|
|
193
|
+
? ['.mcp.json', 'mcp.json']
|
|
194
|
+
: layout === 'github-copilot' || layout === 'universal'
|
|
195
|
+
? ['.mcp.json', '.github/mcp.json', 'mcp.json']
|
|
196
|
+
: layout === 'codex' || layout === 'claude-code'
|
|
197
|
+
? ['.mcp.json', 'mcp.json']
|
|
198
|
+
: layout === 'cursor'
|
|
199
|
+
? ['mcp.json']
|
|
200
|
+
: ['mcp.json', '.mcp.json'];
|
|
201
|
+
const declared = portable ? undefined : resolved?.components?.mcpServers;
|
|
202
|
+
// Portable mode reads exactly the fixed location `mcp.json` (§7.2.1), so
|
|
203
|
+
// the candidate list is the location itself rather than a fallback probe.
|
|
204
|
+
// The file is optional for portable suites too — §7.2 fixes its location,
|
|
205
|
+
// not its presence — so an absent file resolves to zero servers without a
|
|
206
|
+
// diagnostic; an existing but unreadable path still fails loudly below.
|
|
207
|
+
const fallback = portable ? ((await isFile(join(root, 'mcp.json'))) ? 'mcp.json' : undefined) : layout === 'kimi' ? undefined : await firstComponentFile(root, names);
|
|
165
208
|
const additive = layout === 'zcode' || layout === 'claude-code';
|
|
166
209
|
const values = declared === undefined
|
|
167
210
|
? fallback === undefined
|
|
@@ -177,8 +220,11 @@ export async function discoverMcp(root, errors, manifest) {
|
|
|
177
220
|
let schema = '';
|
|
178
221
|
for (const document of documents) {
|
|
179
222
|
const isBareFile = document.path === join(root, 'mcp.json');
|
|
180
|
-
const strict = isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder'));
|
|
181
|
-
const result = await validateMcpJson(root, document.value, {
|
|
223
|
+
const strict = portable || (isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder')));
|
|
224
|
+
const result = await validateMcpJson(root, document.value, {
|
|
225
|
+
strict,
|
|
226
|
+
...(portable && resolved?.schemaVersion !== undefined ? { manifestVersion: recognizedSpecVersion(resolved.schemaVersion)?.version } : {})
|
|
227
|
+
});
|
|
182
228
|
errors.push(...result.errors);
|
|
183
229
|
if (result.config === undefined)
|
|
184
230
|
return undefined;
|
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`,
|
|
@@ -28,6 +29,8 @@ export const MARKET_ROUTES = {
|
|
|
28
29
|
setSurface: `${MARKET_API_PREFIX}set-surface`,
|
|
29
30
|
mcpOverrides: `${MARKET_API_PREFIX}mcp-overrides`,
|
|
30
31
|
setMcpOverride: `${MARKET_API_PREFIX}set-mcp-override`,
|
|
32
|
+
setMcpServerEnabled: `${MARKET_API_PREFIX}set-mcp-server-enabled`,
|
|
33
|
+
setMcpServerTool: `${MARKET_API_PREFIX}set-mcp-server-tool`,
|
|
31
34
|
mcpRetry: `${MARKET_API_PREFIX}mcp-retry`,
|
|
32
35
|
mcpReauthorize: `${MARKET_API_PREFIX}mcp-reauthorize`,
|
|
33
36
|
mcpBackend: `${MARKET_API_PREFIX}mcp-backend`,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe settings contract of the market's host settings namespace: the
|
|
3
|
+
* namespace key, the fields with their defaults, and the one reader that turns
|
|
4
|
+
* a stored section into resolved values.
|
|
5
|
+
*
|
|
6
|
+
* The defaults live here and nowhere else. The namespace schema declares them
|
|
7
|
+
* to the host, the node half reads them back through {@link resolveMarketSettings},
|
|
8
|
+
* and the browser half — which cannot import `src/runtime` — reads the same
|
|
9
|
+
* resolved values off the settings snapshot it is handed. Changing a default is
|
|
10
|
+
* therefore a one-line edit in this module.
|
|
11
|
+
*
|
|
12
|
+
* @module contracts/settings
|
|
13
|
+
*/
|
|
14
|
+
/** Settings namespace this plugin registers; the key the plugin-config tab pairs our card by. */
|
|
15
|
+
export const MARKET_SETTINGS_NAMESPACE = 'dsh-agent-plugins-market';
|
|
16
|
+
/**
|
|
17
|
+
* The default of every market setting — the single place one is written.
|
|
18
|
+
*
|
|
19
|
+
* These are user-visible behavior: a flip here is a behavior change that ships
|
|
20
|
+
* with its documentation, not a private implementation detail.
|
|
21
|
+
*/
|
|
22
|
+
export const MARKET_SETTINGS_DEFAULTS = {
|
|
23
|
+
mcpEnhanced: true,
|
|
24
|
+
scanProjectLayouts: false,
|
|
25
|
+
downloadRegion: 'auto',
|
|
26
|
+
feedbackEnabled: true,
|
|
27
|
+
autoUpdateSources: false
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Narrow an untrusted stored region to a setting; anything else reads as the
|
|
31
|
+
* declared default.
|
|
32
|
+
* @param value - the stored `downloadRegion` value.
|
|
33
|
+
* @param fallback - the value an unrecognized input reads as.
|
|
34
|
+
*/
|
|
35
|
+
export function narrowDownloadRegion(value, fallback = MARKET_SETTINGS_DEFAULTS.downloadRegion) {
|
|
36
|
+
return value === 'auto' || value === 'global' || value === 'china' ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
/** One boolean field: a stored boolean, else the field's default. */
|
|
39
|
+
function narrowBoolean(value, fallback) {
|
|
40
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a stored settings section the way the host resolves it: each field
|
|
44
|
+
* takes its stored value when it is one this field accepts, and its declared
|
|
45
|
+
* default otherwise.
|
|
46
|
+
*
|
|
47
|
+
* The host hands the resolved section back on its own, but not every caller has
|
|
48
|
+
* one — the plugin's own halves read a section they may not have received yet,
|
|
49
|
+
* and a test hands in a bare literal. Funnelling all of them through this
|
|
50
|
+
* function is what keeps a second reading of "what does missing mean" from
|
|
51
|
+
* growing somewhere else.
|
|
52
|
+
* @param section - the stored (or absent) namespace section, untrusted.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveMarketSettings(section) {
|
|
55
|
+
const stored = (typeof section === 'object' && section !== null ? section : {});
|
|
56
|
+
return {
|
|
57
|
+
mcpEnhanced: narrowBoolean(stored['mcpEnhanced'], MARKET_SETTINGS_DEFAULTS.mcpEnhanced),
|
|
58
|
+
scanProjectLayouts: narrowBoolean(stored['scanProjectLayouts'], MARKET_SETTINGS_DEFAULTS.scanProjectLayouts),
|
|
59
|
+
downloadRegion: narrowDownloadRegion(stored['downloadRegion']),
|
|
60
|
+
feedbackEnabled: narrowBoolean(stored['feedbackEnabled'], MARKET_SETTINGS_DEFAULTS.feedbackEnabled),
|
|
61
|
+
autoUpdateSources: narrowBoolean(stored['autoUpdateSources'], MARKET_SETTINGS_DEFAULTS.autoUpdateSources)
|
|
62
|
+
};
|
|
63
|
+
}
|