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.
Files changed (114) hide show
  1. package/README.md +31 -26
  2. package/README.zh.md +31 -26
  3. package/client/client.js +29164 -1136
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +8 -1
  6. package/lib/application/catalog.js +27 -2
  7. package/lib/application/details.js +13 -2
  8. package/lib/application/install-store.js +1 -1
  9. package/lib/application/mcp-service.js +191 -19
  10. package/lib/application/panel-resources.js +16 -4
  11. package/lib/application/project-agent-roles.js +7 -1
  12. package/lib/catalog/archive.js +16 -1
  13. package/lib/catalog/manifests.js +124 -13
  14. package/lib/catalog/paths.js +8 -0
  15. package/lib/catalog/plugin-variables.js +38 -0
  16. package/lib/catalog/project-hooks.js +14 -1
  17. package/lib/catalog/scan-resolvers.js +117 -12
  18. package/lib/catalog/skills-parse.js +47 -0
  19. package/lib/catalog/source-catalog.js +8 -3
  20. package/lib/catalog/suite-components.js +2 -2
  21. package/lib/catalog/surfaces.js +73 -27
  22. package/lib/catalog/validate.js +205 -54
  23. package/lib/contracts/market.js +3 -0
  24. package/lib/contracts/settings.js +63 -0
  25. package/lib/index.js +32 -13
  26. package/lib/model/layouts.js +44 -1
  27. package/lib/model/preset-source.js +8 -0
  28. package/lib/routes.js +43 -2
  29. package/lib/runtime/agent-role-router.js +203 -56
  30. package/lib/runtime/commands-mounts.js +50 -9
  31. package/lib/runtime/deadline.js +32 -6
  32. package/lib/runtime/dynamic-context.js +140 -0
  33. package/lib/runtime/feedback-tool.js +7 -3
  34. package/lib/runtime/host-locale.js +45 -15
  35. package/lib/runtime/lsp-mounts.js +23 -2
  36. package/lib/runtime/mcp-backend.js +14 -15
  37. package/lib/runtime/mcp-client/config.js +8 -0
  38. package/lib/runtime/mcp-client/connection.js +9 -8
  39. package/lib/runtime/mcp-client/host-seams.js +8 -88
  40. package/lib/runtime/mcp-client/oauth.js +82 -14
  41. package/lib/runtime/mcp-client/tools.js +2 -1
  42. package/lib/runtime/mcp-config.js +182 -35
  43. package/lib/runtime/mcp-direct-config.js +49 -6
  44. package/lib/runtime/mcp-mounts.js +15 -3
  45. package/lib/runtime/mcp-overrides.js +131 -1
  46. package/lib/runtime/mcp-redaction.js +8 -0
  47. package/lib/runtime/mcp-status.js +21 -5
  48. package/lib/runtime/project-runtime.js +23 -9
  49. package/lib/runtime/reconcile-scheduler.js +6 -8
  50. package/lib/runtime/reconciler.js +2 -1
  51. package/lib/runtime/regions.js +0 -19
  52. package/lib/runtime/server-config.js +49 -8
  53. package/lib/runtime/settings-namespace.js +21 -13
  54. package/lib/runtime/skills-provider.js +33 -11
  55. package/lib/runtime/source-auto-update.js +10 -14
  56. package/lib/runtime/subagent-catalog.js +62 -22
  57. package/lib/runtime/timer-seat.js +80 -0
  58. package/lib/runtime/tool-registry-observer.js +24 -20
  59. package/lib/runtime/user-commands.js +5 -1
  60. package/lib/runtime/user-panels.js +110 -41
  61. package/lib/runtime/user-store.js +114 -58
  62. package/lib/types/application/catalog-context.d.ts +6 -0
  63. package/lib/types/application/catalog.d.ts +19 -1
  64. package/lib/types/application/install-store.d.ts +1 -1
  65. package/lib/types/application/mcp-service.d.ts +38 -3
  66. package/lib/types/application/ports.d.ts +1 -1
  67. package/lib/types/application/queries.d.ts +8 -1
  68. package/lib/types/catalog/manifests.d.ts +6 -2
  69. package/lib/types/catalog/paths.d.ts +6 -0
  70. package/lib/types/catalog/plugin-variables.d.ts +16 -0
  71. package/lib/types/catalog/skills-parse.d.ts +13 -0
  72. package/lib/types/catalog/source-catalog.d.ts +7 -2
  73. package/lib/types/catalog/surfaces.d.ts +14 -4
  74. package/lib/types/catalog/validate.d.ts +72 -11
  75. package/lib/types/contracts/market.d.ts +77 -0
  76. package/lib/types/contracts/mcp-status.d.ts +11 -0
  77. package/lib/types/contracts/settings.d.ts +59 -0
  78. package/lib/types/model/layouts.d.ts +30 -0
  79. package/lib/types/model/preset-source.d.ts +18 -0
  80. package/lib/types/model/types.d.ts +37 -1
  81. package/lib/types/runtime/agent-role-router.d.ts +96 -18
  82. package/lib/types/runtime/commands-mounts.d.ts +15 -7
  83. package/lib/types/runtime/deadline.d.ts +10 -11
  84. package/lib/types/runtime/dynamic-context.d.ts +81 -0
  85. package/lib/types/runtime/host-locale.d.ts +20 -2
  86. package/lib/types/runtime/lsp-mounts.d.ts +7 -0
  87. package/lib/types/runtime/mcp-backend.d.ts +8 -15
  88. package/lib/types/runtime/mcp-client/config.d.ts +8 -0
  89. package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
  90. package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
  91. package/lib/types/runtime/mcp-config.d.ts +75 -13
  92. package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
  93. package/lib/types/runtime/mcp-overrides.d.ts +44 -0
  94. package/lib/types/runtime/mcp-redaction.d.ts +6 -0
  95. package/lib/types/runtime/project-runtime.d.ts +10 -3
  96. package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
  97. package/lib/types/runtime/regions.d.ts +1 -4
  98. package/lib/types/runtime/server-config.d.ts +26 -1
  99. package/lib/types/runtime/settings-namespace.d.ts +10 -10
  100. package/lib/types/runtime/skills-provider.d.ts +11 -2
  101. package/lib/types/runtime/source-auto-update.d.ts +3 -4
  102. package/lib/types/runtime/subagent-catalog.d.ts +14 -2
  103. package/lib/types/runtime/timer-seat.d.ts +51 -0
  104. package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
  105. package/lib/types/runtime/user-panels.d.ts +65 -12
  106. package/lib/types/runtime/user-store.d.ts +65 -16
  107. package/package.json +23 -6
  108. package/schemas/1.0.0/mcp.schema.json +0 -22
  109. package/schemas/1.1.0/mcp.schema.json +120 -0
  110. package/schemas/1.1.0/plugin.schema.json +65 -0
  111. package/schemas/README.md +25 -19
  112. package/schemas/agent-plugins/spec.md +23 -9
  113. package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
  114. package/schemas/com.deepseek.harness/spec.md +52 -0
