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/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { RuntimeReconciler } from './runtime/reconciler.js';
|
|
|
4
4
|
import { ReconcileScheduler } from './runtime/reconcile-scheduler.js';
|
|
5
5
|
import { MarketSettingsNamespace } from './runtime/settings-namespace.js';
|
|
6
6
|
import { deleteMcpAuthGrant } from './runtime/mcp-auth-record.js';
|
|
7
|
-
import { inspectToolRegistry } from './runtime/tool-registry-observer.js';
|
|
7
|
+
import { inspectToolRegistry, toolsServiceOf } from './runtime/tool-registry-observer.js';
|
|
8
8
|
import { migratePluginStorage } from './runtime/storage-migration.js';
|
|
9
9
|
import { mountAgentRoleTool } from './runtime/agent-role-router.js';
|
|
10
10
|
import { projectAgentRoles } from './application/project-agent-roles.js';
|
|
@@ -13,13 +13,15 @@ import { createPanelResources } from './application/panel-resources.js';
|
|
|
13
13
|
import { resolveAgentsRoot, resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
|
|
14
14
|
import { mountSuiteRoutes } from './routes.js';
|
|
15
15
|
import { SuiteSkillProvider } from './runtime/skills-provider.js';
|
|
16
|
+
import { shellSeamOf } from './runtime/dynamic-context.js';
|
|
16
17
|
import { loadLspServers } from './runtime/lsp-direct-config.js';
|
|
17
18
|
import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
|
|
18
|
-
import { bindHostLocale, loadHostLocale } from './runtime/host-locale.js';
|
|
19
|
+
import { bindHostLocale, loadHostLocale, readHostLocalePreference, setHostLocaleSource } from './runtime/host-locale.js';
|
|
19
20
|
import { createUserPanelStores } from './runtime/user-panels.js';
|
|
20
21
|
import { SourceAutoUpdater } from './runtime/source-auto-update.js';
|
|
21
22
|
import { UserPanelSkillProvider } from './runtime/user-panels.js';
|
|
22
23
|
import { UserCommandMountRegistry } from './runtime/user-commands.js';
|
|
24
|
+
import { presetSourceRef } from './model/preset-source.js';
|
|
23
25
|
export const name = 'dsh-agent-plugins-market';
|
|
24
26
|
export const inject = ['skills', 'commands'];
|
|
25
27
|
export async function apply(ctx, config = {}) {
|
|
@@ -34,10 +36,20 @@ export async function apply(ctx, config = {}) {
|
|
|
34
36
|
// Host runtime copy resolves from the harness `locale.preference` setting;
|
|
35
37
|
// the async settings read lands before the first session starts in practice.
|
|
36
38
|
const hostLocale = { t: bindHostLocale(undefined) };
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const refreshHostLocale = () => {
|
|
40
|
+
void loadHostLocale().then(locale => {
|
|
41
|
+
hostLocale.t = locale.t;
|
|
42
|
+
providerControl?.invalidate();
|
|
43
|
+
userPanelControl?.invalidate();
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
void refreshHostLocale();
|
|
47
|
+
// The locale preference the GUI writes is owned by the host settings service:
|
|
48
|
+
// read it there rather than re-parsing the document, and re-bind once the
|
|
49
|
+
// service lands (the locale namespace may register after this plugin).
|
|
50
|
+
ctx.inject(['settings'], settingsCtx => {
|
|
51
|
+
setHostLocaleSource(() => readHostLocalePreference(settingsCtx));
|
|
52
|
+
refreshHostLocale();
|
|
41
53
|
});
|
|
42
54
|
const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
|
|
43
55
|
// User panel stores (skills / commands / agent personas) and their runtime
|
|
@@ -110,9 +122,10 @@ export async function apply(ctx, config = {}) {
|
|
|
110
122
|
// port below is read at call time: this plugin's apply may run before the
|
|
111
123
|
// credentials, tools and settings services provision, and a snapshot taken
|
|
112
124
|
// here would be permanently undefined even after the service is live.
|
|
113
|
-
|
|
125
|
+
// Read at call time: the tools service provisions after apply() returns.
|
|
126
|
+
let toolsRegistry = toolsServiceOf(ctx);
|
|
114
127
|
const ports = {
|
|
115
|
-
mcpToolSnapshot: () =>
|
|
128
|
+
mcpToolSnapshot: () => inspectToolRegistry(toolsRegistry),
|
|
116
129
|
credentialsStore: {
|
|
117
130
|
deleteGrantRecord: async (serverName) => {
|
|
118
131
|
const store = ctx.get?.('credentials');
|
|
@@ -132,7 +145,10 @@ export async function apply(ctx, config = {}) {
|
|
|
132
145
|
};
|
|
133
146
|
const catalog = new Catalog({ userRoot, dataRoot, agentsRoot, onChanged, ports, ...(config.git === undefined ? {} : { git: config.git }) });
|
|
134
147
|
await catalog.load();
|
|
135
|
-
|
|
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()]);
|
|
136
152
|
const resources = createPanelResources(catalog, panels);
|
|
137
153
|
runtime.setMcpOverridesProvider(async () => catalog.allMcpOverrides(await catalog.enabledUserSuites()));
|
|
138
154
|
runtime.lsp.setDirectProvider(async () => (await loadLspServers(agentsRoot)).servers);
|
|
@@ -155,9 +171,12 @@ export async function apply(ctx, config = {}) {
|
|
|
155
171
|
await scheduler.request();
|
|
156
172
|
})
|
|
157
173
|
.catch(() => { });
|
|
174
|
+
// The shell seam resolves lazily: the service may land after this plugin, and
|
|
175
|
+
// a profile without one keeps dynamic-context placeholders literal.
|
|
176
|
+
const shellSeam = () => shellSeamOf(ctx);
|
|
158
177
|
ctx.skills.registerProvider(control => {
|
|
159
178
|
providerControl = control;
|
|
160
|
-
return new SuiteSkillProvider(catalog);
|
|
179
|
+
return new SuiteSkillProvider(catalog, { dataRoot, shell: shellSeam });
|
|
161
180
|
});
|
|
162
181
|
// User panel skills ride a second provider so a panel
|
|
163
182
|
// edit invalidates only its own catalog contribution.
|
|
@@ -169,14 +188,14 @@ export async function apply(ctx, config = {}) {
|
|
|
169
188
|
hostCtx.effect(() => mountAgentRoleTool(hostCtx, async (parent) => [
|
|
170
189
|
...(await resources.agents.list(true)).map(entry => ({ ...entry, title: entry.name, name: entry.id ?? entry.name })),
|
|
171
190
|
...(await projectAgentRoles(catalog, parent))
|
|
172
|
-
]
|
|
191
|
+
]), 'dsh-agent-plugins-market: agent role routing');
|
|
173
192
|
});
|
|
174
193
|
ctx.inject(['agents'], hostCtx => {
|
|
175
194
|
hostCtx.effect(() => {
|
|
176
|
-
const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key));
|
|
195
|
+
const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key), dataRoot);
|
|
177
196
|
const mcp = mountProjectMcp(hostCtx, catalog, dataRoot);
|
|
178
197
|
const hooks = mountProjectHooks(hostCtx, catalog);
|
|
179
|
-
const prompts = mountSuiteInstructions(hostCtx, catalog);
|
|
198
|
+
const prompts = mountSuiteInstructions(hostCtx, catalog, dataRoot);
|
|
180
199
|
projectCommands = mounted;
|
|
181
200
|
projectMcp = mcp;
|
|
182
201
|
projectHooks = hooks;
|
package/lib/model/layouts.js
CHANGED
|
@@ -12,9 +12,29 @@ 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']);
|
|
34
|
+
/** Equivalent session project-directory variables; only a calling session supplies the directory. */
|
|
35
|
+
export const PROJECT_DIR_VARIABLES = new Set(['CLAUDE_PROJECT_DIR', 'ZCODE_PROJECT_DIR', 'QODER_PROJECT_DIR']);
|
|
36
|
+
/** Claude Code names the directory holding the skill's own file; no dialect alias exists. */
|
|
37
|
+
export const SKILL_DIR_VARIABLE = 'CLAUDE_SKILL_DIR';
|
|
18
38
|
export const PROJECT_LAYOUTS = [
|
|
19
39
|
{
|
|
20
40
|
dirName: '.claude',
|
|
@@ -23,7 +43,12 @@ export const PROJECT_LAYOUTS = [
|
|
|
23
43
|
mcpFiles: ['.mcp.json'],
|
|
24
44
|
hookFiles: ['.claude/settings.json', '.claude/settings.local.json']
|
|
25
45
|
},
|
|
26
|
-
{
|
|
46
|
+
{
|
|
47
|
+
dirName: '.agents',
|
|
48
|
+
label: 'agents',
|
|
49
|
+
subdirs: ['skills', 'agents', 'commands'],
|
|
50
|
+
hookFiles: ['.agents/hooks/hooks.json', '.agents/hooks.json']
|
|
51
|
+
},
|
|
27
52
|
{ dirName: '.codex', label: 'Codex', subdirs: ['skills'], mcpFiles: ['.codex/config.toml'], mcpFormat: 'codex' },
|
|
28
53
|
{ dirName: '.cursor', label: 'Cursor', subdirs: ['skills', 'agents', 'commands'], mcpFiles: ['.cursor/mcp.json'] },
|
|
29
54
|
{ dirName: '.kimi', label: 'Kimi', subdirs: ['skills'] },
|
|
@@ -47,3 +72,21 @@ export const PROJECT_LAYOUTS = [
|
|
|
47
72
|
];
|
|
48
73
|
/** Marketplace catalogs follow manifest layout order; the shared root catalog is the fallback. */
|
|
49
74
|
export const MARKETPLACE_PATHS = [...PLUGIN_LAYOUTS.flatMap(layout => layout.marketplaces), 'marketplace.json'];
|
|
75
|
+
/**
|
|
76
|
+
* Inline top-level manifest keys this manager reads for the portable v1
|
|
77
|
+
* dialect. The specification's closed manifest schema carries none of them
|
|
78
|
+
* (§5.2, §6.1), so under `agent-plugin-v1` they are reported as unknown
|
|
79
|
+
* top-level fields and ignored; every other dialect keeps reading them.
|
|
80
|
+
*/
|
|
81
|
+
export const V1_IGNORED_MANIFEST_KEYS = [
|
|
82
|
+
'skills',
|
|
83
|
+
'commands',
|
|
84
|
+
'agents',
|
|
85
|
+
'hooks',
|
|
86
|
+
'mcpServers',
|
|
87
|
+
'lspServers',
|
|
88
|
+
'skillInstructions',
|
|
89
|
+
'systemPrompt',
|
|
90
|
+
'systemPromptPath',
|
|
91
|
+
'sessionStart'
|
|
92
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Source id the preset record registers under. */
|
|
2
|
+
export const PRESET_SOURCE_ID = 'dsh-agent-plugins';
|
|
3
|
+
/** Repository the preset record points at. */
|
|
4
|
+
export const PRESET_SOURCE_URL = 'https://github.com/Sivan757/dsh-agent-plugins.git';
|
|
5
|
+
/** The preset registration, as the market stores it. */
|
|
6
|
+
export function presetSourceRef() {
|
|
7
|
+
return { id: PRESET_SOURCE_ID, url: PRESET_SOURCE_URL, kind: 'git' };
|
|
8
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -39,7 +39,14 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
39
39
|
sendJson(response, 200, { ok: true, ...value });
|
|
40
40
|
}
|
|
41
41
|
catch (error) {
|
|
42
|
-
|
|
42
|
+
// A rejection that names its fields travels with them, so the editor
|
|
43
|
+
// can place each reason beside the input it belongs to.
|
|
44
|
+
const fields = error.fields;
|
|
45
|
+
sendJson(response, 400, {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: error instanceof Error ? error.message : String(error),
|
|
48
|
+
...(Array.isArray(fields) ? { fields } : {})
|
|
49
|
+
});
|
|
43
50
|
}
|
|
44
51
|
})();
|
|
45
52
|
}
|
|
@@ -77,7 +84,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
77
84
|
throw new Error('invalid service kind');
|
|
78
85
|
if (typeof body.id !== 'string' || body.id === '')
|
|
79
86
|
throw new Error('missing service id');
|
|
80
|
-
await manager.saveServerConfig(body.kind, body.id, body.config);
|
|
87
|
+
await manager.saveServerConfig(body.kind, body.id, body.config, body.policy);
|
|
81
88
|
return {};
|
|
82
89
|
});
|
|
83
90
|
post(MARKET_ROUTES.addMcpServer, async (body) => {
|
|
@@ -86,6 +93,13 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
86
93
|
await manager.addMcpServer(body.name, body.config);
|
|
87
94
|
return {};
|
|
88
95
|
});
|
|
96
|
+
// Paste import: many services in one write. Each entry reports its own
|
|
97
|
+
// outcome, so one rejected definition never fails the whole paste.
|
|
98
|
+
post(MARKET_ROUTES.importMcpServers, async (body) => {
|
|
99
|
+
const overwrite = body['overwrite'] === true;
|
|
100
|
+
const result = await manager.importMcpServers(body['servers'], overwrite);
|
|
101
|
+
return { imported: result.imported, skipped: result.skipped };
|
|
102
|
+
});
|
|
89
103
|
get(MARKET_ROUTES.lspStatus, async (_request, response) => {
|
|
90
104
|
sendJson(response, 200, await manager.lspStatus());
|
|
91
105
|
});
|
|
@@ -257,6 +271,33 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
257
271
|
await manager.setMcpOverride(sourceId, suiteId, serverKey, sanitized ?? null);
|
|
258
272
|
return {};
|
|
259
273
|
});
|
|
274
|
+
// Enable or disable one declared MCP server. Addressed by the same
|
|
275
|
+
// source-qualified suite id the status rows carry, so the card can toggle a
|
|
276
|
+
// server without holding the source and suite ids apart.
|
|
277
|
+
post(MARKET_ROUTES.setMcpServerEnabled, async (body) => {
|
|
278
|
+
const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
|
|
279
|
+
const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
|
|
280
|
+
const enabled = body['enabled'];
|
|
281
|
+
if (typeof enabled !== 'boolean')
|
|
282
|
+
throw new Error('missing boolean enabled');
|
|
283
|
+
await manager.setMcpServerEnabled(suiteKey, serverKey, enabled);
|
|
284
|
+
return {};
|
|
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
|
+
});
|
|
260
301
|
// Manual MCP reconcile: retries failed mounts and clears residual tools
|
|
261
302
|
// without touching any catalog state.
|
|
262
303
|
post(MARKET_ROUTES.mcpRetry, async () => {
|
|
@@ -3,9 +3,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { mountSubagentCatalog } from './subagent-catalog.js';
|
|
6
|
-
import { bindHostLocale } from './host-locale.js';
|
|
7
6
|
import { namedAgentRoles } from './agent-role-names.js';
|
|
8
|
-
|
|
7
|
+
import { expandPluginPaths } from '../catalog/plugin-variables.js';
|
|
8
|
+
export const AGENT_ROLE_TOOL_NAME = 'subagent_role';
|
|
9
9
|
/**
|
|
10
10
|
* Absolute delegation-depth cap applied to every role child. The host default is
|
|
11
11
|
* the same value; keeping it explicit preserves the shipped recursion budget.
|
|
@@ -62,13 +62,17 @@ function parentRouteOf(parent) {
|
|
|
62
62
|
return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Resolve the child LLM options one
|
|
65
|
+
* Resolve the child LLM options for one role call.
|
|
66
66
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* effort
|
|
71
|
-
*
|
|
67
|
+
* The call's own `provider` + `model` pair wins, and the role card supplies the
|
|
68
|
+
* default when the call names neither. Only an exact pair is applied, and only
|
|
69
|
+
* when the live LLM runtime accepts it; a bare id, a Claude alias, a lone
|
|
70
|
+
* `provider` or an unsupported effort degrades to inheriting the parent route
|
|
71
|
+
* and reports a diagnostic. An explicit route with no explicit effort uses the
|
|
72
|
+
* selected model's default, so a card's effort never leaks onto a route it was
|
|
73
|
+
* not written for.
|
|
74
|
+
* @param policy - the card's parsed routing metadata, used as the default route.
|
|
75
|
+
* @param requested - the route the call itself asked for.
|
|
72
76
|
* @param parent - the calling Agent whose route an unusable declaration falls back to.
|
|
73
77
|
* @param llm - live LLM runtime owning provider, model and effort validation.
|
|
74
78
|
* @param signal - tool-call cancellation signal.
|
|
@@ -76,27 +80,34 @@ function parentRouteOf(parent) {
|
|
|
76
80
|
* @param diagnose - diagnostic sink; degradation is reported, never silent.
|
|
77
81
|
* @returns the options to send, or undefined for pure inheritance.
|
|
78
82
|
*/
|
|
79
|
-
export async function resolveAgentOptions(policy, parent, llm, signal, subject, diagnose) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (requested === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
|
|
83
|
+
export async function resolveAgentOptions(policy, requested, parent, llm, signal, subject, diagnose) {
|
|
84
|
+
const cardRoute = policy.provider !== undefined && policy.model !== undefined ? { provider: policy.provider, model: policy.model } : undefined;
|
|
85
|
+
if (cardRoute === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
|
|
83
86
|
const declared = [policy.provider, policy.model].filter(value => value !== undefined).join('/');
|
|
84
87
|
diagnose(`agent "${subject}": route "${declared}" needs an exact provider and model pair; it is ignored and the child inherits the parent route`);
|
|
85
88
|
}
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
89
|
+
const callRoute = requested.provider !== undefined && requested.model !== undefined ? { provider: requested.provider, model: requested.model } : undefined;
|
|
90
|
+
if (callRoute === undefined && (requested.provider !== undefined || requested.model !== undefined)) {
|
|
91
|
+
const declared = [requested.provider, requested.model].filter(value => value !== undefined).join('/');
|
|
92
|
+
diagnose(`agent "${subject}": the call's route "${declared}" needs an exact provider and model pair; it is ignored`);
|
|
93
|
+
}
|
|
94
|
+
// One value, so a half-specified route can never be forwarded downstream.
|
|
95
|
+
const route = callRoute ?? cardRoute;
|
|
96
|
+
const routeEffort = callRoute === undefined ? policy.reasoningEffort : undefined;
|
|
97
|
+
const effort = requested.reasoningEffort ?? routeEffort;
|
|
98
|
+
if (effort === undefined && route === undefined)
|
|
88
99
|
return undefined;
|
|
89
100
|
const inherited = parentRouteOf(parent);
|
|
90
|
-
const provider =
|
|
91
|
-
const model =
|
|
101
|
+
const provider = route?.provider ?? inherited.provider;
|
|
102
|
+
const model = route?.model ?? inherited.model;
|
|
92
103
|
if (provider === undefined || model === undefined) {
|
|
93
104
|
/* v8 ignore next 3 -- an effective route is always known once a parent Agent ran a request. */
|
|
94
105
|
if (effort !== undefined)
|
|
95
106
|
diagnose(`agent "${subject}": reasoning effort "${effort}" has no effective route to validate against; it is ignored`);
|
|
96
107
|
return undefined;
|
|
97
108
|
}
|
|
98
|
-
|
|
99
|
-
const effectiveEffort = effort ?? (
|
|
109
|
+
// An explicit route choice starts from the selected model's default effort.
|
|
110
|
+
const effectiveEffort = effort ?? (route === undefined ? inherited.reasoningEffort : undefined);
|
|
100
111
|
try {
|
|
101
112
|
await llm.resolveCallConfig({ provider, model, ...(effectiveEffort === undefined ? {} : { reasoningEffort: effectiveEffort }) }, signal);
|
|
102
113
|
}
|
|
@@ -108,12 +119,12 @@ export async function resolveAgentOptions(policy, parent, llm, signal, subject,
|
|
|
108
119
|
// Cancellation raised during the adapter lookup must not create a child.
|
|
109
120
|
signal.throwIfAborted();
|
|
110
121
|
return {
|
|
111
|
-
...(
|
|
122
|
+
...(route ?? {}),
|
|
112
123
|
...(effort === undefined ? {} : { reasoningEffort: effort })
|
|
113
124
|
};
|
|
114
125
|
}
|
|
115
126
|
/** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
|
|
116
|
-
export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
127
|
+
export async function agentRoleCatalog(entries, signal, diagnose = () => { }, projectDir) {
|
|
117
128
|
const summaries = [];
|
|
118
129
|
const counts = new Map();
|
|
119
130
|
for (const entry of entries)
|
|
@@ -137,7 +148,7 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
137
148
|
}
|
|
138
149
|
let policy;
|
|
139
150
|
try {
|
|
140
|
-
policy = parseAgentRole(text);
|
|
151
|
+
policy = expandPolicyPaths(parseAgentRole(text), entry, projectDir);
|
|
141
152
|
}
|
|
142
153
|
catch (error) {
|
|
143
154
|
diagnose(`${entry.path}: ${String(error)}`);
|
|
@@ -160,74 +171,210 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
160
171
|
signal.throwIfAborted();
|
|
161
172
|
return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
162
173
|
}
|
|
174
|
+
/** A suite card carries the same path variables as every other surface its author wrote for. */
|
|
175
|
+
function expandPolicyPaths(policy, entry, projectDir) {
|
|
176
|
+
if (entry.suiteRoot === undefined && entry.suiteData === undefined && projectDir === undefined)
|
|
177
|
+
return policy;
|
|
178
|
+
const context = {
|
|
179
|
+
...(entry.suiteRoot === undefined ? {} : { root: entry.suiteRoot }),
|
|
180
|
+
...(entry.suiteData === undefined ? {} : { data: entry.suiteData }),
|
|
181
|
+
...(projectDir === undefined ? {} : { projectDir })
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
...policy,
|
|
185
|
+
content: expandPluginPaths(policy.content, context),
|
|
186
|
+
...(policy.title === undefined ? {} : { title: expandPluginPaths(policy.title, context) }),
|
|
187
|
+
...(policy.description === undefined ? {} : { description: expandPluginPaths(policy.description, context) })
|
|
188
|
+
};
|
|
189
|
+
}
|
|
163
190
|
/** Read the same declaration for catalog summaries and execution, including inline resources. */
|
|
164
|
-
export async function readAgentRole(entry) {
|
|
165
|
-
return parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8')));
|
|
191
|
+
export async function readAgentRole(entry, projectDir) {
|
|
192
|
+
return expandPolicyPaths(parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8'))), entry, projectDir);
|
|
193
|
+
}
|
|
194
|
+
/** The calling session's directory, which is what a card's `${CLAUDE_PROJECT_DIR}` names. */
|
|
195
|
+
function sessionCwd(agent) {
|
|
196
|
+
const cwd = agent?.session?.header?.cwd;
|
|
197
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
|
|
166
198
|
}
|
|
167
199
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
200
|
+
* Run one role child through the requested channel.
|
|
201
|
+
* @param host - the exact live seams this executor owns: tools, llm and subagents.
|
|
202
|
+
* @param mode - which channel the call asked for.
|
|
203
|
+
* @param jobs - the host jobs registry; only the `background` channel needs it.
|
|
171
204
|
*/
|
|
172
|
-
export async function executeAgentRole(host, listRoles, agentName, prompt, parent, signal, diagnose = () => { }) {
|
|
205
|
+
export async function executeAgentRole(host, listRoles, agentName, prompt, parent, requestedRoute, mode, signal, diagnose = () => { }, jobs) {
|
|
173
206
|
if (parent === undefined)
|
|
174
|
-
throw new Error(
|
|
207
|
+
throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a calling agent`);
|
|
175
208
|
if (prompt.trim() === '')
|
|
176
|
-
throw new Error(
|
|
209
|
+
throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a non-empty prompt`);
|
|
177
210
|
signal.throwIfAborted();
|
|
178
211
|
const [entry, ...duplicates] = namedAgentRoles(await listRoles(parent)).filter(candidate => candidate.callName === agentName);
|
|
179
212
|
if (entry === undefined || duplicates.length > 0)
|
|
180
213
|
throw new Error(`agent "${agentName}" is unavailable or ambiguous`);
|
|
181
214
|
if (entry.disabled)
|
|
182
215
|
throw new Error(`agent "${agentName}" is disabled`);
|
|
183
|
-
const policy = await readAgentRole(entry);
|
|
216
|
+
const policy = await readAgentRole(entry, sessionCwd(parent));
|
|
184
217
|
if (policy.disabled)
|
|
185
218
|
throw new Error(`agent "${agentName}" is disabled`);
|
|
186
|
-
const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
|
|
219
|
+
const agentOptions = await resolveAgentOptions(policy, requestedRoute, parent, host.llm, signal, agentName, diagnose);
|
|
187
220
|
signal.throwIfAborted();
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
221
|
+
const request = {
|
|
222
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
223
|
+
parent,
|
|
224
|
+
maxDepth: MAX_AGENT_ROLE_DEPTH,
|
|
225
|
+
persona: policy.content,
|
|
226
|
+
...(agentOptions === undefined ? {} : { agentOptions })
|
|
227
|
+
};
|
|
228
|
+
if (mode === 'continuable') {
|
|
229
|
+
const started = await host.subagents.startContinuable({ provider: 'spawn', label: agentName, request, signal });
|
|
230
|
+
return { kind: 'continuable', subagentId: started.childId };
|
|
231
|
+
}
|
|
232
|
+
if (mode === 'background') {
|
|
233
|
+
if (jobs === undefined) {
|
|
234
|
+
throw new Error(`background role jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs, or call ${AGENT_ROLE_TOOL_NAME} with run_in_background false`);
|
|
235
|
+
}
|
|
236
|
+
// The run's own controller outlives `jobs.start`, which returns at
|
|
237
|
+
// registration: cancellation and disposal both go through the job's hooks.
|
|
238
|
+
return {
|
|
239
|
+
kind: 'background',
|
|
240
|
+
jobId: jobs.start({
|
|
241
|
+
kind: 'subagent',
|
|
242
|
+
label: agentName,
|
|
243
|
+
owner: parent,
|
|
244
|
+
run: () => {
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
return {
|
|
247
|
+
cancel: (reason) => controller.abort(reason ?? 'background role job killed'),
|
|
248
|
+
done: (async () => {
|
|
249
|
+
const run = await host.subagents.start('spawn', { ...request, signal: controller.signal });
|
|
250
|
+
try {
|
|
251
|
+
const result = await run.result;
|
|
252
|
+
return result.stopReason === 'completed' ? { status: 'completed' } : { status: 'failed', detail: result.diagnostic ?? result.stopReason };
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await run.dispose();
|
|
256
|
+
}
|
|
257
|
+
})()
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const run = await host.subagents.start('spawn', { ...request, signal });
|
|
264
|
+
try {
|
|
265
|
+
const result = await run.result;
|
|
266
|
+
if (result.stopReason !== 'completed') {
|
|
267
|
+
const detail = result.diagnostic === undefined ? '' : `: ${result.diagnostic}`;
|
|
268
|
+
throw new Error(`agent "${agentName}" did not complete (${result.stopReason})${detail}`);
|
|
269
|
+
}
|
|
270
|
+
// The registry performs the authoritative lossless snapshot of the child's
|
|
271
|
+
// output blocks on the way out, exactly as the host's own delegation tool does.
|
|
272
|
+
return { kind: 'foreground', runId: run.id, output: result.output };
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await run.dispose();
|
|
276
|
+
}
|
|
201
277
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle.
|
|
280
|
+
*
|
|
281
|
+
* The delegation contract lives here rather than in the injected catalog: a tool
|
|
282
|
+
* description is present in every request, while the catalog message is
|
|
283
|
+
* re-published whenever roles change, so guidance kept out of the catalog is
|
|
284
|
+
* sent once and stays readable through compaction.
|
|
285
|
+
*/
|
|
286
|
+
export function mountAgentRoleTool(ctx, listRoles) {
|
|
204
287
|
const host = ctx;
|
|
205
288
|
const tool = defineTool({
|
|
206
289
|
name: AGENT_ROLE_TOOL_NAME,
|
|
207
|
-
description:
|
|
208
|
-
'
|
|
209
|
-
|
|
290
|
+
description: "Delegate a task to a role child defined by an installed plugin: the named role's own instructions and its configured model apply, and the child starts without this conversation. This tool runs in " +
|
|
291
|
+
'the background by default, immediately returns a durable subagent id, and keeps the child available for later turns; when that run settles, the runtime sends the parent a notice carrying its outcome ' +
|
|
292
|
+
'and final reply. Set `run_in_background: false` only when your next action depends on receiving the result. A child usually runs for minutes, so while it works on the background path, continue with ' +
|
|
293
|
+
'independent work instead of waiting. `prompt` must stand alone, since the child cannot see this conversation and a question it asks while it runs goes unanswered. When the notice arrives, verify its ' +
|
|
294
|
+
"assertions against the files and relay the result to the user; the child's output is not visible to them. The current catalog lists the available roles and states the usage guidance; when no listed " +
|
|
295
|
+
"role matches, use the host's own `subagent` tools instead.",
|
|
210
296
|
parameters: {
|
|
211
297
|
agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
|
|
212
|
-
prompt: {
|
|
298
|
+
prompt: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
required: true,
|
|
301
|
+
description: "The complete, self-contained task for the role child. It does not share this conversation's context, so include everything it needs: what the task is and why, the relevant files and known findings, the output you expect, and the boundaries of the task."
|
|
302
|
+
},
|
|
303
|
+
provider: {
|
|
304
|
+
type: 'string',
|
|
305
|
+
description: "LLM provider route for the child. Supply together with model to override the route the role card declares; omit both to use the card's route or inherit the parent route."
|
|
306
|
+
},
|
|
307
|
+
model: {
|
|
308
|
+
type: 'string',
|
|
309
|
+
description: "Model id interpreted by provider. Supply together with provider to override the route the role card declares; omit both to use the card's route or inherit the parent route."
|
|
310
|
+
},
|
|
311
|
+
reasoning_effort: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: "Adapter-owned reasoning effort for the effective child route. Omit to use the card's effort on the card's route, or the selected model's default after an explicit route change."
|
|
314
|
+
},
|
|
315
|
+
run_in_background: {
|
|
316
|
+
type: 'boolean',
|
|
317
|
+
description: "Which channel runs the child. Omit for the default: a continuable child whose durable subagent id comes back at once, with steering and a settlement notice. Set true for a tracked background job that returns a job id collected with `job_output` and stopped with `job_kill`. Set false for one foreground child whose report comes back as this call's result."
|
|
318
|
+
}
|
|
213
319
|
},
|
|
214
320
|
output: {
|
|
215
321
|
schema: {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
322
|
+
oneOf: [
|
|
323
|
+
{
|
|
324
|
+
type: 'object',
|
|
325
|
+
additionalProperties: false,
|
|
326
|
+
properties: {
|
|
327
|
+
kind: { type: 'string', required: true, const: 'continuable' },
|
|
328
|
+
subagentId: { type: 'string', required: true }
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
type: 'object',
|
|
333
|
+
additionalProperties: false,
|
|
334
|
+
properties: {
|
|
335
|
+
kind: { type: 'string', required: true, const: 'background' },
|
|
336
|
+
jobId: { type: 'string', required: true }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: 'object',
|
|
341
|
+
additionalProperties: false,
|
|
342
|
+
properties: {
|
|
343
|
+
kind: { type: 'string', required: true, const: 'foreground' },
|
|
344
|
+
runId: { type: 'string', required: true },
|
|
345
|
+
output: { type: 'array', required: true, items: { type: 'json' } }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
]
|
|
219
349
|
},
|
|
220
|
-
render: (_args, value) => [
|
|
350
|
+
render: (_args, value) => [
|
|
351
|
+
{
|
|
352
|
+
type: 'text',
|
|
353
|
+
text: value.kind === 'continuable'
|
|
354
|
+
? `started subagent ${value.subagentId}`
|
|
355
|
+
: value.kind === 'background'
|
|
356
|
+
? `started background subagent job ${value.jobId}`
|
|
357
|
+
: `subagent ${value.runId} finished`
|
|
358
|
+
}
|
|
359
|
+
]
|
|
221
360
|
},
|
|
222
361
|
isConcurrencySafe: () => true,
|
|
223
362
|
async execute(args, exec) {
|
|
224
|
-
|
|
363
|
+
// The parameter keeps the host tool's name and boolean shape while this
|
|
364
|
+
// tool owns three channels: omitting it keeps the durable child, `true`
|
|
365
|
+
// runs a tracked job, and `false` returns the report from one foreground run.
|
|
366
|
+
const mode = args.run_in_background === undefined ? 'continuable' : args.run_in_background ? 'background' : 'foreground';
|
|
367
|
+
return executeAgentRole(host, listRoles, args.agent, args.prompt, exec.agent, {
|
|
368
|
+
...(args.provider === undefined ? {} : { provider: args.provider }),
|
|
369
|
+
...(args.model === undefined ? {} : { model: args.model }),
|
|
370
|
+
...(args.reasoning_effort === undefined ? {} : { reasoningEffort: args.reasoning_effort })
|
|
371
|
+
}, mode, exec.signal, message => ctx.logger?.warn(message), ctx.get('jobs'));
|
|
225
372
|
}
|
|
226
373
|
});
|
|
227
374
|
const disposeTool = host.tools.register(tool);
|
|
228
375
|
let disposeCatalog;
|
|
229
376
|
try {
|
|
230
|
-
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message)
|
|
377
|
+
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent)));
|
|
231
378
|
}
|
|
232
379
|
catch (error) {
|
|
233
380
|
disposeTool();
|