dsh-agent-plugins-market 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/client/client.js +1172 -159
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +14 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +185 -30
- package/lib/catalog/archive.js +15 -0
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +2 -0
- package/lib/index.js +6 -2
- package/lib/model/layouts.js +34 -0
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +31 -2
- package/lib/runtime/agent-role-router.js +176 -51
- package/lib/runtime/host-locale.js +6 -12
- package/lib/runtime/mcp-client/connection.js +3 -2
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +178 -34
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/tool-registry-observer.js +8 -4
- package/lib/types/application/catalog.d.ts +10 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +32 -3
- package/lib/types/application/queries.d.ts +6 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +67 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/model/layouts.d.ts +20 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +90 -16
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/tool-registry-observer.d.ts +5 -3
- package/package.json +8 -8
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
|
@@ -3,10 +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';
|
|
9
|
-
export const AGENT_ROLE_TOOL_NAME = '
|
|
8
|
+
export const AGENT_ROLE_TOOL_NAME = 'subagent_role';
|
|
10
9
|
/**
|
|
11
10
|
* Absolute delegation-depth cap applied to every role child. The host default is
|
|
12
11
|
* the same value; keeping it explicit preserves the shipped recursion budget.
|
|
@@ -63,13 +62,17 @@ function parentRouteOf(parent) {
|
|
|
63
62
|
return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
|
|
64
63
|
}
|
|
65
64
|
/**
|
|
66
|
-
* Resolve the child LLM options one
|
|
65
|
+
* Resolve the child LLM options for one role call.
|
|
67
66
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* effort
|
|
72
|
-
*
|
|
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.
|
|
73
76
|
* @param parent - the calling Agent whose route an unusable declaration falls back to.
|
|
74
77
|
* @param llm - live LLM runtime owning provider, model and effort validation.
|
|
75
78
|
* @param signal - tool-call cancellation signal.
|
|
@@ -77,27 +80,34 @@ function parentRouteOf(parent) {
|
|
|
77
80
|
* @param diagnose - diagnostic sink; degradation is reported, never silent.
|
|
78
81
|
* @returns the options to send, or undefined for pure inheritance.
|
|
79
82
|
*/
|
|
80
|
-
export async function resolveAgentOptions(policy, parent, llm, signal, subject, diagnose) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
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)) {
|
|
84
86
|
const declared = [policy.provider, policy.model].filter(value => value !== undefined).join('/');
|
|
85
87
|
diagnose(`agent "${subject}": route "${declared}" needs an exact provider and model pair; it is ignored and the child inherits the parent route`);
|
|
86
88
|
}
|
|
87
|
-
const
|
|
88
|
-
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)
|
|
89
99
|
return undefined;
|
|
90
100
|
const inherited = parentRouteOf(parent);
|
|
91
|
-
const provider =
|
|
92
|
-
const model =
|
|
101
|
+
const provider = route?.provider ?? inherited.provider;
|
|
102
|
+
const model = route?.model ?? inherited.model;
|
|
93
103
|
if (provider === undefined || model === undefined) {
|
|
94
104
|
/* v8 ignore next 3 -- an effective route is always known once a parent Agent ran a request. */
|
|
95
105
|
if (effort !== undefined)
|
|
96
106
|
diagnose(`agent "${subject}": reasoning effort "${effort}" has no effective route to validate against; it is ignored`);
|
|
97
107
|
return undefined;
|
|
98
108
|
}
|
|
99
|
-
|
|
100
|
-
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);
|
|
101
111
|
try {
|
|
102
112
|
await llm.resolveCallConfig({ provider, model, ...(effectiveEffort === undefined ? {} : { reasoningEffort: effectiveEffort }) }, signal);
|
|
103
113
|
}
|
|
@@ -109,7 +119,7 @@ export async function resolveAgentOptions(policy, parent, llm, signal, subject,
|
|
|
109
119
|
// Cancellation raised during the adapter lookup must not create a child.
|
|
110
120
|
signal.throwIfAborted();
|
|
111
121
|
return {
|
|
112
|
-
...(
|
|
122
|
+
...(route ?? {}),
|
|
113
123
|
...(effort === undefined ? {} : { reasoningEffort: effort })
|
|
114
124
|
};
|
|
115
125
|
}
|
|
@@ -187,15 +197,16 @@ function sessionCwd(agent) {
|
|
|
187
197
|
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
|
|
188
198
|
}
|
|
189
199
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
204
|
*/
|
|
194
|
-
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) {
|
|
195
206
|
if (parent === undefined)
|
|
196
|
-
throw new Error(
|
|
207
|
+
throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a calling agent`);
|
|
197
208
|
if (prompt.trim() === '')
|
|
198
|
-
throw new Error(
|
|
209
|
+
throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a non-empty prompt`);
|
|
199
210
|
signal.throwIfAborted();
|
|
200
211
|
const [entry, ...duplicates] = namedAgentRoles(await listRoles(parent)).filter(candidate => candidate.callName === agentName);
|
|
201
212
|
if (entry === undefined || duplicates.length > 0)
|
|
@@ -205,51 +216,165 @@ export async function executeAgentRole(host, listRoles, agentName, prompt, paren
|
|
|
205
216
|
const policy = await readAgentRole(entry, sessionCwd(parent));
|
|
206
217
|
if (policy.disabled)
|
|
207
218
|
throw new Error(`agent "${agentName}" is disabled`);
|
|
208
|
-
const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
|
|
219
|
+
const agentOptions = await resolveAgentOptions(policy, requestedRoute, parent, host.llm, signal, agentName, diagnose);
|
|
209
220
|
signal.throwIfAborted();
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
277
|
}
|
|
224
|
-
/**
|
|
225
|
-
|
|
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) {
|
|
226
287
|
const host = ctx;
|
|
227
288
|
const tool = defineTool({
|
|
228
289
|
name: AGENT_ROLE_TOOL_NAME,
|
|
229
|
-
description:
|
|
230
|
-
'
|
|
231
|
-
|
|
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.",
|
|
232
296
|
parameters: {
|
|
233
297
|
agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
|
|
234
|
-
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
|
+
}
|
|
235
319
|
},
|
|
236
320
|
output: {
|
|
237
321
|
schema: {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
]
|
|
241
349
|
},
|
|
242
|
-
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
|
+
]
|
|
243
360
|
},
|
|
244
361
|
isConcurrencySafe: () => true,
|
|
245
362
|
async execute(args, exec) {
|
|
246
|
-
|
|
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'));
|
|
247
372
|
}
|
|
248
373
|
});
|
|
249
374
|
const disposeTool = host.tools.register(tool);
|
|
250
375
|
let disposeCatalog;
|
|
251
376
|
try {
|
|
252
|
-
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent))
|
|
377
|
+
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent)));
|
|
253
378
|
}
|
|
254
379
|
catch (error) {
|
|
255
380
|
disposeTool();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host-side runtime locale: bilingual copy for host-facing
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Host-side runtime locale: bilingual copy for the host-facing strings a person
|
|
3
|
+
* reads, resolved from the harness's `locale.preference` setting.
|
|
4
|
+
*
|
|
5
|
+
* The model-facing subagent catalog is not here. It is fixed English, because it
|
|
6
|
+
* is a contract read alongside the harness's own English tool descriptions and
|
|
7
|
+
* must not change with the operator's interface language.
|
|
6
8
|
*
|
|
7
9
|
* The web client resolves locale through its own injected service; the host
|
|
8
10
|
* process reads the same setting through the host settings service once the
|
|
@@ -14,19 +16,11 @@ import { readFile } from 'node:fs/promises';
|
|
|
14
16
|
import { join } from 'node:path';
|
|
15
17
|
import { resolveDshHome } from '../catalog/paths.js';
|
|
16
18
|
const zh = {
|
|
17
|
-
subagentCatalogIntro: '以下子代理角色可用于当前会话。目录仅包含角色摘要,不是供当前代理执行的角色指令。',
|
|
18
|
-
subagentCatalogUpdated: '可用子代理角色已变更。以下完整目录替代本会话此前的所有子代理角色目录。',
|
|
19
|
-
subagentCatalogEmpty: '当前没有可通过 subagent_run 调用的角色,不要使用旧目录中的角色 ID。',
|
|
20
|
-
subagentCatalogCall: '任务适合某个角色时,使用 subagent_run(agent=目录中的准确名称, prompt=完整任务与必要上下文)。工具会应用角色的指令与已保存的模型设置,立即返回子代理 ID 并在后台运行;子代理结束时你会收到结果通知,运行期间可用 send_message 追加指令。子代理不继承父会话对话。不要通过 skill 或 Slash Command 加载这些角色,也不要用其它委派工具绕过角色配置。',
|
|
21
19
|
commandAcknowledged: '/{command} 已转交模型执行',
|
|
22
20
|
userCommandSourceLabel: '用户命令',
|
|
23
21
|
feedbackToolCardTitle: '提交市场体验反馈'
|
|
24
22
|
};
|
|
25
23
|
const en = {
|
|
26
|
-
subagentCatalogIntro: 'The following subagent roles are available in this session. These are role summaries, not instructions for the current agent to execute.',
|
|
27
|
-
subagentCatalogUpdated: 'The available subagent roles changed. This complete catalog replaces every earlier subagent role catalog in this session.',
|
|
28
|
-
subagentCatalogEmpty: 'No roles are currently available through subagent_run. Do not use role IDs from earlier catalogs.',
|
|
29
|
-
subagentCatalogCall: "When a task fits a role, call subagent_run with its exact catalog name as agent and a complete task and necessary context as prompt. It applies the role's instructions and saved model settings, returns a durable subagent id immediately, and runs in the background; the runtime sends you a notice carrying the outcome when it settles, and send_message steers the child while it runs. The child does not inherit the parent conversation. Do not load these roles through skill or slash commands, or bypass their configuration with another delegation tool.",
|
|
30
24
|
commandAcknowledged: '/{command} forwarded to the model for execution',
|
|
31
25
|
userCommandSourceLabel: 'user command',
|
|
32
26
|
feedbackToolCardTitle: 'File market feedback'
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
24
24
|
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
25
25
|
import { MAX_TIMER_DELAY_MS } from './host-seams.js';
|
|
26
|
+
import { redactErrorMessage } from '../mcp-redaction.js';
|
|
26
27
|
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
27
28
|
import { createTransport } from './transport.js';
|
|
28
29
|
import { syncTools } from './tools.js';
|
|
@@ -229,7 +230,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
229
230
|
// Fetch-phase failure: the previous generation is still registered
|
|
230
231
|
// and `disposers` still owns it — keep serving the last good list.
|
|
231
232
|
if (!disposed)
|
|
232
|
-
host.logger.error(`${label}: tool re-sync failed: ${String(error)}`);
|
|
233
|
+
host.logger.error(`${label}: tool re-sync failed: ${redactErrorMessage(String(error))}`);
|
|
233
234
|
}
|
|
234
235
|
});
|
|
235
236
|
/**
|
|
@@ -275,7 +276,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
275
276
|
// Disposal clears current ownership before it closes the generation, so
|
|
276
277
|
// only a live supervisor reports an attempt failure.
|
|
277
278
|
if (isCurrent(generation))
|
|
278
|
-
host.logger.warn(`${label}: connection attempt failed: ${String(error)}`);
|
|
279
|
+
host.logger.warn(`${label}: connection attempt failed: ${redactErrorMessage(String(error))}`);
|
|
279
280
|
// A pending browser leg outlives the initialize timeout: the human is
|
|
280
281
|
// mid-approval. Hold this generation open — transport and loopback
|
|
281
282
|
// listener stay alive — until the leg settles; abandoning it would
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
* @module runtime/mcp-client/oauth
|
|
29
29
|
*/
|
|
30
30
|
import { createServer } from 'node:http';
|
|
31
|
+
import { randomBytes } from 'node:crypto';
|
|
32
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
31
33
|
import { credentialKey, scrubbedParentEnv } from './host-seams.js';
|
|
34
|
+
import { redactErrorMessage, redactUrl } from '../mcp-redaction.js';
|
|
32
35
|
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
33
36
|
/** OAuth client identity presented during dynamic registration. */
|
|
34
37
|
const CLIENT_NAME = `DeepSeek Harness (${PLUGIN_NAME})`;
|
|
@@ -65,6 +68,18 @@ function asMcpAuthRecord(record) {
|
|
|
65
68
|
...(payload.tokens === undefined ? {} : { tokens: payload.tokens })
|
|
66
69
|
};
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Merge one locally changed durable field without overwriting a concurrent
|
|
73
|
+
* process's update. If the value read before the mutation no longer matches
|
|
74
|
+
* the store's locked current value, the current value wins conservatively.
|
|
75
|
+
*/
|
|
76
|
+
function mergeDurableField(base, desired, current) {
|
|
77
|
+
if (isDeepStrictEqual(base, desired))
|
|
78
|
+
return current;
|
|
79
|
+
if (isDeepStrictEqual(base, current))
|
|
80
|
+
return desired;
|
|
81
|
+
return current;
|
|
82
|
+
}
|
|
68
83
|
/**
|
|
69
84
|
* Error thrown when the human leg fails or is abandoned: the browser never
|
|
70
85
|
* returned, the callback carried an OAuth error, or no browser could open.
|
|
@@ -89,7 +104,7 @@ export const platformBrowserOpener = async (authorizationUrl, serverName, log) =
|
|
|
89
104
|
detached: platform !== 'win32'
|
|
90
105
|
});
|
|
91
106
|
child.once('error', error => {
|
|
92
|
-
log(`${serverName}: could not open a browser automatically (${String(error)}); open this URL manually: ${authorizationUrl.toString()}`);
|
|
107
|
+
log(`${serverName}: could not open a browser automatically (${redactErrorMessage(String(error))}); open this URL manually: ${redactUrl(authorizationUrl.toString())}`);
|
|
93
108
|
});
|
|
94
109
|
child.once('spawn', () => {
|
|
95
110
|
child.unref();
|
|
@@ -113,6 +128,8 @@ export class LoopbackOAuthClientProvider {
|
|
|
113
128
|
providerState = {};
|
|
114
129
|
/** The transport generation whose `finishAuth` completes the browser leg. */
|
|
115
130
|
activeTransport;
|
|
131
|
+
/** One process-local OAuth state value for the current browser leg. */
|
|
132
|
+
expectedState;
|
|
116
133
|
/** Resolves `true` when the leg saved tokens, `false` when it failed; undefined while no leg is open. */
|
|
117
134
|
browserLegSettled;
|
|
118
135
|
callbackServer;
|
|
@@ -148,6 +165,12 @@ export class LoopbackOAuthClientProvider {
|
|
|
148
165
|
software_version: PLUGIN_VERSION
|
|
149
166
|
};
|
|
150
167
|
}
|
|
168
|
+
/** Issue one unpredictable state value for the SDK's authorization request. */
|
|
169
|
+
state() {
|
|
170
|
+
const state = randomBytes(32).toString('base64url');
|
|
171
|
+
this.expectedState = state;
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
151
174
|
async clientInformation() {
|
|
152
175
|
return (await this.load()).clientInformation;
|
|
153
176
|
}
|
|
@@ -177,6 +200,11 @@ export class LoopbackOAuthClientProvider {
|
|
|
177
200
|
if (transport === undefined) {
|
|
178
201
|
throw new AuthorizationAbortedError(`${this.serverName}: no active transport to complete authorization into`);
|
|
179
202
|
}
|
|
203
|
+
// The SDK normally calls state() before this hook. Keep a defensive path
|
|
204
|
+
// for direct callers too, and overwrite any caller-supplied value with the
|
|
205
|
+
// state issued by this provider.
|
|
206
|
+
const state = this.expectedState ?? this.state();
|
|
207
|
+
authorizationUrl.searchParams.set('state', state);
|
|
180
208
|
this.browserLegSettled = Promise.withResolvers();
|
|
181
209
|
let succeeded = false;
|
|
182
210
|
try {
|
|
@@ -222,6 +250,7 @@ export class LoopbackOAuthClientProvider {
|
|
|
222
250
|
this.callbackServer?.close(() => undefined);
|
|
223
251
|
this.callbackServer = undefined;
|
|
224
252
|
this.boundPort = undefined;
|
|
253
|
+
this.expectedState = undefined;
|
|
225
254
|
}
|
|
226
255
|
saveCodeVerifier(codeVerifier) {
|
|
227
256
|
// The verifier only needs to survive the browser leg; keeping it out of
|
|
@@ -287,17 +316,41 @@ export class LoopbackOAuthClientProvider {
|
|
|
287
316
|
async mutate(mutator) {
|
|
288
317
|
const run = this.writeChain.then(async () => {
|
|
289
318
|
await this.load();
|
|
319
|
+
const base = {
|
|
320
|
+
clientInformation: this.providerState.clientInformation,
|
|
321
|
+
tokens: this.providerState.tokens
|
|
322
|
+
};
|
|
290
323
|
mutator(this.providerState);
|
|
291
324
|
if (this.store === undefined)
|
|
292
325
|
return;
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
const desired = {
|
|
327
|
+
clientInformation: this.providerState.clientInformation,
|
|
328
|
+
tokens: this.providerState.tokens
|
|
329
|
+
};
|
|
330
|
+
const saved = await this.store.modifyRecord(this.recordKey, async (current) => {
|
|
331
|
+
const currentPayload = asMcpAuthRecord(current);
|
|
332
|
+
// Never replace a record owned by another credential provider just
|
|
333
|
+
// because this process raced it; the next read will observe it as
|
|
334
|
+
// absent and the caller can decide what to do.
|
|
335
|
+
if (current !== undefined && currentPayload === undefined)
|
|
336
|
+
return current;
|
|
337
|
+
const payload = currentPayload ?? {};
|
|
338
|
+
const clientInformation = mergeDurableField(base.clientInformation, desired.clientInformation, payload.clientInformation);
|
|
339
|
+
const tokens = mergeDurableField(base.tokens, desired.tokens, payload.tokens);
|
|
340
|
+
const merged = {
|
|
341
|
+
kind: 'grant',
|
|
342
|
+
payload: {
|
|
343
|
+
...(clientInformation === undefined ? {} : { clientInformation }),
|
|
344
|
+
...(tokens === undefined ? {} : { tokens })
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return merged;
|
|
348
|
+
});
|
|
349
|
+
const savedPayload = asMcpAuthRecord(saved);
|
|
350
|
+
this.providerState = {
|
|
351
|
+
...(savedPayload ?? {}),
|
|
352
|
+
...(this.providerState.codeVerifier === undefined ? {} : { codeVerifier: this.providerState.codeVerifier })
|
|
299
353
|
};
|
|
300
|
-
await this.store.modifyRecord(this.recordKey, () => Promise.resolve(record));
|
|
301
354
|
});
|
|
302
355
|
// A failed write must not poison the chain; the caller's rejection is the
|
|
303
356
|
// signal, and the next write re-reads whatever is durable now.
|
|
@@ -317,7 +370,7 @@ export class LoopbackOAuthClientProvider {
|
|
|
317
370
|
});
|
|
318
371
|
server.on('error', error => {
|
|
319
372
|
this.clearCallbackTimer();
|
|
320
|
-
reject(new AuthorizationAbortedError(`${this.serverName}: loopback callback server failed: ${String(error)}`));
|
|
373
|
+
reject(new AuthorizationAbortedError(`${this.serverName}: loopback callback server failed: ${redactErrorMessage(String(error))}`));
|
|
321
374
|
});
|
|
322
375
|
server.listen(this.storage.callbackPort ?? 0, '127.0.0.1', () => {
|
|
323
376
|
const address = server.address();
|
|
@@ -343,19 +396,34 @@ export class LoopbackOAuthClientProvider {
|
|
|
343
396
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
344
397
|
const oauthError = url.searchParams.get('error');
|
|
345
398
|
const code = url.searchParams.get('code');
|
|
346
|
-
const respond = (status, message) => {
|
|
399
|
+
const respond = (status, message, closeServer = true) => {
|
|
347
400
|
// Drain the request, then answer with a plain page the user can close.
|
|
348
401
|
request.resume();
|
|
349
402
|
request.socket.end(`HTTP/1.1 ${status} ${status === 200 ? 'OK' : 'Bad Request'}\r\n` + 'content-type: text/plain; charset=utf-8\r\nconnection: close\r\n\r\n' + `${message}\n`);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
403
|
+
if (closeServer) {
|
|
404
|
+
request.on('close', () => {
|
|
405
|
+
server.close(() => undefined);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
353
408
|
};
|
|
409
|
+
// Keep the listener alive for malformed or forged callbacks so the real
|
|
410
|
+
// browser redirect can still complete the same authorization leg.
|
|
411
|
+
if (url.pathname !== '/callback') {
|
|
412
|
+
respond(404, 'Not an OAuth callback endpoint.', false);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const expectedState = this.expectedState;
|
|
416
|
+
const callbackState = url.searchParams.get('state');
|
|
417
|
+
if (expectedState === undefined || callbackState !== expectedState) {
|
|
418
|
+
respond(400, 'Authorization callback carried an invalid state.', false);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
354
421
|
this.clearCallbackTimer();
|
|
355
422
|
this.callbackServer = undefined;
|
|
356
423
|
this.boundPort = undefined;
|
|
424
|
+
this.expectedState = undefined;
|
|
357
425
|
if (oauthError !== null) {
|
|
358
|
-
const description = url.searchParams.get('error_description') ?? '';
|
|
426
|
+
const description = redactErrorMessage(url.searchParams.get('error_description') ?? '');
|
|
359
427
|
respond(400, `Authorization failed: ${oauthError}${description === '' ? '' : ` — ${description}`}`);
|
|
360
428
|
reject(new AuthorizationAbortedError(`${this.serverName}: the authorization server returned "${oauthError}"${description === '' ? '' : ` (${description})`}`));
|
|
361
429
|
return;
|
|
@@ -21,6 +21,7 @@ import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
21
21
|
import { z } from 'zod';
|
|
22
22
|
import { assertSupportedJsonSchema } from './json-schema-subset.js';
|
|
23
23
|
import { containsImage, extractText, prepareImageProjection } from './projection.js';
|
|
24
|
+
import { redactErrorMessage } from '../mcp-redaction.js';
|
|
24
25
|
/**
|
|
25
26
|
* DeepSeek function-name contract: at most 64 characters. Wire-protocol
|
|
26
27
|
* constant, not configuration.
|
|
@@ -122,7 +123,7 @@ export async function syncTools(client, host, opts, previous) {
|
|
|
122
123
|
// sees either the full generation or none of it — never a partial set.
|
|
123
124
|
for (const dispose of disposers.values())
|
|
124
125
|
dispose();
|
|
125
|
-
host.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${String(error)}`);
|
|
126
|
+
host.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${redactErrorMessage(String(error))}`);
|
|
126
127
|
if (opts.registrationFailure === 'throw')
|
|
127
128
|
throw error;
|
|
128
129
|
return new Map();
|