@@ -23,7 +23,8 @@ import { join } from 'node:path';
23
23
  import { sanitizeId } from './paths.js';
24
24
  import { isFile } from './fs-probes.js';
25
25
  import { isRecord } from './component-files.js';
26
- import { isRecognizedSchema, validatePluginManifest } from './validate.js';
26
+ import { formatSchemaErrors, isRecognizedSchema, validatePluginManifest } from './validate.js';
27
+ import { EXTENSION_NAMESPACE, NAMESPACE_SCHEMA_VERSIONS, V1_IGNORED_MANIFEST_KEYS } from '../model/layouts.js';
27
28
  import { PLUGIN_LAYOUTS, MANIFEST_ALIASES, MARKETPLACE_PATHS } from '../model/layouts.js';
28
29
  /**
29
30
  * Every manifest file a directory carries, in selection order: highest
@@ -65,14 +66,20 @@ export function componentDeclarations(record) {
65
66
  * go to `fallbacks`, so the winner stays usable while the caller can still
66
67
  * report why the higher-priority declaration was ignored. The winner's own
67
68
  * diagnostics stay in `errors` and remain fatal.
69
+ *
70
+ * `notes` carries the winner's non-fatal diagnostics — §5.2 unknown
71
+ * top-level fields, §8.1 extension data problems. Callers surface them
72
+ * without failing the suite.
68
73
  */
