dsh-agent-plugins-market 0.6.2 → 0.7.1
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 +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +28533 -1511
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +188 -0
- package/lib/application/catalog.js +229 -1109
- package/lib/application/details.js +19 -8
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +263 -0
- package/lib/application/panel-resources.js +43 -14
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +8 -2
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +35 -30
- package/lib/catalog/component-files.js +14 -6
- package/lib/catalog/fs-probes.js +43 -0
- package/lib/catalog/manifests.js +53 -92
- package/lib/catalog/native-project.js +2 -2
- package/lib/catalog/paths.js +74 -55
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +23 -2
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +61 -10
- package/lib/catalog/source-catalog.js +10 -4
- package/lib/catalog/suite-components.js +7 -7
- package/lib/catalog/suite-scanner.js +3 -1
- package/lib/catalog/surfaces.js +5 -26
- package/lib/catalog/validate.js +7 -11
- package/lib/contracts/market.js +2 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +115 -217
- package/lib/model/layouts.js +10 -1
- package/lib/model/types.js +26 -3
- package/lib/routes.js +57 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +142 -106
- package/lib/runtime/commands-mounts.js +55 -11
- package/lib/runtime/deadline.js +54 -0
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +101 -46
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +49 -24
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +182 -70
- package/lib/runtime/lsp-server-state.js +4 -4
- package/lib/runtime/lsp-status.js +1 -1
- package/lib/runtime/mcp-auth-record.js +42 -0
- package/lib/runtime/mcp-backend.js +14 -14
- package/lib/runtime/mcp-client/config.js +8 -1
- package/lib/runtime/mcp-client/connection.js +38 -43
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +5 -7
- package/lib/runtime/mcp-client/plugin-identity.js +28 -0
- package/lib/runtime/mcp-client/projection.js +202 -0
- package/lib/runtime/mcp-client/tools.js +4 -189
- package/lib/runtime/mcp-config.js +36 -14
- package/lib/runtime/mcp-direct-config.js +33 -16
- package/lib/runtime/mcp-mounts.js +35 -75
- package/lib/runtime/mcp-overrides.js +5 -5
- package/lib/runtime/mcp-redaction.js +3 -2
- package/lib/runtime/mcp-status.js +7 -8
- package/lib/runtime/mount-lifecycle.js +73 -0
- package/lib/runtime/profile-seam.js +321 -0
- package/lib/runtime/project-runtime.js +34 -17
- package/lib/runtime/reconcile-scheduler.js +97 -0
- package/lib/runtime/reconciler.js +6 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +144 -0
- package/lib/runtime/skills-provider.js +38 -14
- package/lib/runtime/source-auto-update.js +66 -0
- package/lib/{model/state.js → runtime/state-store.js} +9 -11
- package/lib/runtime/storage-migration.js +35 -18
- package/lib/runtime/subagent-catalog.js +5 -4
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +20 -20
- package/lib/runtime/user-commands.js +7 -3
- package/lib/runtime/user-panels.js +125 -46
- package/lib/runtime/user-store.js +124 -69
- package/lib/types/application/catalog-context.d.ts +124 -0
- package/lib/types/application/catalog.d.ts +108 -304
- package/lib/types/application/install-store.d.ts +21 -0
- package/lib/types/application/lsp-service.d.ts +36 -0
- package/lib/types/application/mcp-service.d.ts +77 -0
- package/lib/types/application/panel-resources.d.ts +8 -0
- package/lib/types/application/ports.d.ts +72 -0
- package/lib/types/application/queries.d.ts +15 -26
- package/lib/types/application/snapshot-cache.d.ts +51 -0
- package/lib/types/application/source-store.d.ts +100 -0
- package/lib/types/catalog/archive.d.ts +3 -11
- package/lib/types/catalog/component-files.d.ts +4 -0
- package/lib/types/catalog/fs-probes.d.ts +10 -0
- package/lib/types/catalog/manifests.d.ts +17 -16
- package/lib/types/catalog/native-project.d.ts +2 -2
- package/lib/types/catalog/paths.d.ts +53 -15
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/scan-pipeline.d.ts +3 -3
- package/lib/types/catalog/scan-resolvers.d.ts +3 -9
- package/lib/types/catalog/skills-parse.d.ts +13 -7
- package/lib/types/catalog/source-catalog.d.ts +10 -5
- package/lib/types/catalog/suite-scanner.d.ts +8 -6
- package/lib/types/catalog/validate.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +35 -0
- package/lib/types/contracts/market.d.ts +13 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +10 -0
- package/lib/types/model/types.d.ts +29 -5
- package/lib/types/runtime/agent-role-names.d.ts +6 -0
- package/lib/types/runtime/agent-role-router.d.ts +59 -52
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +17 -0
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/feedback-tool.d.ts +33 -11
- package/lib/types/runtime/hooks-mounts.d.ts +1 -1
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +69 -19
- package/lib/types/runtime/lsp-status.d.ts +2 -1
- package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
- package/lib/types/runtime/mcp-backend.d.ts +10 -14
- package/lib/types/runtime/mcp-client/config.d.ts +8 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
- package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
- package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
- package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
- package/lib/types/runtime/mcp-config.d.ts +25 -4
- package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
- package/lib/types/runtime/mcp-mounts.d.ts +10 -15
- package/lib/types/runtime/mcp-overrides.d.ts +1 -1
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
- package/lib/types/runtime/profile-seam.d.ts +64 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +32 -0
- package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
- package/lib/types/runtime/storage-migration.d.ts +5 -3
- package/lib/types/runtime/subagent-catalog.d.ts +2 -0
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
- package/lib/types/runtime/user-panels.d.ts +69 -19
- package/lib/types/runtime/user-store.d.ts +67 -20
- package/package.json +68 -24
- package/schemas/README.md +1 -1
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module runtime/mcp-client/config
|
|
10
10
|
*/
|
|
11
|
-
export { RECONNECT_DEFAULTS } from './connection.js';
|
|
12
11
|
/** Default timeout for individual MCP tool calls (ms). */
|
|
13
12
|
export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* How long one connection attempt may spend initializing (ms). A server that
|
|
15
|
+
* never answers would otherwise hold its process, transport and browser leg
|
|
16
|
+
* until the SDK's own 60-second request default expired; a working server
|
|
17
|
+
* handshakes well under a second, and a genuinely slow one can declare its own
|
|
18
|
+
* `startupTimeoutMs`.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
|
|
14
21
|
/** Valid `serverName`, kept below the public tool-name budget. */
|
|
15
22
|
export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
|
|
16
23
|
/**
|
|
@@ -23,8 +23,10 @@
|
|
|
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 { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
26
27
|
import { createTransport } from './transport.js';
|
|
27
28
|
import { syncTools } from './tools.js';
|
|
29
|
+
import { DEFAULT_STARTUP_TIMEOUT_MS } from './config.js';
|
|
28
30
|
/** Defaults shared by the config and {@link resolveReconnectPolicy}. */
|
|
29
31
|
export const RECONNECT_DEFAULTS = Object.freeze({
|
|
30
32
|
enabled: true,
|
|
@@ -88,9 +90,11 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
88
90
|
registrationFailure: 'contain',
|
|
89
91
|
serverName: config.serverName,
|
|
90
92
|
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
93
|
+
// Every attempt is bounded: an unreachable server must not hold its
|
|
94
|
+
// process, transport and browser leg while nothing can arrive.
|
|
95
|
+
startupTimeoutMs: config.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
|
|
91
96
|
...(config.enabledTools === undefined ? {} : { enabledTools: config.enabledTools }),
|
|
92
|
-
...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools })
|
|
93
|
-
...(config.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: config.startupTimeoutMs })
|
|
97
|
+
...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools })
|
|
94
98
|
};
|
|
95
99
|
// The initial sync uses 'throw' when failOnStartupError is configured, so
|
|
96
100
|
// a registration conflict propagates to the startup-await path. Re-syncs
|
|
@@ -197,7 +201,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
197
201
|
* @param startup - Whether this is the bridge's activation attempt.
|
|
198
202
|
*/
|
|
199
203
|
async function connectGeneration(startup) {
|
|
200
|
-
const generation = new Client({ name:
|
|
204
|
+
const generation = new Client({ name: PLUGIN_NAME, version: PLUGIN_VERSION }, { capabilities: {} });
|
|
201
205
|
const closed = Promise.withResolvers();
|
|
202
206
|
let attemptSettled = false;
|
|
203
207
|
let closeObserved = false;
|
|
@@ -228,14 +232,36 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
228
232
|
host.logger.error(`${label}: tool re-sync failed: ${String(error)}`);
|
|
229
233
|
}
|
|
230
234
|
});
|
|
235
|
+
/**
|
|
236
|
+
* Retire a generation whose attempt failed: close it, wait for the
|
|
237
|
+
* transport's own close signal, then hand it to the disconnect path. A
|
|
238
|
+
* generation that will not close stops reconnection outright — respawning
|
|
239
|
+
* would overlap the server process still running behind it.
|
|
240
|
+
*/
|
|
241
|
+
async function teardownFailedAttempt() {
|
|
242
|
+
try {
|
|
243
|
+
await generation.close();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* transport already gone */
|
|
247
|
+
}
|
|
248
|
+
const quiesced = hasClosed() || (await waitForClose(closed.promise));
|
|
249
|
+
attemptSettled = true;
|
|
250
|
+
if (!isCurrent(generation))
|
|
251
|
+
return;
|
|
252
|
+
if (quiesced) {
|
|
253
|
+
generationDown(generation);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
client = undefined;
|
|
257
|
+
clientClosed = undefined;
|
|
258
|
+
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`);
|
|
259
|
+
}
|
|
231
260
|
const { transport, oauthProvider } = createTransport(config, credentials, (message) => {
|
|
232
261
|
host.logger.info(message);
|
|
233
262
|
});
|
|
234
263
|
try {
|
|
235
|
-
|
|
236
|
-
await generation.connect(transport);
|
|
237
|
-
else
|
|
238
|
-
await generation.connect(transport, { timeout: config.startupTimeoutMs });
|
|
264
|
+
await generation.connect(transport, { timeout: opts.startupTimeoutMs });
|
|
239
265
|
if (hasClosed()) {
|
|
240
266
|
attemptSettled = true;
|
|
241
267
|
generationDown(generation);
|
|
@@ -261,11 +287,9 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
261
287
|
attemptSettled = true;
|
|
262
288
|
if (!isCurrent(generation))
|
|
263
289
|
return;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
if (!legSettled) {
|
|
290
|
+
// The leg failed, or the transport closed while it ran: no generation
|
|
291
|
+
// is left to finish establishing.
|
|
292
|
+
if (hasClosed() || !legSettled) {
|
|
269
293
|
generationDown(generation);
|
|
270
294
|
return;
|
|
271
295
|
}
|
|
@@ -277,20 +301,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
277
301
|
catch (syncError) {
|
|
278
302
|
if (isCurrent(generation))
|
|
279
303
|
host.logger.warn(`${label}: tool sync after authorization failed: ${String(syncError)}`);
|
|
280
|
-
|
|
281
|
-
await generation.close();
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
/* transport already gone */
|
|
285
|
-
}
|
|
286
|
-
const quiescedSync = hasClosed() || (await waitForClose(closed.promise));
|
|
287
|
-
if (!quiescedSync) {
|
|
288
|
-
client = undefined;
|
|
289
|
-
clientClosed = undefined;
|
|
290
|
-
host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
generationDown(generation);
|
|
304
|
+
await teardownFailedAttempt();
|
|
294
305
|
return;
|
|
295
306
|
}
|
|
296
307
|
if (hasClosed()) {
|
|
@@ -304,23 +315,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
304
315
|
host.logger.info(`${label}: authorized and re-synced tools (attempt ${failedAttempts}/${policy.maxAttempts})`);
|
|
305
316
|
return;
|
|
306
317
|
}
|
|
307
|
-
|
|
308
|
-
await generation.close();
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
/* transport already gone */
|
|
312
|
-
}
|
|
313
|
-
const quiesced = hasClosed() || (await waitForClose(closed.promise));
|
|
314
|
-
attemptSettled = true;
|
|
315
|
-
if (!isCurrent(generation))
|
|
316
|
-
return;
|
|
317
|
-
if (!quiesced) {
|
|
318
|
-
client = undefined;
|
|
319
|
-
clientClosed = undefined;
|
|
320
|
-
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`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
generationDown(generation);
|
|
318
|
+
await teardownFailedAttempt();
|
|
324
319
|
return;
|
|
325
320
|
}
|
|
326
321
|
attemptSettled = true;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural mirrors of the harness tool-registration surface the bridge
|
|
3
|
+
* registers against: the runtime accepts the same object shape the upstream
|
|
4
|
+
* `@deepseek-ai/dsh-tools` bridge registers, so the model-facing behavior is
|
|
5
|
+
* identical. Mirrored rather than imported so the market never loads a second
|
|
6
|
+
* copy of the host runtime.
|
|
7
|
+
*
|
|
8
|
+
* @module runtime/mcp-client/host-contract
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -1,88 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* host change can be re-diffed against `~/workspace/deepseek-harness`.
|
|
10
|
-
*
|
|
11
|
-
* Anything larger than a handful of lines (the JSON Schema subset validator)
|
|
12
|
-
* lives in its own module.
|
|
13
|
-
*
|
|
14
|
-
* @module runtime/mcp-client/host-seams
|
|
15
|
-
*/
|
|
16
|
-
// ---- dsh-timeout: MAX_TIMER_DELAY_MS (packages/util/timeout/src/index.ts) ----
|
|
17
|
-
/**
|
|
18
|
-
* The largest delay `setTimeout` accepts without overflowing to a 1ms clamp
|
|
19
|
-
* (2^31 - 1). Shared guard for reconnect and callback timers.
|
|
20
|
-
*/
|
|
21
|
-
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
22
|
-
// ---- dsh-subprocess: scrubbedParentEnv (packages/subprocess/subprocess/src/index.ts) ----
|
|
23
|
-
/** `DSH_*` environment namespace owned by the harness. */
|
|
24
|
-
export const DSH_ENV_PREFIX = 'DSH_';
|
|
25
|
-
/**
|
|
26
|
-
* Credential-shaped environment names are NOT forwarded to MCP server child
|
|
27
|
-
* processes (the harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a
|
|
28
|
-
* spawned server implicitly). Deliberately supplied env entries survive
|
|
29
|
-
* because the config's explicit env merges after the scrub.
|
|
30
|
-
*/
|
|
31
|
-
export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i;
|
|
32
|
-
/**
|
|
33
|
-
* The ambient parent environment minus credential-shaped names and minus all
|
|
34
|
-
* `DSH_*` names — the canonical base every harness child starts from. `PATH`,
|
|
35
|
-
* `HOME`, locale, and proxy variables survive, so child CLIs run normally;
|
|
36
|
-
* harness identity never leaks implicitly. Both scrubs match case-insensitively:
|
|
37
|
-
* Windows environment names are case-insensitive, so a parent `dsh_*` entry
|
|
38
|
-
* would otherwise survive and read back as `$env:DSH_*` in the child.
|
|
39
|
-
* @returns a fresh environment object safe to hand to a child spawn.
|
|
40
|
-
*/
|
|
41
|
-
export function scrubbedParentEnv() {
|
|
42
|
-
const env = {};
|
|
43
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
44
|
-
if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX))
|
|
45
|
-
env[key] = value;
|
|
46
|
-
}
|
|
47
|
-
return env;
|
|
48
|
-
}
|
|
49
|
-
/** Key segments are lowercase hyphenated identifiers. */
|
|
50
|
-
const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
51
|
-
/**
|
|
52
|
-
* Brand a scope and an id as a {@link CredentialKey}.
|
|
53
|
-
* @param scope - the owning plugin's registered name (here: the `mcp-auth` record scope).
|
|
54
|
-
* @param id - the plugin's own addressing unit (here: the folded serverName).
|
|
55
|
-
* @returns the branded key.
|
|
56
|
-
* @throws TypeError when either segment is not a lowercase hyphenated identifier.
|
|
57
|
-
*/
|
|
58
|
-
export function credentialKey(scope, id) {
|
|
59
|
-
for (const segment of [scope, id]) {
|
|
60
|
-
if (!KEY_SEGMENT_PATTERN.test(segment)) {
|
|
61
|
-
throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return `${scope}/${id}`;
|
|
65
|
-
}
|
|
66
|
-
// ---- dsh-attachment: image admission error routing (packages/attachment/attachment/src/error.ts) ----
|
|
67
|
-
/** Caller-correctable attachment failure codes raised while admitting image input. */
|
|
68
|
-
const IMAGE_ADMISSION_ERROR_CODES = [
|
|
69
|
-
'TOO_MANY_IMAGES',
|
|
70
|
-
'IMAGES_TOO_LARGE',
|
|
71
|
-
'UNSUPPORTED_IMAGE_TYPE',
|
|
72
|
-
'INVALID_IMAGE_BASE64',
|
|
73
|
-
'INVALID_IMAGE',
|
|
74
|
-
'IMAGE_TYPE_MISMATCH',
|
|
75
|
-
'IMAGE_TOO_LARGE',
|
|
76
|
-
'IMAGE_TOO_MANY_PIXELS',
|
|
77
|
-
'IMAGE_DIMENSION_TOO_LARGE'
|
|
78
|
-
];
|
|
79
|
-
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
80
|
-
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
|
|
81
|
-
/**
|
|
82
|
-
* Distinguish caller-correctable image admission failures from storage faults,
|
|
83
|
-
* so an admission refusal projects text diagnostics while a storage fault is
|
|
84
|
-
* reported as such.
|
|
85
|
-
*/
|
|
86
|
-
export function isImageAdmissionError(error) {
|
|
87
|
-
return error instanceof Error && 'code' in error && typeof error.code === 'string' && IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
88
|
-
}
|
|
1
|
+
// ---- dsh-timeout: timer bounds ----
|
|
2
|
+
export { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
|
|
3
|
+
// ---- dsh-subprocess: the canonical child environment ----
|
|
4
|
+
export { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
|
|
5
|
+
// ---- dsh-credentials: key grammar ----
|
|
6
|
+
export { credentialKey } from '@deepseek-ai/dsh-credentials';
|
|
7
|
+
// ---- dsh-attachment: durable image vocabulary ----
|
|
8
|
+
export { isImageAdmissionError } from '@deepseek-ai/dsh-attachment';
|
|
@@ -22,18 +22,16 @@
|
|
|
22
22
|
* then last exactly as long as the process.
|
|
23
23
|
*
|
|
24
24
|
* Ported from the harness `dsh-mcp-client` OAuth implementation (upstream
|
|
25
|
-
* commits archived at `docs/upstream-proposal/patches/`); the record format is
|
|
25
|
+
* commits archived at `docs/developer/upstream-proposal/patches/`); the record format is
|
|
26
26
|
* byte-compatible, so grants written by either implementation are interchangeable.
|
|
27
27
|
*
|
|
28
28
|
* @module runtime/mcp-client/oauth
|
|
29
29
|
*/
|
|
30
30
|
import { createServer } from 'node:http';
|
|
31
31
|
import { credentialKey, scrubbedParentEnv } from './host-seams.js';
|
|
32
|
+
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
32
33
|
/** OAuth client identity presented during dynamic registration. */
|
|
33
|
-
const CLIENT_NAME =
|
|
34
|
-
/** Software id/version advertised in RFC 7591 registration metadata. */
|
|
35
|
-
const CLIENT_SOFTWARE_ID = 'dsh-agent-plugins-market';
|
|
36
|
-
const CLIENT_SOFTWARE_VERSION = '0.1.0';
|
|
34
|
+
const CLIENT_NAME = `DeepSeek Harness (${PLUGIN_NAME})`;
|
|
37
35
|
/** Credential-record scope for MCP OAuth state; per-server ids follow it. */
|
|
38
36
|
export const MCP_AUTH_RECORD_SCOPE = 'mcp-auth';
|
|
39
37
|
/** Loopback callback budget: the browser must land within five minutes. */
|
|
@@ -146,8 +144,8 @@ export class LoopbackOAuthClientProvider {
|
|
|
146
144
|
// Public client: PKCE carries the proof; no client secret exists.
|
|
147
145
|
token_endpoint_auth_method: 'none',
|
|
148
146
|
...(this.scope === undefined ? {} : { scope: this.scope }),
|
|
149
|
-
software_id:
|
|
150
|
-
software_version:
|
|
147
|
+
software_id: PLUGIN_NAME,
|
|
148
|
+
software_version: PLUGIN_VERSION
|
|
151
149
|
};
|
|
152
150
|
}
|
|
153
151
|
async clientInformation() {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This plugin's own identity as advertised to the servers it talks to: the MCP
|
|
3
|
+
* client implementation name and version, and the RFC 7591 `software_id` /
|
|
4
|
+
* `software_version` carried in OAuth dynamic-registration metadata.
|
|
5
|
+
*
|
|
6
|
+
* The version is read from the installed manifest through Node's package
|
|
7
|
+
* self-reference, so a release bumps exactly one number — `package.json`. A
|
|
8
|
+
* copy that can no longer resolve its own manifest (a bundle that inlined this
|
|
9
|
+
* module) reports {@link UNKNOWN_VERSION} rather than failing the connection.
|
|
10
|
+
*
|
|
11
|
+
* @module runtime/mcp-client/plugin-identity
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
/** Name npm and the harness know this plugin by. */
|
|
15
|
+
export const PLUGIN_NAME = 'dsh-agent-plugins-market';
|
|
16
|
+
/** Reported when the plugin cannot read its own manifest; never a released version. */
|
|
17
|
+
const UNKNOWN_VERSION = '0.0.0';
|
|
18
|
+
function readVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const manifest = createRequire(import.meta.url)('dsh-agent-plugins-market/package.json');
|
|
21
|
+
return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : UNKNOWN_VERSION;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return UNKNOWN_VERSION;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** This plugin's release version. */
|
|
28
|
+
export const PLUGIN_VERSION = readVersion();
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP result projection: turn the content blocks an MCP server returns into
|
|
3
|
+
* the host content vocabulary the model sees.
|
|
4
|
+
*
|
|
5
|
+
* Text-like blocks collapse into newline-joined text runs; declared images are
|
|
6
|
+
* decoded, admitted against the live model route, and saved to the durable
|
|
7
|
+
* attachment store. Every refusal — an unsupported media type, a model that
|
|
8
|
+
* does not declare image input, a storage fault — projects the block as text
|
|
9
|
+
* explaining why, and the canonical raw value stays available to programmatic
|
|
10
|
+
* callers in the tool result.
|
|
11
|
+
*
|
|
12
|
+
* Everything here reads an untrusted network payload: the MCP server is an
|
|
13
|
+
* external process and the SDK's declared-required fields may be absent at
|
|
14
|
+
* runtime, so each block is narrowed field by field.
|
|
15
|
+
*
|
|
16
|
+
* @module runtime/mcp-client/projection
|
|
17
|
+
*/
|
|
18
|
+
import { isImageAdmissionError } from './host-seams.js';
|
|
19
|
+
/** Raster formats supported by the durable attachment vocabulary. */
|
|
20
|
+
const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
21
|
+
/** Canonical RFC 4648 base64, excluding whitespace and URL-safe aliases. */
|
|
22
|
+
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
23
|
+
/** Whether an untrusted MCP content array contains a declared image block. */
|
|
24
|
+
export function containsImage(content) {
|
|
25
|
+
return content.some(value => isRecord(value) && value.type === 'image');
|
|
26
|
+
}
|
|
27
|
+
/** Narrow one JSON value to a string-keyed object. */
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
/** Narrow a declared MIME string to the durable image vocabulary. */
|
|
32
|
+
function isImageMediaType(value) {
|
|
33
|
+
return IMAGE_MEDIA_TYPES.includes(value);
|
|
34
|
+
}
|
|
35
|
+
/** Decode one untrusted MCP image block without accepting base64 aliases. */
|
|
36
|
+
function decodeImage(block) {
|
|
37
|
+
if (block.mimeType === undefined || !isImageMediaType(block.mimeType)) {
|
|
38
|
+
throw new Error('the declared media type is not PNG, JPEG, WebP, or GIF');
|
|
39
|
+
}
|
|
40
|
+
if (block.data === undefined || !CANONICAL_BASE64.test(block.data)) {
|
|
41
|
+
throw new Error('the image data is not canonical base64');
|
|
42
|
+
}
|
|
43
|
+
const data = Buffer.from(block.data, 'base64');
|
|
44
|
+
if (data.toString('base64') !== block.data) {
|
|
45
|
+
throw new Error('the image data is not canonical base64');
|
|
46
|
+
}
|
|
47
|
+
return { data, mediaType: block.mimeType };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the active model route and durable store for an image-bearing result.
|
|
51
|
+
* @param host - bridge host with optional services.
|
|
52
|
+
* @param exec - exact tool execution whose agent supplies the latest route.
|
|
53
|
+
* @returns the attachment store after exact positive image-capability proof.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveImageAdmission(host, exec) {
|
|
56
|
+
const attachments = host.getService?.('attachments');
|
|
57
|
+
if (attachments === undefined || typeof attachments.saveImages !== 'function') {
|
|
58
|
+
throw new Error('no attachment store is mounted');
|
|
59
|
+
}
|
|
60
|
+
const routed = exec.agent?.session?.requestHeader?.()?.config;
|
|
61
|
+
const provider = routed?.provider ?? exec.agent?.options?.provider;
|
|
62
|
+
const model = routed?.model ?? exec.agent?.options?.model;
|
|
63
|
+
const llm = host.getService?.('llm');
|
|
64
|
+
if (provider === undefined || model === undefined || llm === undefined) {
|
|
65
|
+
throw new Error('the current model route could not be resolved');
|
|
66
|
+
}
|
|
67
|
+
let info;
|
|
68
|
+
try {
|
|
69
|
+
info = await llm.resolveModelInfo(provider, model, exec.signal);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error('the current model route could not be verified');
|
|
73
|
+
}
|
|
74
|
+
if (info?.inputModalities === undefined || !info.inputModalities.includes('image')) {
|
|
75
|
+
throw new Error(`model "${model}" does not declare image input`);
|
|
76
|
+
}
|
|
77
|
+
if (exec.signal.aborted)
|
|
78
|
+
throw new Error('the tool call was canceled before image storage');
|
|
79
|
+
return attachments;
|
|
80
|
+
}
|
|
81
|
+
/** Stable diagnostic text for an image block that was not admitted. */
|
|
82
|
+
function imageDiagnostic(block, reason) {
|
|
83
|
+
return `[image unavailable: ${block.mimeType ?? 'unknown media type'}; ${reason}; raw image data remains available to programmatic callers]`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Decode, preflight, and durably save one MCP result's ordered image batch.
|
|
87
|
+
* Any refusal projects every image as text while retaining the canonical raw
|
|
88
|
+
* value for programmatic callers.
|
|
89
|
+
*/
|
|
90
|
+
export async function prepareImageProjection(host, exec, content, toolName) {
|
|
91
|
+
const decoded = [];
|
|
92
|
+
const validationErrors = new Map();
|
|
93
|
+
const imageIndexes = [];
|
|
94
|
+
for (const [index, value] of content.entries()) {
|
|
95
|
+
if (!isRecord(value) || value.type !== 'image')
|
|
96
|
+
continue;
|
|
97
|
+
imageIndexes.push(index);
|
|
98
|
+
try {
|
|
99
|
+
decoded.push(decodeImage(value));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// decodeImage owns every throw above and always produces Error.
|
|
103
|
+
validationErrors.set(index, error.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (validationErrors.size > 0) {
|
|
107
|
+
return projectContent(content, toolName, (block, index) => ({
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: imageDiagnostic(block, validationErrors.get(index) ?? 'another image in the same result was invalid')
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
let attachments;
|
|
113
|
+
try {
|
|
114
|
+
attachments = await resolveImageAdmission(host, exec);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// resolveImageAdmission contains provider failures and throws Error only.
|
|
118
|
+
const reason = error.message;
|
|
119
|
+
return projectContent(content, toolName, block => ({ type: 'text', text: imageDiagnostic(block, reason) }));
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const refs = await attachments.saveImages(decoded);
|
|
123
|
+
const byIndex = new Map(imageIndexes.map((index, offset) => [index, refs[offset]]));
|
|
124
|
+
return projectContent(content, toolName, (_block, index) => ({
|
|
125
|
+
type: 'image',
|
|
126
|
+
attachment: byIndex.get(index)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const reason = isImageAdmissionError(error) ? `image admission rejected the result: ${error.message}` : 'durable image storage rejected the result';
|
|
131
|
+
return projectContent(content, toolName, block => ({
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: imageDiagnostic(block, reason)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract text from an MCP content array into a single string.
|
|
139
|
+
* - text blocks: join with '\n'
|
|
140
|
+
* - image/audio/resource blocks: replaced with a placeholder
|
|
141
|
+
*
|
|
142
|
+
* Defensive: fields that the MCP spec declares required (mimeType, text) are
|
|
143
|
+
* guarded with fallbacks because this is a network trust boundary.
|
|
144
|
+
*/
|
|
145
|
+
export function extractText(mcpContent, toolName) {
|
|
146
|
+
const content = projectContent(mcpContent, toolName);
|
|
147
|
+
// The default image projector below also returns text, so this local call
|
|
148
|
+
// cannot produce a core image block.
|
|
149
|
+
return content.map(block => block.text).join('\n');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Project ordered MCP blocks into the core content vocabulary.
|
|
153
|
+
* Text-like runs are newline-coalesced; admitted images split those runs at
|
|
154
|
+
* their original position.
|
|
155
|
+
*/
|
|
156
|
+
function projectContent(mcpContent, toolName, image = block => ({
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: imageDiagnostic(block, 'this result was not admitted to durable model context')
|
|
159
|
+
})) {
|
|
160
|
+
const projected = [];
|
|
161
|
+
const text = [];
|
|
162
|
+
const flushText = () => {
|
|
163
|
+
if (text.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
projected.push({ type: 'text', text: text.splice(0).join('\n') });
|
|
166
|
+
};
|
|
167
|
+
for (const [index, value] of mcpContent.entries()) {
|
|
168
|
+
if (!isRecord(value)) {
|
|
169
|
+
text.push('[unsupported MCP content block: expected an object]');
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const block = value;
|
|
173
|
+
switch (block.type) {
|
|
174
|
+
case 'text':
|
|
175
|
+
if (block.text !== undefined)
|
|
176
|
+
text.push(block.text);
|
|
177
|
+
break;
|
|
178
|
+
case 'image':
|
|
179
|
+
flushText();
|
|
180
|
+
projected.push(image(block, index));
|
|
181
|
+
break;
|
|
182
|
+
case 'resource_link':
|
|
183
|
+
if (block.name === undefined || block.uri === undefined) {
|
|
184
|
+
text.push('[resource link unavailable: the MCP block is missing its name or URI]');
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
text.push(`Resource link: ${block.name} (${block.uri})`);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case 'audio':
|
|
191
|
+
text.push(`[audio result unsupported: ${block.mimeType ?? 'unknown media type'}; raw audio data remains available to programmatic callers]`);
|
|
192
|
+
break;
|
|
193
|
+
case 'resource':
|
|
194
|
+
text.push('[embedded resource unsupported; raw resource data remains available to programmatic callers]');
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
text.push(`[unsupported MCP content type: ${block.type}]`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
flushText();
|
|
201
|
+
return projected.length > 0 ? projected : [{ type: 'text', text: `(${toolName} returned no model-visible content)` }];
|
|
202
|
+
}
|