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.
Files changed (53) hide show
  1. package/README.md +6 -6
  2. package/README.zh.md +6 -6
  3. package/client/client.js +116 -92
  4. package/client/client.js.map +1 -1
  5. package/lib/packages/market-catalog/src/scanning/archive.js +31 -0
  6. package/lib/packages/market-catalog/src/scanning/validate.js +18 -1
  7. package/lib/packages/market-mcp/src/runtime/mcp/bridge/connection.js +42 -23
  8. package/lib/packages/market-mcp/src/runtime/mcp/bridge/oauth.js +29 -2
  9. package/lib/packages/market-mcp/src/runtime/mcp/bridge/transport.js +8 -5
  10. package/lib/packages/market-runtime/src/runtime/agents/agent-role-router.js +2 -1
  11. package/lib/packages/market-runtime/src/runtime/agents/subagent-seam.js +29 -0
  12. package/lib/packages/market-runtime/src/runtime/agents/teammate-role-runtime.js +8 -2
  13. package/lib/packages/market-runtime/src/runtime/host/scoped-contributors.js +4 -0
  14. package/lib/packages/market-runtime/src/runtime/surfaces/project-runtime.js +4 -1
  15. package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/connection.d.ts +4 -1
  16. package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/oauth.d.ts +14 -0
  17. package/lib/types/packages/market-mcp/src/runtime/mcp/bridge/transport.d.ts +5 -1
  18. package/lib/types/packages/market-runtime/src/runtime/agents/agent-role-router.d.ts +2 -10
  19. package/lib/types/packages/market-runtime/src/runtime/agents/subagent-seam.d.ts +63 -0
  20. package/package.json +67 -62
  21. package/schemas/1.0.0/mcp.schema.json +0 -120
  22. package/schemas/1.0.0/plugin.schema.json +0 -65
  23. package/schemas/1.1.0/mcp.schema.json +0 -120
  24. package/schemas/1.1.0/plugin.schema.json +0 -65
  25. package/schemas/README.md +0 -79
  26. package/schemas/agent-plugins/spec.md +0 -62
  27. package/schemas/claude-code/marketplace.schema.json +0 -143
  28. package/schemas/claude-code/plugin.schema.json +0 -105
  29. package/schemas/claude-code/spec.md +0 -108
  30. package/schemas/codex/marketplace.schema.json +0 -78
  31. package/schemas/codex/plugin.schema.json +0 -77
  32. package/schemas/codex/spec.md +0 -91
  33. package/schemas/com.deepseek.harness/namespace.schema.json +0 -42
  34. package/schemas/com.deepseek.harness/spec.md +0 -52
  35. package/schemas/cursor/marketplace.schema.json +0 -82
  36. package/schemas/cursor/plugin.schema.json +0 -58
  37. package/schemas/cursor/spec.md +0 -92
  38. package/schemas/github-copilot/marketplace.schema.json +0 -107
  39. package/schemas/github-copilot/plugin.schema.json +0 -89
  40. package/schemas/github-copilot/spec.md +0 -105
  41. package/schemas/kimi/marketplace.schema.json +0 -38
  42. package/schemas/kimi/plugin.schema.json +0 -84
  43. package/schemas/kimi/spec.md +0 -94
  44. package/schemas/qoder/marketplace.schema.json +0 -115
  45. package/schemas/qoder/plugin.schema.json +0 -127
  46. package/schemas/qoder/spec.md +0 -111
  47. package/schemas/skill-collection/spec.md +0 -37
  48. package/schemas/universal/marketplace.schema.json +0 -98
  49. package/schemas/universal/plugin.schema.json +0 -49
  50. package/schemas/universal/spec.md +0 -53
  51. package/schemas/zcode/marketplace.schema.json +0 -161
  52. package/schemas/zcode/plugin.schema.json +0 -89
  53. 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) && JSON.parse(readFileSync(manifest, 'utf8')).name === 'dsh-agent-plugins-market')
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
- // The leg failed, or the transport closed while it ran: no generation
292
- // is left to finish establishing.
293
- if (hasClosed() || !legSettled) {
294
- generationDown(generation);
295
- return;
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
- connectedAt = Date.now();
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.boundPort = undefined;
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.startContinuable({ provider: 'spawn', label: agentName, request, signal });
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: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
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.drainContinuableChildren(parent, ids)));
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
- const dispose = host.systemPrompt.section({ name: 'agent-plugins:instructions', order: 500, text: () => text });
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>;