dsh-agent-plugins-market 0.9.2 → 0.10.0
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 +6 -6
- package/README.zh.md +6 -6
- package/client/client.js +116 -92
- package/client/client.js.map +1 -1
- package/lib/packages/market-catalog/src/scanning/archive.js +31 -0
- package/lib/packages/market-catalog/src/scanning/validate.js +18 -1
- package/lib/packages/market-mcp/src/runtime/mcp/bridge/connection.js +42 -23
- package/lib/packages/market-mcp/src/runtime/mcp/bridge/oauth.js +29 -2
- package/lib/packages/market-mcp/src/runtime/mcp/bridge/transport.js +8 -5
- package/lib/packages/market-runtime/src/runtime/agents/agent-role-router.js +2 -1
- package/lib/packages/market-runtime/src/runtime/agents/subagent-seam.js +29 -0
- package/lib/packages/market-runtime/src/runtime/agents/teammate-role-runtime.js +8 -2
- package/lib/packages/market-runtime/src/runtime/host/scoped-contributors.js +4 -0
- package/lib/packages/market-runtime/src/runtime/surfaces/project-runtime.js +4 -1
- package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/connection.d.ts +4 -1
- package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/oauth.d.ts +14 -0
- package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/transport.d.ts +5 -1
- package/lib/types/packages/market-runtime/src/runtime/agents/agent-role-router.d.ts +2 -10
- package/lib/types/packages/market-runtime/src/runtime/agents/subagent-seam.d.ts +63 -0
- package/package.json +67 -62
- package/schemas/1.0.0/mcp.schema.json +0 -120
- package/schemas/1.0.0/plugin.schema.json +0 -65
- package/schemas/1.1.0/mcp.schema.json +0 -120
- package/schemas/1.1.0/plugin.schema.json +0 -65
- package/schemas/README.md +0 -79
- package/schemas/agent-plugins/spec.md +0 -62
- package/schemas/claude-code/marketplace.schema.json +0 -143
- package/schemas/claude-code/plugin.schema.json +0 -105
- package/schemas/claude-code/spec.md +0 -108
- package/schemas/codex/marketplace.schema.json +0 -78
- package/schemas/codex/plugin.schema.json +0 -77
- package/schemas/codex/spec.md +0 -91
- package/schemas/com.deepseek.harness/namespace.schema.json +0 -42
- package/schemas/com.deepseek.harness/spec.md +0 -52
- package/schemas/cursor/marketplace.schema.json +0 -82
- package/schemas/cursor/plugin.schema.json +0 -58
- package/schemas/cursor/spec.md +0 -92
- package/schemas/github-copilot/marketplace.schema.json +0 -107
- package/schemas/github-copilot/plugin.schema.json +0 -89
- package/schemas/github-copilot/spec.md +0 -105
- package/schemas/kimi/marketplace.schema.json +0 -38
- package/schemas/kimi/plugin.schema.json +0 -84
- package/schemas/kimi/spec.md +0 -94
- package/schemas/qoder/marketplace.schema.json +0 -115
- package/schemas/qoder/plugin.schema.json +0 -127
- package/schemas/qoder/spec.md +0 -111
- package/schemas/skill-collection/spec.md +0 -37
- package/schemas/universal/marketplace.schema.json +0 -98
- package/schemas/universal/plugin.schema.json +0 -49
- package/schemas/universal/spec.md +0 -53
- package/schemas/zcode/marketplace.schema.json +0 -161
- package/schemas/zcode/plugin.schema.json +0 -89
- package/schemas/zcode/spec.md +0 -102
|
@@ -141,8 +141,39 @@ export async function archiveInstall(url, dest, options = {}) {
|
|
|
141
141
|
* within fflate's synchronous push recursion overflows the stack); the
|
|
142
142
|
* chunked feed halts at the next slice.
|
|
143
143
|
*/
|
|
144
|
+
/**
|
|
145
|
+
* Entry count declared by a zip's end-of-central-directory record.
|
|
146
|
+
*
|
|
147
|
+
* The stream cannot always reach its per-entry guard: fflate parses a
|
|
148
|
+
* whole-archive central directory recursively, and a zip with tens of thousands
|
|
149
|
+
* of tiny members overflows the stack inside one `push()` before `onfile` can
|
|
150
|
+
* count them. Reading the declared count first refuses such an archive before
|
|
151
|
+
* any decompression, and the streaming guard still covers what the record
|
|
152
|
+
* understates.
|
|
153
|
+
* @param bytes - the complete archive.
|
|
154
|
+
* @returns the declared total entry count, or undefined when unreadable or zip64.
|
|
155
|
+
*/
|
|
156
|
+
function zipDeclaredEntryCount(bytes) {
|
|
157
|
+
const EOCD_SIGNATURE = 0x06_05_4b_50;
|
|
158
|
+
const EOCD_MIN_BYTES = 22;
|
|
159
|
+
const MAX_COMMENT_BYTES = 0xffff;
|
|
160
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
161
|
+
const lowest = Math.max(0, bytes.byteLength - EOCD_MIN_BYTES - MAX_COMMENT_BYTES);
|
|
162
|
+
for (let offset = bytes.byteLength - EOCD_MIN_BYTES; offset >= lowest; offset -= 1) {
|
|
163
|
+
if (view.getUint32(offset, true) !== EOCD_SIGNATURE)
|
|
164
|
+
continue;
|
|
165
|
+
const declared = view.getUint16(offset + 10, true);
|
|
166
|
+
// 0xffff defers to zip64, whose real count lives in a different record.
|
|
167
|
+
return declared === 0xffff ? undefined : declared;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
144
171
|
async function extractZip(archiveFile, dest) {
|
|
145
172
|
const compressed = await readFile(archiveFile);
|
|
173
|
+
const declared = zipDeclaredEntryCount(compressed);
|
|
174
|
+
if (declared !== undefined && declared > ARCHIVE_MAX_ENTRIES) {
|
|
175
|
+
throw new Error(`archive exceeds the ${ARCHIVE_MAX_ENTRIES} entry limit`);
|
|
176
|
+
}
|
|
146
177
|
let totalBytes = 0;
|
|
147
178
|
let entryCount = 0;
|
|
148
179
|
let failure;
|
|
@@ -33,8 +33,25 @@ function schemasDirectory() {
|
|
|
33
33
|
let directory = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
while (true) {
|
|
35
35
|
const manifest = join(directory, 'package.json');
|
|
36
|
-
if (existsSync(manifest)
|
|
36
|
+
if (!existsSync(manifest)) {
|
|
37
|
+
const parent = dirname(directory);
|
|
38
|
+
if (parent === directory)
|
|
39
|
+
throw new Error('dsh-agent-plugins-market package root is unavailable');
|
|
40
|
+
directory = parent;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const name = JSON.parse(readFileSync(manifest, 'utf8')).name;
|
|
44
|
+
// The workspace package and the packed artifact expose the library at the
|
|
45
|
+
// same two relative spots: beside the catalog package, or under the
|
|
46
|
+
// packages/ path preserved by the npm files entry.
|
|
47
|
+
if (name === '@dsh-market/catalog')
|
|
37
48
|
return join(directory, 'schemas');
|
|
49
|
+
if (name === 'dsh-agent-plugins-market')
|
|
50
|
+
return join(directory, 'packages/market-catalog/schemas');
|
|
51
|
+
const parentDir = dirname(directory);
|
|
52
|
+
if (parentDir === directory)
|
|
53
|
+
throw new Error('dsh-agent-plugins-market package root is unavailable');
|
|
54
|
+
directory = parentDir;
|
|
38
55
|
const parent = dirname(directory);
|
|
39
56
|
if (parent === directory)
|
|
40
57
|
throw new Error('dsh-agent-plugins-market package root is unavailable');
|
|
@@ -83,9 +83,11 @@ export function resolveReconnectPolicy(config, path) {
|
|
|
83
83
|
* @param policy - Resolved reconnect policy from {@link resolveReconnectPolicy}.
|
|
84
84
|
* @param credentials - The composition's credential store, when mounted; OAuth
|
|
85
85
|
* state persists only when it is.
|
|
86
|
+
* @param opener - Browser opener for the OAuth human leg; the platform browser
|
|
87
|
+
* by default.
|
|
86
88
|
* @returns Handle with a `ready` promise for startup-await and a `dispose` for teardown.
|
|
87
89
|
*/
|
|
88
|
-
export function startConnection(host, config, policy, credentials) {
|
|
90
|
+
export function startConnection(host, config, policy, credentials, opener) {
|
|
89
91
|
const label = `mcp-client(${config.serverName})`;
|
|
90
92
|
const opts = {
|
|
91
93
|
registrationFailure: 'contain',
|
|
@@ -258,9 +260,33 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
258
260
|
clientClosed = undefined;
|
|
259
261
|
host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes; reload the plugin or restart the Host to retry`);
|
|
260
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Retire the generation that raised the `401` and re-establish in place.
|
|
265
|
+
*
|
|
266
|
+
* The SDK aborts that generation's handshake the moment the human leg
|
|
267
|
+
* returns: the initialize that triggered the authorization is gone, so the
|
|
268
|
+
* saved tokens only ever reach the server on a fresh call. Running that
|
|
269
|
+
* call here — instead of scheduling a backoff retry — lets `ready` report
|
|
270
|
+
* the mount's real outcome rather than a failure the caller has to count on
|
|
271
|
+
* a retry to repair after the user already approved.
|
|
272
|
+
*/
|
|
273
|
+
async function reestablishAfterAuthorization() {
|
|
274
|
+
attemptSettled = true;
|
|
275
|
+
client = undefined;
|
|
276
|
+
clientClosed = undefined;
|
|
277
|
+
try {
|
|
278
|
+
await generation.close();
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
/* transport already gone */
|
|
282
|
+
}
|
|
283
|
+
if (disposed)
|
|
284
|
+
return;
|
|
285
|
+
await connectGeneration(false);
|
|
286
|
+
}
|
|
261
287
|
const { transport, oauthProvider } = createTransport(config, credentials, (message) => {
|
|
262
288
|
host.logger.info(message);
|
|
263
|
-
});
|
|
289
|
+
}, opener);
|
|
264
290
|
try {
|
|
265
291
|
await generation.connect(transport, { timeout: opts.startupTimeoutMs });
|
|
266
292
|
if (hasClosed()) {
|
|
@@ -288,32 +314,25 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
288
314
|
attemptSettled = true;
|
|
289
315
|
if (!isCurrent(generation))
|
|
290
316
|
return;
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
// The exchange succeeded inside this generation: finish establishing
|
|
298
|
-
// it (tool sync + connected bookkeeping) instead of respawning.
|
|
299
|
-
try {
|
|
300
|
-
await enqueueSync(generation, startup ? startupOpts : opts);
|
|
301
|
-
}
|
|
302
|
-
catch (syncError) {
|
|
303
|
-
if (isCurrent(generation))
|
|
304
|
-
host.logger.warn(`${label}: tool sync after authorization failed: ${String(syncError)}`);
|
|
305
|
-
await teardownFailedAttempt();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (hasClosed()) {
|
|
317
|
+
// A failed leg leaves nothing to establish. A successful one always
|
|
318
|
+
// needs a fresh generation: the SDK closes this one as soon as the
|
|
319
|
+
// initialize that raised the `401` fails, whether that was the request
|
|
320
|
+
// timeout the human approval outlived or the redirect the leg returns.
|
|
321
|
+
if (!legSettled) {
|
|
309
322
|
generationDown(generation);
|
|
310
323
|
return;
|
|
311
324
|
}
|
|
325
|
+
await reestablishAfterAuthorization();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// A leg that finished inside this attempt leaves no pending promise: the
|
|
329
|
+
// SDK aborts the connect that raised the `401` as soon as the browser leg
|
|
330
|
+
// returns. The tokens are saved either way, so no handshake on this
|
|
331
|
+
// generation can carry them.
|
|
332
|
+
if (oauthProvider?.lastBrowserLegOutcome === true) {
|
|
312
333
|
if (!isCurrent(generation))
|
|
313
334
|
return;
|
|
314
|
-
|
|
315
|
-
if (failedAttempts > 0)
|
|
316
|
-
host.logger.info(`${label}: authorized and re-synced tools (attempt ${failedAttempts}/${policy.maxAttempts})`);
|
|
335
|
+
await reestablishAfterAuthorization();
|
|
317
336
|
return;
|
|
318
337
|
}
|
|
319
338
|
await teardownFailedAttempt();
|
|
@@ -132,6 +132,14 @@ export class LoopbackOAuthClientProvider {
|
|
|
132
132
|
expectedState;
|
|
133
133
|
/** Resolves `true` when the leg saved tokens, `false` when it failed; undefined while no leg is open. */
|
|
134
134
|
browserLegSettled;
|
|
135
|
+
/**
|
|
136
|
+
* Outcome of the most recently finished browser leg, `true` when the code
|
|
137
|
+
* exchange saved tokens. The pending-leg promise disappears when the leg
|
|
138
|
+
* settles, and a leg that finishes inside the `connect` that raised the
|
|
139
|
+
* `401` leaves nothing pending for a supervisor to observe — this field is
|
|
140
|
+
* how the supervisor still learns that the authorization succeeded.
|
|
141
|
+
*/
|
|
142
|
+
lastLegOutcome;
|
|
135
143
|
callbackServer;
|
|
136
144
|
callbackTimer;
|
|
137
145
|
boundPort;
|
|
@@ -149,7 +157,10 @@ export class LoopbackOAuthClientProvider {
|
|
|
149
157
|
get redirectUrl() {
|
|
150
158
|
// RFC 8252 §7.3: loopback redirects may vary the port per request, so the
|
|
151
159
|
// bound port is filled in when the listener starts and registration
|
|
152
|
-
// metadata always reflects the current attempt's URL.
|
|
160
|
+
// metadata always reflects the current attempt's URL. The value must keep
|
|
161
|
+
// reading as the bound port until the code exchange finishes: the token
|
|
162
|
+
// request repeats this exact URL (RFC 6749 §4.1.3), and an authorization
|
|
163
|
+
// server that compares it with the authorization request rejects port `0`.
|
|
153
164
|
return `http://127.0.0.1:${this.boundPort ?? this.storage.callbackPort ?? 0}/callback`;
|
|
154
165
|
}
|
|
155
166
|
get clientMetadata() {
|
|
@@ -214,6 +225,12 @@ export class LoopbackOAuthClientProvider {
|
|
|
214
225
|
return;
|
|
215
226
|
}
|
|
216
227
|
finally {
|
|
228
|
+
// The loopback URL stays published until here. Clearing the port when
|
|
229
|
+
// the callback arrives would hand the token endpoint `redirect_uri =
|
|
230
|
+
// http://127.0.0.1:0/callback` while the authorization request carried
|
|
231
|
+
// the real port, which RFC 6749 §4.1.3 forbids and servers enforce.
|
|
232
|
+
this.boundPort = undefined;
|
|
233
|
+
this.lastLegOutcome = succeeded;
|
|
217
234
|
this.browserLegSettled?.resolve(succeeded);
|
|
218
235
|
this.browserLegSettled = undefined;
|
|
219
236
|
}
|
|
@@ -232,6 +249,14 @@ export class LoopbackOAuthClientProvider {
|
|
|
232
249
|
get browserLeg() {
|
|
233
250
|
return this.browserLegSettled?.promise;
|
|
234
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The most recent finished leg's outcome; `undefined` until one finishes.
|
|
254
|
+
* The supervisor reads it after a failed attempt: a leg that settled inside
|
|
255
|
+
* the attempt itself leaves no pending {@link browserLeg} behind.
|
|
256
|
+
*/
|
|
257
|
+
get lastBrowserLegOutcome() {
|
|
258
|
+
return this.lastLegOutcome;
|
|
259
|
+
}
|
|
235
260
|
/**
|
|
236
261
|
* Register the transport generation whose `finishAuth` completes the flow.
|
|
237
262
|
* The transport calls `redirectToAuthorization` during `connect`; binding
|
|
@@ -420,7 +445,9 @@ export class LoopbackOAuthClientProvider {
|
|
|
420
445
|
}
|
|
421
446
|
this.clearCallbackTimer();
|
|
422
447
|
this.callbackServer = undefined;
|
|
423
|
-
this
|
|
448
|
+
// The bound port deliberately survives this callback: the code exchange
|
|
449
|
+
// runs a moment later and must present the same `redirect_uri` the
|
|
450
|
+
// authorization request used. An aborted leg clears it in resetCallback.
|
|
424
451
|
this.expectedState = undefined;
|
|
425
452
|
if (oauthError !== null) {
|
|
426
453
|
const description = redactErrorMessage(url.searchParams.get('error_description') ?? '');
|
|
@@ -33,11 +33,11 @@ function hasStaticAuthorization(headers) {
|
|
|
33
33
|
* when the config opts out or the headers already carry a static
|
|
34
34
|
* Authorization.
|
|
35
35
|
*/
|
|
36
|
-
function buildOAuthProvider(config, credentials, log) {
|
|
36
|
+
function buildOAuthProvider(config, credentials, log, opener) {
|
|
37
37
|
const auth = config.auth;
|
|
38
38
|
if (auth?.enabled === false || hasStaticAuthorization(config.headers))
|
|
39
39
|
return undefined;
|
|
40
|
-
return new LoopbackOAuthClientProvider(config.serverName, auth?.storage ?? {}, auth?.scope, log, credentials);
|
|
40
|
+
return new LoopbackOAuthClientProvider(config.serverName, auth?.storage ?? {}, auth?.scope, log, credentials, opener);
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Create an MCP transport from the resolved bridge config.
|
|
@@ -54,11 +54,14 @@ function buildOAuthProvider(config, credentials, log) {
|
|
|
54
54
|
* @param credentials - The composition's credential store, when mounted; OAuth
|
|
55
55
|
* state persists only when it is.
|
|
56
56
|
* @param log - Diagnostic sink for the human leg of the flow.
|
|
57
|
+
* @param opener - Browser opener for the OAuth human leg; the platform browser
|
|
58
|
+
* by default. A caller that owns the human leg (a host surface, or a test
|
|
59
|
+
* driving the authorization endpoint directly) passes its own.
|
|
57
60
|
* @returns The transport plus its OAuth provider, when active — the
|
|
58
61
|
* supervisor queries the provider to hold the generation open while a
|
|
59
62
|
* browser leg is pending.
|
|
60
63
|
*/
|
|
61
|
-
export function createTransport(config, credentials, log = () => { }) {
|
|
64
|
+
export function createTransport(config, credentials, log = () => { }, opener) {
|
|
62
65
|
switch (config.transport) {
|
|
63
66
|
case 'stdio':
|
|
64
67
|
return {
|
|
@@ -71,7 +74,7 @@ export function createTransport(config, credentials, log = () => { }) {
|
|
|
71
74
|
oauthProvider: undefined
|
|
72
75
|
};
|
|
73
76
|
case 'streamable-http': {
|
|
74
|
-
const oauthProvider = buildOAuthProvider(config, credentials, log);
|
|
77
|
+
const oauthProvider = buildOAuthProvider(config, credentials, log, opener);
|
|
75
78
|
// The MCP SDK's StreamableHTTPClientTransport has optional callback
|
|
76
79
|
// properties typed without `| undefined` (exactOptionalPropertyTypes
|
|
77
80
|
// mismatch with the Transport interface); the SDK constructed the
|
|
@@ -86,7 +89,7 @@ export function createTransport(config, credentials, log = () => { }) {
|
|
|
86
89
|
return { transport, oauthProvider };
|
|
87
90
|
}
|
|
88
91
|
case 'sse': {
|
|
89
|
-
const oauthProvider = buildOAuthProvider(config, credentials, log);
|
|
92
|
+
const oauthProvider = buildOAuthProvider(config, credentials, log, opener);
|
|
90
93
|
// The legacy SSE transport negotiates a separate endpoint POST channel.
|
|
91
94
|
// The SDK applies `requestInit.headers` to both the stream request and
|
|
92
95
|
// every endpoint POST (its `_commonHeaders` merge), so no separate
|
|
@@ -6,6 +6,7 @@ import { isAbsentPath } from '../../application/fs-probe.js';
|
|
|
6
6
|
import { namedAgentRoles } from './agent-role-names.js';
|
|
7
7
|
import { expandPluginPaths } from '../../../../market-catalog/src/index.js';
|
|
8
8
|
import { parseAgentRole } from '../../application/agent-roles.js';
|
|
9
|
+
import { startContinuableChild } from './subagent-seam.js';
|
|
9
10
|
export { parseAgentRole };
|
|
10
11
|
export const AGENT_ROLE_TOOL_NAME = 'subagent_role';
|
|
11
12
|
/**
|
|
@@ -217,7 +218,7 @@ export async function executeAgentRole(host, listRoles, agentName, prompt, paren
|
|
|
217
218
|
...(agentOptions === undefined ? {} : { agentOptions })
|
|
218
219
|
};
|
|
219
220
|
if (mode === 'continuable') {
|
|
220
|
-
const started = await host.subagents
|
|
221
|
+
const started = await startContinuableChild(host.subagents, { provider: 'spawn', label: agentName, request, signal });
|
|
221
222
|
return { kind: 'continuable', subagentId: started.childId };
|
|
222
223
|
}
|
|
223
224
|
const run = await host.subagents.start('spawn', { ...request, signal });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start one continuable child on whichever start member the host exposes.
|
|
3
|
+
* @param seam - the live `ctx.subagents` service.
|
|
4
|
+
* @param input - provider, label, delegation request, and cancellation.
|
|
5
|
+
* @returns the child identity the host reserved.
|
|
6
|
+
* @throws when the host exposes neither start member.
|
|
7
|
+
*/
|
|
8
|
+
export function startContinuableChild(seam, input) {
|
|
9
|
+
if (typeof seam.startContinuable === 'function')
|
|
10
|
+
return seam.startContinuable(input);
|
|
11
|
+
if (typeof seam.startActivation === 'function')
|
|
12
|
+
return seam.startActivation({ ...input, delivery: 'parent' });
|
|
13
|
+
return Promise.reject(new Error('the host subagent service exposes neither startContinuable nor startActivation'));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Release selected resident continuable children on whichever drain member the
|
|
17
|
+
* host exposes.
|
|
18
|
+
* @param seam - the live `ctx.subagents` service.
|
|
19
|
+
* @param parent - exact live direct parent authorizing the release.
|
|
20
|
+
* @param childIds - durable direct-child ids to release when resident.
|
|
21
|
+
* @throws when the host exposes neither drain member.
|
|
22
|
+
*/
|
|
23
|
+
export function drainContinuableChildIds(seam, parent, childIds) {
|
|
24
|
+
if (typeof seam.drainContinuableChildren === 'function')
|
|
25
|
+
return seam.drainContinuableChildren(parent, childIds);
|
|
26
|
+
if (typeof seam.drainChildren === 'function')
|
|
27
|
+
return seam.drainChildren(parent, childIds);
|
|
28
|
+
return Promise.reject(new Error('the host subagent service exposes neither drainContinuableChildren nor drainChildren'));
|
|
29
|
+
}
|
|
@@ -4,6 +4,7 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
4
4
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { drainContinuableChildIds } from './subagent-seam.js';
|
|
7
8
|
const PROVIDER = 'market-role-team';
|
|
8
9
|
export const TEAM_ROLE_SOURCE = 'teammate-role-binding';
|
|
9
10
|
const routeSchema = z.object({ provider: z.string().min(1), model: z.string().min(1), reasoningEffort: z.string().min(1).optional() }).strict();
|
|
@@ -79,10 +80,15 @@ export class TeammateRoleRuntime {
|
|
|
79
80
|
this.ctx = ctx;
|
|
80
81
|
this.teams = ctx.get('agentTeams');
|
|
81
82
|
this.subagents = ctx.get('subagents');
|
|
83
|
+
// The 0.2.1-alpha.4 host removed `toolFilter` and `persona` from the capability
|
|
84
|
+
// record and added `agentPreset`; the 0.2.0-rc.2 baseline types require the first
|
|
85
|
+
// two. One variable satisfies both shapes because the host reads only the flags
|
|
86
|
+
// it knows, while an object literal would fail the newer excess-property check.
|
|
87
|
+
const capabilities = { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false };
|
|
82
88
|
this.registrations.push(this.subagents.registerProvider({
|
|
83
89
|
name: PROVIDER,
|
|
84
90
|
inheritsParentContext: false,
|
|
85
|
-
capabilities
|
|
91
|
+
capabilities,
|
|
86
92
|
start: () => Promise.reject(new Error('role teammates require Team continuation')),
|
|
87
93
|
prepareContinuable: request => {
|
|
88
94
|
const pending = this.pending.getStore();
|
|
@@ -227,7 +233,7 @@ export class TeammateRoleRuntime {
|
|
|
227
233
|
byParent.set(parent, [...(byParent.get(parent) ?? []), agent.id]);
|
|
228
234
|
}
|
|
229
235
|
try {
|
|
230
|
-
await Promise.all([...byParent].map(([parent, ids]) => this.subagents
|
|
236
|
+
await Promise.all([...byParent].map(([parent, ids]) => drainContinuableChildIds(this.subagents, parent, ids)));
|
|
231
237
|
}
|
|
232
238
|
finally {
|
|
233
239
|
for (const off of this.registrations.splice(0).reverse())
|
|
@@ -98,6 +98,10 @@ export class ScopedExtensionContributors {
|
|
|
98
98
|
scope.systemPrompt.section({
|
|
99
99
|
name: 'agent-plugins:instructions',
|
|
100
100
|
order: 500,
|
|
101
|
+
// Suite-authored Markdown is literal text, never a template: the host
|
|
102
|
+
// interpolates by default and refuses an unknown `{{name}}`, so a suite
|
|
103
|
+
// that quotes `{{cwd}}` would fail prompt assembly before any request.
|
|
104
|
+
interpolate: false,
|
|
101
105
|
text: () => mount.active
|
|
102
106
|
? mount.instructions
|
|
103
107
|
.filter(row => this.ports.allows(agent, row.parent) && this.ports.allows(agent, row.id))
|
|
@@ -28,7 +28,10 @@ suiteAllowed) {
|
|
|
28
28
|
return mountProjectSurface(ctx, catalog, 'systemPrompt', (scope, agent) => {
|
|
29
29
|
let text = '';
|
|
30
30
|
const host = scope;
|
|
31
|
-
|
|
31
|
+
// Suite-authored Markdown is literal text, never a template: the host
|
|
32
|
+
// interpolates by default and refuses an unknown `{{name}}`, so a suite that
|
|
33
|
+
// quotes `{{cwd}}` would fail prompt assembly before any model request.
|
|
34
|
+
const dispose = host.systemPrompt.section({ name: 'agent-plugins:instructions', order: 500, interpolate: false, text: () => text });
|
|
32
35
|
return {
|
|
33
36
|
async reconcile(projectSuites) {
|
|
34
37
|
const projectDir = agent.session.header.cwd;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @module runtime/mcp-client/connection
|
|
22
22
|
*/
|
|
23
|
+
import type { BrowserOpener } from './oauth.js';
|
|
23
24
|
import type { ToolHost } from './tools.js';
|
|
24
25
|
import type { Config } from '../../../application/mcp/mcp-bridge-config.js';
|
|
25
26
|
/** Automatic reconnect policy for one MCP server connection. */
|
|
@@ -69,6 +70,8 @@ export interface ConnectionHandle {
|
|
|
69
70
|
* @param policy - Resolved reconnect policy from {@link resolveReconnectPolicy}.
|
|
70
71
|
* @param credentials - The composition's credential store, when mounted; OAuth
|
|
71
72
|
* state persists only when it is.
|
|
73
|
+
* @param opener - Browser opener for the OAuth human leg; the platform browser
|
|
74
|
+
* by default.
|
|
72
75
|
* @returns Handle with a `ready` promise for startup-await and a `dispose` for teardown.
|
|
73
76
|
*/
|
|
74
|
-
export declare function startConnection(host: ToolHost, config: Config, policy: ResolvedReconnectPolicy, credentials?: unknown): ConnectionHandle;
|
|
77
|
+
export declare function startConnection(host: ToolHost, config: Config, policy: ResolvedReconnectPolicy, credentials?: unknown, opener?: BrowserOpener): ConnectionHandle;
|
|
@@ -67,6 +67,14 @@ export declare class LoopbackOAuthClientProvider implements OAuthClientProvider
|
|
|
67
67
|
private expectedState;
|
|
68
68
|
/** Resolves `true` when the leg saved tokens, `false` when it failed; undefined while no leg is open. */
|
|
69
69
|
private browserLegSettled;
|
|
70
|
+
/**
|
|
71
|
+
* Outcome of the most recently finished browser leg, `true` when the code
|
|
72
|
+
* exchange saved tokens. The pending-leg promise disappears when the leg
|
|
73
|
+
* settles, and a leg that finishes inside the `connect` that raised the
|
|
74
|
+
* `401` leaves nothing pending for a supervisor to observe — this field is
|
|
75
|
+
* how the supervisor still learns that the authorization succeeded.
|
|
76
|
+
*/
|
|
77
|
+
private lastLegOutcome;
|
|
70
78
|
private callbackServer;
|
|
71
79
|
private callbackTimer;
|
|
72
80
|
private boundPort;
|
|
@@ -98,6 +106,12 @@ export declare class LoopbackOAuthClientProvider implements OAuthClientProvider
|
|
|
98
106
|
* settles.
|
|
99
107
|
*/
|
|
100
108
|
get browserLeg(): Promise<boolean> | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* The most recent finished leg's outcome; `undefined` until one finishes.
|
|
111
|
+
* The supervisor reads it after a failed attempt: a leg that settled inside
|
|
112
|
+
* the attempt itself leaves no pending {@link browserLeg} behind.
|
|
113
|
+
*/
|
|
114
|
+
get lastBrowserLegOutcome(): boolean | undefined;
|
|
101
115
|
/**
|
|
102
116
|
* Register the transport generation whose `finishAuth` completes the flow.
|
|
103
117
|
* The transport calls `redirectToAuthorization` during `connect`; binding
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
12
12
|
import { LoopbackOAuthClientProvider } from './oauth.js';
|
|
13
|
+
import type { BrowserOpener } from './oauth.js';
|
|
13
14
|
import type { Config } from '../../../application/mcp/mcp-bridge-config.js';
|
|
14
15
|
/**
|
|
15
16
|
* Create an MCP transport from the resolved bridge config.
|
|
@@ -26,11 +27,14 @@ import type { Config } from '../../../application/mcp/mcp-bridge-config.js';
|
|
|
26
27
|
* @param credentials - The composition's credential store, when mounted; OAuth
|
|
27
28
|
* state persists only when it is.
|
|
28
29
|
* @param log - Diagnostic sink for the human leg of the flow.
|
|
30
|
+
* @param opener - Browser opener for the OAuth human leg; the platform browser
|
|
31
|
+
* by default. A caller that owns the human leg (a host surface, or a test
|
|
32
|
+
* driving the authorization endpoint directly) passes its own.
|
|
29
33
|
* @returns The transport plus its OAuth provider, when active — the
|
|
30
34
|
* supervisor queries the provider to hold the generation open while a
|
|
31
35
|
* browser leg is pending.
|
|
32
36
|
*/
|
|
33
|
-
export declare function createTransport(config: Config, credentials?: unknown, log?: (message: string) => void): {
|
|
37
|
+
export declare function createTransport(config: Config, credentials?: unknown, log?: (message: string) => void, opener?: BrowserOpener): {
|
|
34
38
|
transport: Transport;
|
|
35
39
|
oauthProvider: LoopbackOAuthClientProvider | undefined;
|
|
36
40
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { type SubagentCatalogEntry } from './subagent-catalog.js';
|
|
3
3
|
import { parseAgentRole, type AgentRoleEntry, type AgentRolePolicy } from '../../application/agent-roles.js';
|
|
4
|
+
import { type ContinuableSubagentSeam } from './subagent-seam.js';
|
|
4
5
|
export { parseAgentRole };
|
|
5
6
|
export type { AgentRoleEntry, AgentRolePolicy };
|
|
6
7
|
/** A declaration that has been through the call-name pass, so it answers exactly one callable name. */
|
|
@@ -40,16 +41,7 @@ export interface AgentRoleHost {
|
|
|
40
41
|
reasoningEffort?: string;
|
|
41
42
|
}, signal?: AbortSignal): Promise<unknown>;
|
|
42
43
|
};
|
|
43
|
-
subagents: {
|
|
44
|
-
startContinuable(spec: {
|
|
45
|
-
provider: string;
|
|
46
|
-
label: string;
|
|
47
|
-
request: AgentRoleDelegation;
|
|
48
|
-
signal: AbortSignal;
|
|
49
|
-
}): Promise<{
|
|
50
|
-
childId: string;
|
|
51
|
-
messageId: string;
|
|
52
|
-
}>;
|
|
44
|
+
subagents: ContinuableSubagentSeam & {
|
|
53
45
|
/** One-shot foreground run: the host settles it and hands back the child's output. */
|
|
54
46
|
start(provider: string, request: AgentRoleRunRequest): Promise<AgentRoleRun>;
|
|
55
47
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-release access to the host subagent seam.
|
|
3
|
+
*
|
|
4
|
+
* The host renamed its continuable-child operations in 0.2.1-alpha.4:
|
|
5
|
+
* `startContinuable` became `startActivation`, and `drainContinuableChildren`
|
|
6
|
+
* became `drainChildren`. The plugin pins the 0.2.0-rc.2 baseline, so the
|
|
7
|
+
* published types name the old members; this module resolves whichever member
|
|
8
|
+
* the live service exposes and keeps one source tree working on both releases.
|
|
9
|
+
*
|
|
10
|
+
* Every member is optional and every call is guarded, per the plugin standard's
|
|
11
|
+
* rule for a host capability the declared types do not carry. A host exposing
|
|
12
|
+
* neither name is unsupported, and the refusal names both members rather than
|
|
13
|
+
* failing with a property access on undefined.
|
|
14
|
+
*
|
|
15
|
+
* @module runtime/agents/subagent-seam
|
|
16
|
+
*/
|
|
17
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
18
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
19
|
+
/** The child identity both releases return once the child accepts its first prompt. */
|
|
20
|
+
export interface ContinuableChildStart {
|
|
21
|
+
/** The host reserves a branded session id; the plugin only forwards the value. */
|
|
22
|
+
readonly childId: string;
|
|
23
|
+
readonly messageId?: string;
|
|
24
|
+
}
|
|
25
|
+
/** One continuable start as this plugin issues it, in the older release's spelling. */
|
|
26
|
+
export interface ContinuableStartInput {
|
|
27
|
+
readonly provider: string;
|
|
28
|
+
readonly label: string;
|
|
29
|
+
readonly request: unknown;
|
|
30
|
+
readonly signal: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The host subagent members this plugin drives, across both release lines.
|
|
34
|
+
*
|
|
35
|
+
* The 0.2.1-alpha.4 activation additionally requires `delivery`; the older
|
|
36
|
+
* continuable start always notified the parent, so the adapter supplies that
|
|
37
|
+
* value explicitly.
|
|
38
|
+
*/
|
|
39
|
+
export interface ContinuableSubagentSeam {
|
|
40
|
+
startContinuable?(spec: ContinuableStartInput): Promise<ContinuableChildStart>;
|
|
41
|
+
startActivation?(spec: ContinuableStartInput & {
|
|
42
|
+
readonly delivery?: 'parent' | 'caller';
|
|
43
|
+
}): Promise<ContinuableChildStart>;
|
|
44
|
+
drainContinuableChildren?(parent: Agent, childIds: readonly SessionId[]): Promise<void>;
|
|
45
|
+
drainChildren?(parent: Agent, childIds: readonly SessionId[]): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Start one continuable child on whichever start member the host exposes.
|
|
49
|
+
* @param seam - the live `ctx.subagents` service.
|
|
50
|
+
* @param input - provider, label, delegation request, and cancellation.
|
|
51
|
+
* @returns the child identity the host reserved.
|
|
52
|
+
* @throws when the host exposes neither start member.
|
|
53
|
+
*/
|
|
54
|
+
export declare function startContinuableChild(seam: ContinuableSubagentSeam, input: ContinuableStartInput): Promise<ContinuableChildStart>;
|
|
55
|
+
/**
|
|
56
|
+
* Release selected resident continuable children on whichever drain member the
|
|
57
|
+
* host exposes.
|
|
58
|
+
* @param seam - the live `ctx.subagents` service.
|
|
59
|
+
* @param parent - exact live direct parent authorizing the release.
|
|
60
|
+
* @param childIds - durable direct-child ids to release when resident.
|
|
61
|
+
* @throws when the host exposes neither drain member.
|
|
62
|
+
*/
|
|
63
|
+
export declare function drainContinuableChildIds(seam: ContinuableSubagentSeam, parent: Agent, childIds: readonly SessionId[]): Promise<void>;
|