69
- export async function readManifest(root, errors, hint, fallbacks = []) {
74
+ export async function readManifest(root, errors, hint, fallbacks = [], notes = []) {
70
75
  const candidates = await detectManifests(root);
71
76
  for (const [index, candidate] of candidates.entries()) {
72
77
  const attempt = [];
73
- const manifest = await readOneManifest(root, candidate, attempt, hint);
78
+ const attemptNotes = [];
79
+ const manifest = await readOneManifest(root, candidate, attempt, hint, attemptNotes);
74
80
  if (manifest !== undefined) {
75
81
  errors.push(...attempt);
82
+ notes.push(...attemptNotes);
76
83
  return manifest;
77
84
  }
78
85
  if (index === candidates.length - 1)
@@ -83,7 +90,7 @@ export async function readManifest(root, errors, hint, fallbacks = []) {
83
90
  return undefined;
84
91
  }
85
92
  /** Parse one manifest candidate; undefined when this candidate is unusable. */
86
- async function readOneManifest(root, candidate, errors, hint) {
93
+ async function readOneManifest(root, candidate, errors, hint, notes = []) {
87
94
  let raw;
88
95
  try {
89
96
  raw = JSON.parse(await readFile(candidate.path, 'utf8'));
@@ -130,21 +137,47 @@ async function readOneManifest(root, candidate, errors, hint) {
130
137
  }
131
138
  }
132
139
  }
133
- const problems = kind === 'agent-plugin-v1' ? await validatePluginManifest(raw) : [];
134
- if (problems.length > 0) {
135
- errors.push(...problems.map(problem => `${candidate.path}: ${problem}`));
140
+ // v1 validation verdicts split per §5.2: fatal violations reject this
141
+ // candidate (the next one by priority is tried), ignored violations —
142
+ // unknown top-level fields and the whole `extensions` subtree — are
143
+ // reported through `notes` and the manifest keeps loading.
144
+ const verdict = kind === 'agent-plugin-v1' ? await validatePluginManifest(raw) : undefined;
145
+ if (verdict !== undefined && verdict.fatal.length > 0) {
146
+ errors.push(...formatSchemaErrors(verdict.fatal).map(problem => `${candidate.path}: ${problem}`));
136
147
  return undefined;
137
148
  }
149
+ const ignored = verdict === undefined ? [] : formatSchemaErrors(verdict.ignored);
150
+ if (kind === 'agent-plugin-v1') {
151
+ // §5.2 / §6.1: the portable manifest carries no component configuration
152
+ // and no client semantics beyond `extensions`; every inline key this
153
+ // manager would otherwise read is an unknown top-level field here.
154
+ for (const key of V1_IGNORED_MANIFEST_KEYS) {
155
+ if (raw[key] !== undefined)
156
+ ignored.push(`${key}: not part of the portable manifest; ignored`);
157
+ }
158
+ }
159
+ notes.push(...ignored.map(problem => `${candidate.path}: ${problem}`));
160
+ const harness = kind === 'agent-plugin-v1' ? parseHarnessExtension(record['extensions'], notes) : undefined;
138
161
  const name = pickString(record.name) ?? hint?.name ?? syntheticManifestName(root);
139
162
  const version = pickString(record.version) ?? hint?.version;
140
163
  const description = pickString(record.description) ?? hint?.description;
141
164
  const author = record.author;
142
165
  return {
143
- components: { ...componentDeclarations(hint ?? {}), ...fallbackComponents, ...componentDeclarations(raw) },
144
- ...(typeof raw.skillInstructions === 'string' ? { skillInstructions: raw.skillInstructions } : {}),
145
- ...(typeof raw.systemPrompt === 'string' ? { systemPrompt: raw.systemPrompt } : {}),
146
- ...(typeof raw.systemPromptPath === 'string' ? { systemPromptPath: raw.systemPromptPath } : {}),
147
- ...(typeof raw.sessionStart?.skill === 'string'
166
+ components: {
167
+ ...(kind === 'agent-plugin-v1' ? {} : componentDeclarations(hint ?? {})),
168
+ ...fallbackComponents,
169
+ ...(kind === 'agent-plugin-v1' ? {} : componentDeclarations(raw))
170
+ },
171
+ ...(typeof raw.skillInstructions === 'string' && kind !== 'agent-plugin-v1'
172
+ ? { skillInstructions: raw.skillInstructions }
173
+ : {}),
174
+ ...(typeof raw.systemPrompt === 'string' && kind !== 'agent-plugin-v1'
175
+ ? { systemPrompt: raw.systemPrompt }
176
+ : {}),
177
+ ...(typeof raw.systemPromptPath === 'string' && kind !== 'agent-plugin-v1'
178
+ ? { systemPromptPath: raw.systemPromptPath }
179
+ : {}),
180
+ ...(kind !== 'agent-plugin-v1' && typeof raw.sessionStart?.skill === 'string'
148
181
  ? { startupSkill: raw.sessionStart.skill }
149
182
  : {}),
150
183
  layout: kind,
@@ -161,9 +194,87 @@ async function readOneManifest(root, candidate, errors, hint) {
161
194
  ? { author: pickString(record.homepage) }
162
195
  : {}),
163
196
  keywords: Array.isArray(record.keywords) ? record.keywords.filter((entry) => typeof entry === 'string') : [],
164
- ...(isRecognizedSchema(record.$schema) ? { schemaVersion: record.$schema } : {})
197
+ ...(isRecognizedSchema(record.$schema) ? { schemaVersion: record.$schema } : {}),
198
+ ...(harness === undefined ? {} : { harness })
165
199
  };
166
200
  }
201
+ /**
202
+ * Read this client's §8.1 extension data from a v1 manifest's `extensions`
203
+ * object. Unknown namespaces are ignored without validating their contents
204
+ * (§8.1); our own namespace is validated against the namespace contract, and
205
+ * an unusable value is reported through `notes` instead of failing the
206
+ * plugin.
207
+ *
208
+ * @returns the parsed namespace data, or undefined when the suite declares
209
+ * none; diagnostics land in `notes`.
210
+ */
211
+ function parseHarnessExtension(extensions, notes) {
212
+ if (!isRecord(extensions) || extensions[EXTENSION_NAMESPACE] === undefined)
213
+ return undefined;
214
+ return parseHarnessNamespace(extensions[EXTENSION_NAMESPACE], notes);
215
+ }
216
+ /** Validate one namespace value against the namespace contract; diagnostics go to `notes`. */
217
+ function parseHarnessNamespace(value, notes) {
218
+ if (!isRecord(value)) {
219
+ notes.push(`extensions.${EXTENSION_NAMESPACE}: value must be an object; ignored`);
220
+ return undefined;
221
+ }
222
+ const schemaVersion = value['schemaVersion'];
223
+ if (typeof schemaVersion !== 'string' || !NAMESPACE_SCHEMA_VERSIONS.has(schemaVersion)) {
224
+ notes.push(`extensions.${EXTENSION_NAMESPACE}: unsupported or missing schemaVersion ${JSON.stringify(schemaVersion)}; supported: ${[...NAMESPACE_SCHEMA_VERSIONS].join(', ')}; namespace extensions are ignored`);
225
+ return undefined;
226
+ }
227
+ const policies = Object.create(null);
228
+ const rawPolicies = value['mcpServers'];
229
+ if (rawPolicies !== undefined) {
230
+ if (!isRecord(rawPolicies)) {
231
+ notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers: value must be an object; ignored`);
232
+ return { schemaVersion, mcpServers: policies };
233
+ }
234
+ for (const [name, policy] of Object.entries(rawPolicies)) {
235
+ if (!isRecord(policy)) {
236
+ notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}: policy must be an object; ignored`);
237
+ continue;
238
+ }
239
+ const auth = policy['auth'];
240
+ if (auth !== undefined &&
241
+ (!isRecord(auth) || (auth['enabled'] !== undefined && typeof auth['enabled'] !== 'boolean') || (auth['scope'] !== undefined && typeof auth['scope'] !== 'string'))) {
242
+ notes.push(`extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.auth: invalid auth declaration; ignored`);
243
+ continue;
244
+ }
245
+ const enabledTools = optionalStringArray(policy['enabledTools'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.enabledTools`, notes);
246
+ const disabledTools = optionalStringArray(policy['disabledTools'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.disabledTools`, notes);
247
+ const startupTimeoutMs = optionalPositiveNumber(policy['startupTimeoutMs'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.startupTimeoutMs`, notes);
248
+ const toolCallTimeoutMs = optionalPositiveNumber(policy['toolCallTimeoutMs'], `extensions.${EXTENSION_NAMESPACE}.mcpServers.${name}.toolCallTimeoutMs`, notes);
249
+ policies[name] = {
250
+ ...(enabledTools === undefined ? {} : { enabledTools }),
251
+ ...(disabledTools === undefined ? {} : { disabledTools }),
252
+ ...(startupTimeoutMs === undefined ? {} : { startupTimeoutMs }),
253
+ ...(toolCallTimeoutMs === undefined ? {} : { toolCallTimeoutMs }),
254
+ ...(auth === undefined || !isRecord(auth) ? {} : { auth: { enabled: auth['enabled'] !== false, ...(typeof auth['scope'] === 'string' ? { scope: auth['scope'] } : {}) } })
255
+ };
256
+ }
257
+ }
258
+ return { schemaVersion, mcpServers: policies };
259
+ }
260
+ function optionalStringArray(value, label, notes) {
261
+ if (value === undefined)
262
+ return undefined;
263
+ if (!Array.isArray(value) || !value.every(entry => typeof entry === 'string')) {
264
+ notes.push(`${label}: must be an array of strings; ignored`);
265
+ return undefined;
266
+ }
267
+ return value;
268
+ }
269
+ function optionalPositiveNumber(value, label, notes) {
270
+ if (value === undefined)
271
+ return undefined;
272
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
273
+ notes.push(`${label}: must be a positive number; ignored`);
274
+ return undefined;
275
+ }
276
+ return value;
277
+ }
167
278
  function pickString(value) {
168
279
  return typeof value === 'string' && value !== '' ? value : undefined;
169
280
  }
@@ -93,6 +93,14 @@ export function sourceCheckoutDir(dimensionRoot, sourceId) {
93
93
  export function qualifiedSuiteId(sourceId, suiteId) {
94
94
  return `${sourceId}/${suiteId}`;
95
95
  }
96
+ /**
97
+ * The data directory `${PLUGIN_DATA}` resolves to for one suite: a
98
+ * source-qualified directory under the plugin storage root, shared by every
99
+ * surface of that suite so MCP servers, skills and commands see one location.
100
+ */
101
+ export function suiteDataDir(dataRoot, sourceId, suiteId) {
102
+ return `${dataRoot.replace(/[\\/]$/, '')}/${qualifiedSuiteId(sourceId, suiteId)}`;
103
+ }
96
104
  /**
97
105
  * Whether `candidate` is `root` itself or a path below it, compared on whole
98
106
  * path segments by `flavor`'s rules so a sibling whose name merely starts with
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Path-variable expansion for author-written suite text.
3
+ *
4
+ * A suite authored for Claude Code or its dialects writes `${CLAUDE_PLUGIN_ROOT}`
5
+ * (or the dialect's own spelling) into command bodies, agent personas, hook
6
+ * commands, LSP declarations, startup instructions and skill prose, and uses
7
+ * `${CLAUDE_PLUGIN_DATA}`, `${CLAUDE_PROJECT_DIR}` and `${CLAUDE_SKILL_DIR}` the
8
+ * same way. Its home ecosystem replaces those variables before the text reaches
9
+ * the model or a process; every surface here has to do the same, or the command
10
+ * runs with an empty path. One implementation keeps the surfaces agreeing on
11
+ * what each spelling means.
12
+ *
13
+ * A variable whose value the calling layer does not hold is left exactly as
14
+ * written: an author's `${NAME:-default}`-style intent is never silently
15
+ * emptied, and a surface that cannot supply a session directory does not
16
+ * invent one.
17
+ */
18
+ import { PLUGIN_DATA_VARIABLES, PLUGIN_ROOT_VARIABLES, PROJECT_DIR_VARIABLES, SKILL_DIR_VARIABLE } from '../model/layouts.js';
19
+ /** The suite root `${PLUGIN_ROOT}` and its dialect aliases resolve to. */
20
+ export function pluginRootOf(suite) {
21
+ // Project-native directories are the repository's own files, read in place;
22
+ // no plugin root exists for them, so the variable stays as the author wrote it.
23
+ return suite.manifest.layout === 'project-native' ? undefined : suite.root;
24
+ }
25
+ /** Replace every path variable the context can resolve; other text stays exactly as written. */
26
+ export function expandPluginPaths(text, context) {
27
+ return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => {
28
+ if (PLUGIN_ROOT_VARIABLES.has(name))
29
+ return context.root ?? match;
30
+ if (PLUGIN_DATA_VARIABLES.has(name))
31
+ return context.data ?? match;
32
+ if (name === SKILL_DIR_VARIABLE)
33
+ return context.skillDir ?? match;
34
+ if (PROJECT_DIR_VARIABLES.has(name))
35
+ return context.projectDir ?? match;
36
+ return match;
37
+ });
38
+ }
@@ -11,6 +11,19 @@ function object(value) {
11
11
  function declarationText(value) {
12
12
  return String(value);
13
13
  }
14
+ /**
15
+ * Present one read document as the settings shape the shared validator reads:
16
+ * a standalone hook file may carry the bare event table instead of a `hooks`
17
+ * key. A recognized top-level event name is the marker — a document with no
18
+ * event name at the top stays verbatim, so an ordinary settings file missing
19
+ * `hooks` is still not accepted as a bare event table.
20
+ */
21
+ function settingsDocument(settings, format) {
22
+ if (format !== 'claude' || settings.hooks !== undefined)
23
+ return settings;
24
+ const isEventTable = Object.keys(settings).some(key => EVENTS.has(key));
25
+ return isEventTable ? { ...settings, hooks: settings } : settings;
26
+ }
14
27
  /** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
15
28
  export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
16
29
  const documents = [];
@@ -19,7 +32,7 @@ export async function discoverProjectHooks(projectRoot, files, errors, format =
19
32
  if (settings === null)
20
33
  return undefined;
21
34
  if (settings !== undefined)
22
- documents.push({ file, settings });
35
+ documents.push({ file, settings: settingsDocument(settings, format) });
23
36
  }
24
37
  return normalizeHookDocuments(projectRoot, documents, errors, format);
25
38
  }
@@ -18,14 +18,17 @@
18
18
  * 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
19
19
  * `<root>/<name>/SKILL.md` collections.
20
20
  */
21
- import { realpath } from 'node:fs/promises';
21
+ import { realpath, readFile } from 'node:fs/promises';
22
22
  import { join, resolve } from 'node:path';
23
23
  import { isWithin, sanitizeId } from './paths.js';
24
24
  import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
25
+ import { EXTENSION_NAMESPACE, NAMESPACE_DIR, NAMESPACE_HOOKS_FILE, NAMESPACE_LSP_FILE } from '../model/layouts.js';
25
26
  import { componentDeclarations, hasSuiteManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
26
27
  import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
27
28
  import { discoverMarkdownResources, isUnknownArray } from './component-files.js';
28
29
  import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
30
+ import { parseLspServers } from './lsp-spec.js';
31
+ import { normalizeHookDocuments } from './project-hooks.js';
29
32
  import { runScanChain } from './scan-pipeline.js';
30
33
  const CONTAINER_DIRS = ['plugins', 'external_plugins', 'skills'];
31
34
  /**
@@ -327,10 +330,11 @@ export async function hasSkillFiles(dir) {
327
330
  /** Read one suite root into the normalized shape, or undefined when no manifest parses. */
328
331
  export async function readSuite(root, sourceId, dimension, hint, notes = []) {
329
332
  const errors = [];
333
+ const manifestNotes = [];
330
334
  const fallbacks = [];
331
335
  const declaredManifest = await hasSuiteManifest(root);
332
- let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks) : await syntheticManifest(root);
333
- if (declaredManifest && (manifest === undefined || errors.length > 0)) {
336
+ let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks, manifestNotes) : await syntheticManifest(root);
337
+ if (declaredManifest && manifest === undefined) {
334
338
  notes.push(...errors, ...fallbacks, `suite ${root}: rejected declared manifest`);
335
339
  return undefined;
336
340
  }
@@ -338,6 +342,7 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
338
342
  // higher-priority declaration was skipped visible without failing the suite.
339
343
  if (fallbacks.length > 0)
340
344
  notes.push(`suite ${root}: a higher-priority manifest was rejected`, ...fallbacks);
345
+ notes.push(...manifestNotes);
341
346
  // A declaration-only suite (official CC lsp plugins ship just a README):
342
347
  // the marketplace entry's inline lspServers are its manifest.
343
348
  if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
@@ -354,24 +359,41 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
354
359
  }
355
360
  if (manifest === undefined)
356
361
  return undefined;
362
+ // The portable v1 dialect reads only the spec's fixed locations plus this
363
+ // client's extension namespace (§6.1, §8.2): skills and MCP from their
364
+ // fixed locations, extension surfaces from `com.deepseek.harness/`.
365
+ // Marketplace-entry and hint declarations carry no inline components for
366
+ // this dialect, so `components` stays empty and every discovery call below
367
+ // takes its namespace path.
368
+ const portable = manifest.layout === 'agent-plugin-v1';
369
+ // The namespace seats (manifest data and directory) open only when the
370
+ // suite declares our namespace with a supported contract version; an
371
+ // unsupported or missing declaration reads the portable core alone.
372
+ const namespaceOpen = portable && (manifest.harness !== undefined || manifest.schemaVersion !== undefined) && namespaceReadable(manifest);
357
373
  let declared = manifest.components?.skills;
358
374
  if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
359
375
  declared = ['skills', ...(isUnknownArray(declared) ? declared : [declared])];
360
376
  }
361
- const skills = await discoverSkills(root, errors, declared);
377
+ if (portable)
378
+ await noteUnreadRootComponents(root, notes);
379
+ const skills = await discoverSkills(root, errors, declared, portable);
362
380
  if (manifest.layout === 'skill-collection' && skills.length === 0) {
363
381
  notes.push(...errors);
364
382
  return undefined;
365
383
  }
366
- const mcp = await discoverMcp(root, errors, manifest);
384
+ const mcp = await discoverMcp(root, errors, manifest, portable);
367
385
  // Effective component declarations include marketplace fallbacks and manifest overrides.
368
- const lsp = await discoverSuiteLsp(root, manifest, errors);
369
- const hooks = await discoverSuiteHooks(root, manifest, errors);
370
- const resources = {
371
- commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
372
- agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
373
- };
374
- const systemPrompt = await discoverSystemPrompt(root, manifest, errors);
386
+ const lsp = namespaceOpen ? await discoverNamespaceLsp(root, manifest, errors, notes) : await discoverSuiteLsp(root, manifest, errors);
387
+ const hooks = namespaceOpen ? await discoverNamespaceHooks(root, manifest, errors, notes) : await discoverSuiteHooks(root, manifest, errors);
388
+ const resources = namespaceOpen
389
+ ? await discoverNamespaceMarkdown(root, notes)
390
+ : portable
391
+ ? { commands: [], agents: [] }
392
+ : {
393
+ commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
394
+ agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
395
+ };
396
+ const systemPrompt = portable ? undefined : await discoverSystemPrompt(root, manifest, errors);
375
397
  const surfaces = await countSurfaces(root, skills, mcp, lsp);
376
398
  surfaces.commands = resources.commands.length;
377
399
  surfaces.agents = resources.agents.length;
@@ -428,3 +450,86 @@ async function syntheticManifest(root) {
428
450
  name
429
451
  };
430
452
  }
453
+ // ---------------------------------------------------------------------------
454
+ // Extension namespace surfaces (Agent Plugins §8.2)
455
+ //
456
+ // A portable v1 suite carries this client's extension surfaces in the
457
+ // top-level directory named after the namespace. Contents outside it — the
458
+ // dialect root directories other layouts read — are package files this
459
+ // manager does not mount for the portable dialect, and their presence is
460
+ // reported through scan notes rather than read silently.
461
+ // ---------------------------------------------------------------------------
462
+ /** Root component locations other dialects read; under v1 their presence is reported, never read. */
463
+ const ROOT_COMPONENT_DIRS = ['commands', 'agents', 'hooks'];
464
+ /** `.mcp.json` is an alternative MCP path the portable format forbids; `mcp.json` itself is the fixed location. */
465
+ const ROOT_COMPONENT_FILES = ['hooks.json', 'lsp.json', '.lsp.json', '.mcp.json'];
466
+ /**
467
+ * Whether the namespace contract allows reading the extension seats: the
468
+ * manifest must carry our namespace with a supported `schemaVersion`. A
469
+ * directory without manifest data stays closed until the suite declares the
470
+ * contract, so a name collision on the filesystem cannot silently activate
471
+ * extension reading.
472
+ */
473
+ function namespaceReadable(manifest) {
474
+ return manifest.harness !== undefined;
475
+ }
476
+ /** Report portable-suite content this manager does not mount, so nothing fails silently. */
477
+ async function noteUnreadRootComponents(root, notes) {
478
+ for (const dir of ROOT_COMPONENT_DIRS) {
479
+ if (await isDirectory(join(root, dir)))
480
+ notes.push(`suite ${root}: "${dir}/" is outside the portable layout and the ${EXTENSION_NAMESPACE} namespace; not read for this dialect`);
481
+ }
482
+ for (const file of ROOT_COMPONENT_FILES) {
483
+ if (await isFile(join(root, file)))
484
+ notes.push(`suite ${root}: "${file}" is outside the portable layout; not read for this dialect`);
485
+ }
486
+ }
487
+ /** Commands and agents from `<namespace>/commands/` and `<namespace>/agents/`. */
488
+ async function discoverNamespaceMarkdown(root, notes) {
489
+ const namespaceRoot = join(root, NAMESPACE_DIR);
490
+ if (!(await isDirectory(namespaceRoot)))
491
+ return { commands: [], agents: [] };
492
+ const errors = [];
493
+ const commands = await discoverMarkdownResources(namespaceRoot, 'commands', undefined, join(namespaceRoot, 'plugin.json'), errors);
494
+ const agents = await discoverMarkdownResources(namespaceRoot, 'agents', undefined, join(namespaceRoot, 'plugin.json'), errors);
495
+ notes.push(...errors.map(error => `${EXTENSION_NAMESPACE}: ${error}`));
496
+ return { commands, agents };
497
+ }
498
+ /** Hooks from `<namespace>/hooks/hooks.json`, normalized through the command-hook bridge. */
499
+ async function discoverNamespaceHooks(root, manifest, errors, notes) {
500
+ const file = join(root, NAMESPACE_HOOKS_FILE);
501
+ if (!(await isFile(file)))
502
+ return undefined;
503
+ let value;
504
+ try {
505
+ value = JSON.parse(await readFile(file, 'utf8'));
506
+ }
507
+ catch (error) {
508
+ notes.push(`${NAMESPACE_HOOKS_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
509
+ return undefined;
510
+ }
511
+ const events = value?.hooks ?? value;
512
+ if (typeof events !== 'object' || events === null) {
513
+ notes.push(`${NAMESPACE_HOOKS_FILE}: must contain an event table`);
514
+ return undefined;
515
+ }
516
+ return normalizeHookDocuments(root, [{ file: NAMESPACE_HOOKS_FILE, settings: { hooks: events } }], errors);
517
+ }
518
+ /** LSP servers from `<namespace>/lsp.json`. */
519
+ async function discoverNamespaceLsp(root, manifest, errors, notes) {
520
+ const file = join(root, NAMESPACE_LSP_FILE);
521
+ if (!(await isFile(file)))
522
+ return undefined;
523
+ let value;
524
+ try {
525
+ value = JSON.parse(await readFile(file, 'utf8'));
526
+ }
527
+ catch (error) {
528
+ notes.push(`${NAMESPACE_LSP_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
529
+ return undefined;
530
+ }
531
+ const servers = Object.create(null);
532
+ const document = value.lspServers ?? value;
533
+ Object.assign(servers, parseLspServers(document, notes));
534
+ return Object.keys(servers).length > 0 ? { servers } : undefined;
535
+ }
@@ -99,6 +99,53 @@ export function parseSkillFrontmatter(text, expectedName) {
99
99
  }
100
100
  };
101
101
  }
102
+ /** Frontmatter record for the panel's host-parity check, or the reason the YAML is unusable. */
103
+ function strictSkillRecord(text) {
104
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
105
+ if (frontmatter === undefined)
106
+ return 'missing YAML frontmatter';
107
+ let raw;
108
+ try {
109
+ raw = parseYaml(frontmatter);
110
+ }
111
+ catch {
112
+ return 'invalid YAML frontmatter';
113
+ }
114
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
115
+ return 'frontmatter is not an object';
116
+ return raw;
117
+ }
118
+ /**
119
+ * Rejection for a user skill document the harness's own `dsh-skill-filesystem`
120
+ * reader would drop from `~/.agents/skills`, or `undefined` when that reader
121
+ * accepts it.
122
+ *
123
+ * The two readers share the directory, so the panel publishes only documents
124
+ * both accept: a listed entry never becomes a skill that loads in a
125
+ * market-equipped session alone. The checks mirror the shipped reader rather
126
+ * than the lenient suite scan — strict YAML, no display-name normalization,
127
+ * and a `name` the reader compares only against its kebab grammar.
128
+ * @returns the drop reason, phrased as the reader's own diagnostic.
129
+ */
130
+ export function skillEntryRejection(text) {
131
+ const record = strictSkillRecord(text);
132
+ if (typeof record === 'string')
133
+ return record;
134
+ const name = record['name'];
135
+ const description = record['description'];
136
+ if (typeof name !== 'string' || name === '' || typeof description !== 'string' || description.trim() === '') {
137
+ return 'frontmatter requires name and description';
138
+ }
139
+ if (!isSkillName(name))
140
+ return `invalid skill name "${name}"`;
141
+ const disableModel = parseBoolean(record['disable-model-invocation']);
142
+ if (disableModel === undefined && record['disable-model-invocation'] !== undefined)
143
+ return 'invalid disable-model-invocation value';
144
+ const userInvocable = parseBoolean(record['user-invocable']);
145
+ if (userInvocable === undefined && record['user-invocable'] !== undefined)
146
+ return 'invalid user-invocable value';
147
+ return undefined;
148
+ }
102
149
  /** Lenient frontmatter recovery for prose fields with embedded `: `. */
103
150
  function lenientFrontmatter(body) {
104
151
  const record = {};
@@ -15,10 +15,15 @@ import { scanSource as scanSourceWithNotes } from './suite-scanner.js';
15
15
  import { discoverNativeProjectSuites } from './native-project.js';
16
16
  /** Discover suites from the selected configured or project checkouts. */
17
17
  export async function discoverSourceList(sources, dimension, dimensionRoot) {
18
- return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
18
+ return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot, true)).suites;
19
19
  }
20
- /** Discover suites plus per-source scan diagnostics for one dimension. */
21
- export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts = false) {
20
+ /**
21
+ * Discover suites plus per-source scan diagnostics for one dimension.
22
+ * @param scanProjectLayouts - whether the project's own native Agent directories
23
+ * join the scan. Required on purpose: the switch has one owner (the host
24
+ * settings document), so a default here would be a second copy of it.
25
+ */
26
+ export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts) {
22
27
  const checkoutRoot = sourcesDir(dimensionRoot);
23
28
  const listed = new Set(sources.map(source => source.id));
24
29
  const checkouts = sources.map(source => ({
@@ -4,7 +4,7 @@ import { relative } from 'node:path';
4
4
  import { componentDocuments, componentPath, firstComponentFile, isRecord, isStringArray, isUnknownArray } from './component-files.js';
5
5
  import { parseLspServers } from './lsp-spec.js';
6
6
  import { normalizeHookDocuments } from './project-hooks.js';
7
- import { PLUGIN_ROOT_VARIABLES } from '../model/layouts.js';
7
+ import { expandPluginPaths } from './plugin-variables.js';
8
8
  export async function discoverSuiteLsp(root, manifest, errors) {
9
9
  const declaration = manifest.components?.lspServers;
10
10
  const fallback = await firstComponentFile(root, manifest.layout === 'github-copilot' || manifest.layout === 'universal' ? ['lsp.json', '.github/lsp.json', 'lsp-config/servers.json', '.lsp.json'] : ['.lsp.json', 'lsp.json']);
@@ -22,7 +22,7 @@ function quote(argument) {
22
22
  return `'${argument.replaceAll("'", "'\\''")}'`;
23
23
  }
24
24
  function rootVariables(text, root) {
25
- return text.replace(/\$\{([A-Z_]+)\}/g, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? root : match));
25
+ return expandPluginPaths(text, { root });
26
26
  }
27
27
  const EVENT_ALIASES = {
28
28
  sessionStart: 'SessionStart',