dsh-agent-plugins-market 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -26
- package/README.zh.md +31 -26
- package/client/client.js +29164 -1136
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +27 -2
- package/lib/application/details.js +13 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +191 -19
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +16 -1
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +3 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +32 -13
- package/lib/model/layouts.js +44 -1
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +43 -2
- package/lib/runtime/agent-role-router.js +203 -56
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +45 -15
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +9 -8
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +182 -35
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +24 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +19 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +38 -3
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +8 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +77 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +30 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +96 -18
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
- package/lib/types/runtime/user-panels.d.ts +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +23 -6
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
3
|
import * as mcpBridge from './mcp-client/bridge.js';
|
|
3
4
|
import { toMcpMounts } from './mcp-config.js';
|
|
4
5
|
import { mcpCredentialResolver } from './mcp-credentials.js';
|
|
5
6
|
import { SerialPassQueue, RetryScheduler } from './mount-lifecycle.js';
|
|
6
|
-
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
7
|
+
import { qualifiedSuiteId, suiteDataDir } from '../catalog/paths.js';
|
|
8
|
+
import { redactErrorMessage } from './mcp-redaction.js';
|
|
7
9
|
export class McpMountRegistry {
|
|
8
10
|
ctx;
|
|
9
11
|
pluginDataRoot;
|
|
@@ -178,6 +180,16 @@ export class McpMountRegistry {
|
|
|
178
180
|
}
|
|
179
181
|
/** Mount one precomputed request (source config merged with overrides). */
|
|
180
182
|
async mountWith(request) {
|
|
183
|
+
// §9.1: the client-managed PLUGIN_DATA directory must exist and be
|
|
184
|
+
// writable before any plugin subprocess starts. Created recursively and
|
|
185
|
+
// idempotently on every mount so an uninstalled-then-reinstalled suite
|
|
186
|
+
// starts over with a fresh directory while an update keeps its contents.
|
|
187
|
+
if (request.config.transport === 'stdio') {
|
|
188
|
+
const separator = request.suiteId.indexOf('/');
|
|
189
|
+
const sourceId = separator === -1 ? request.suiteId : request.suiteId.slice(0, separator);
|
|
190
|
+
const suiteId = separator === -1 ? request.suiteId : request.suiteId.slice(separator + 1);
|
|
191
|
+
await mkdir(suiteDataDir(this.pluginDataRoot, sourceId, suiteId), { recursive: true });
|
|
192
|
+
}
|
|
181
193
|
const owner = this.serverOwner(request.config.serverName);
|
|
182
194
|
if (owner !== undefined) {
|
|
183
195
|
// Two sources shipping the same suite/server pair derive one serverName:
|
|
@@ -251,7 +263,7 @@ export class McpMountRegistry {
|
|
|
251
263
|
// Ignore teardown errors: the startup failure is the real signal.
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
|
-
return { reason: `mount failed: ${error instanceof Error ? error.message : String(error)}`, code: 'mount-failed' };
|
|
266
|
+
return { reason: `mount failed: ${redactErrorMessage(error instanceof Error ? error.message : String(error))}`, code: 'mount-failed' };
|
|
255
267
|
}
|
|
256
268
|
this.live.set(mountKey(request.suiteId, request.serverKey), {
|
|
257
269
|
suiteId: request.suiteId,
|
|
@@ -271,7 +283,7 @@ export class McpMountRegistry {
|
|
|
271
283
|
return undefined;
|
|
272
284
|
}
|
|
273
285
|
catch (error) {
|
|
274
|
-
const reason = `unmount failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
286
|
+
const reason = `unmount failed: ${redactErrorMessage(error instanceof Error ? error.message : String(error))}`;
|
|
275
287
|
this.ctx.logger?.warn(`[dsh-agent-plugins-market] ${reason} (${live.suiteId}/${live.serverKey})`);
|
|
276
288
|
return reason;
|
|
277
289
|
}
|
|
@@ -15,6 +15,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
17
17
|
import { isSensitiveKey } from './mcp-redaction.js';
|
|
18
|
+
/** The largest timeout a bridge config and the host timer can hold. */
|
|
19
|
+
export const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
18
20
|
/** The overrides directory under the plugin data root. */
|
|
19
21
|
export function overridesDir(dataRoot) {
|
|
20
22
|
return join(dataRoot, 'overrides');
|
|
@@ -69,11 +71,41 @@ export function sanitizeOverrides(raw) {
|
|
|
69
71
|
if (Array.isArray(record['args']) && record['args'].every(entry => typeof entry === 'string')) {
|
|
70
72
|
override.args = record['args'];
|
|
71
73
|
}
|
|
74
|
+
const toolCallTimeoutMs = timeoutMs(record['toolCallTimeoutMs']);
|
|
75
|
+
if (toolCallTimeoutMs !== undefined)
|
|
76
|
+
override.toolCallTimeoutMs = toolCallTimeoutMs;
|
|
77
|
+
const startupTimeoutMs = timeoutMs(record['startupTimeoutMs']);
|
|
78
|
+
if (startupTimeoutMs !== undefined)
|
|
79
|
+
override.startupTimeoutMs = startupTimeoutMs;
|
|
80
|
+
const disabledTools = toolNames(record['disabledTools']);
|
|
81
|
+
if (disabledTools !== undefined)
|
|
82
|
+
override.disabledTools = disabledTools;
|
|
72
83
|
if (Object.keys(override).length > 0)
|
|
73
84
|
result[serverKey] = override;
|
|
74
85
|
}
|
|
75
86
|
return result;
|
|
76
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Keep a timeout that a millisecond count can hold. Anything else passes
|
|
90
|
+
* nothing through, so one malformed field cannot take the rest of the record
|
|
91
|
+
* with it.
|
|
92
|
+
*/
|
|
93
|
+
function timeoutMs(raw) {
|
|
94
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0 || raw > MAX_TIMEOUT_MS)
|
|
95
|
+
return undefined;
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Keep a tool-name list for a deny list: non-empty names, deduplicated, with
|
|
100
|
+
* the empty result reading as "nothing denied" so the field never persists as
|
|
101
|
+
* an empty array.
|
|
102
|
+
*/
|
|
103
|
+
function toolNames(raw) {
|
|
104
|
+
if (!Array.isArray(raw))
|
|
105
|
+
return undefined;
|
|
106
|
+
const names = [...new Set(raw.filter((entry) => typeof entry === 'string' && entry !== ''))];
|
|
107
|
+
return names.length > 0 ? names : undefined;
|
|
108
|
+
}
|
|
77
109
|
/** Keep a well-formed auth block; anything else passes nothing through. */
|
|
78
110
|
function sanitizeAuth(raw) {
|
|
79
111
|
if (typeof raw !== 'object' || raw === null)
|
|
@@ -94,6 +126,69 @@ export function sanitizeOverridePatch(patch) {
|
|
|
94
126
|
return undefined;
|
|
95
127
|
return sanitizeOverrides({ server: patch })['server'];
|
|
96
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Validate one policy save as it arrives over the wire. An absent field leaves
|
|
131
|
+
* its stored value alone, `null` clears one back to inheritance, and a positive
|
|
132
|
+
* whole number of milliseconds within the timer range sets it; anything else
|
|
133
|
+
* rejects the save with a readable reason.
|
|
134
|
+
*/
|
|
135
|
+
export function parseTimeoutPatch(raw) {
|
|
136
|
+
if (raw === undefined)
|
|
137
|
+
return {};
|
|
138
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
139
|
+
throw new Error('policy must be an object');
|
|
140
|
+
const record = raw;
|
|
141
|
+
return {
|
|
142
|
+
...(record['toolCallTimeoutMs'] === undefined ? {} : { toolCallTimeoutMs: timeoutField(record['toolCallTimeoutMs'], 'tool call timeout') }),
|
|
143
|
+
...(record['startupTimeoutMs'] === undefined ? {} : { startupTimeoutMs: timeoutField(record['startupTimeoutMs'], 'startup timeout') })
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Validate a client-supplied policy patch. An absent field keeps its stored
|
|
148
|
+
* value, `null` clears it back to the suite's declaration or the default, and
|
|
149
|
+
* any other shape is rejected with the reason it failed.
|
|
150
|
+
*/
|
|
151
|
+
export function parseMcpPolicyPatch(raw) {
|
|
152
|
+
if (raw === undefined)
|
|
153
|
+
return {};
|
|
154
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
155
|
+
throw new Error('policy must be an object');
|
|
156
|
+
const record = raw;
|
|
157
|
+
const patch = {};
|
|
158
|
+
if (record['toolCallTimeoutMs'] !== undefined)
|
|
159
|
+
patch.toolCallTimeoutMs = timeoutField(record['toolCallTimeoutMs'], 'tool call timeout');
|
|
160
|
+
if (record['startupTimeoutMs'] !== undefined)
|
|
161
|
+
patch.startupTimeoutMs = timeoutField(record['startupTimeoutMs'], 'startup timeout');
|
|
162
|
+
if (record['disabledTools'] !== undefined) {
|
|
163
|
+
if (record['disabledTools'] === null)
|
|
164
|
+
patch.disabledTools = null;
|
|
165
|
+
else {
|
|
166
|
+
const names = toolNames(record['disabledTools']);
|
|
167
|
+
if (names === undefined && record['disabledTools'].length > 0)
|
|
168
|
+
throw new Error('denied tools must be a list of tool names');
|
|
169
|
+
patch.disabledTools = names ?? [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (record['auth'] !== undefined) {
|
|
173
|
+
if (record['auth'] === null)
|
|
174
|
+
patch.auth = null;
|
|
175
|
+
else {
|
|
176
|
+
const auth = sanitizeAuth(record['auth']);
|
|
177
|
+
if (auth === undefined)
|
|
178
|
+
throw new Error('auth must be an object with a boolean "enabled"');
|
|
179
|
+
patch.auth = auth;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return patch;
|
|
183
|
+
}
|
|
184
|
+
function timeoutField(value, label) {
|
|
185
|
+
if (value === null)
|
|
186
|
+
return null;
|
|
187
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS) {
|
|
188
|
+
throw new Error(`${label} must be a positive whole number of milliseconds no greater than ${MAX_TIMEOUT_MS}`);
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
97
192
|
/**
|
|
98
193
|
* Merge one client patch into the override already on disk.
|
|
99
194
|
*
|
|
@@ -113,7 +208,18 @@ export function mergeOverridePatch(existing, patch) {
|
|
|
113
208
|
...(headers === undefined ? {} : { headers }),
|
|
114
209
|
...(env === undefined ? {} : { env }),
|
|
115
210
|
...(patch.args !== undefined ? { args: patch.args } : existing.args === undefined ? {} : { args: existing.args }),
|
|
116
|
-
...(patch.auth !== undefined ? { auth: patch.auth } : existing.auth === undefined ? {} : { auth: existing.auth })
|
|
211
|
+
...(patch.auth !== undefined ? { auth: patch.auth } : existing.auth === undefined ? {} : { auth: existing.auth }),
|
|
212
|
+
...(patch.toolCallTimeoutMs !== undefined
|
|
213
|
+
? { toolCallTimeoutMs: patch.toolCallTimeoutMs }
|
|
214
|
+
: existing.toolCallTimeoutMs === undefined
|
|
215
|
+
? {}
|
|
216
|
+
: { toolCallTimeoutMs: existing.toolCallTimeoutMs }),
|
|
217
|
+
...(patch.startupTimeoutMs !== undefined
|
|
218
|
+
? { startupTimeoutMs: patch.startupTimeoutMs }
|
|
219
|
+
: existing.startupTimeoutMs === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { startupTimeoutMs: existing.startupTimeoutMs }),
|
|
222
|
+
...(patch.disabledTools !== undefined ? { disabledTools: patch.disabledTools } : existing.disabledTools === undefined ? {} : { disabledTools: existing.disabledTools })
|
|
117
223
|
};
|
|
118
224
|
}
|
|
119
225
|
function mergeStringMap(existing, patch) {
|
|
@@ -156,22 +262,46 @@ function stringMap(value) {
|
|
|
156
262
|
export function applyOverride(server, override) {
|
|
157
263
|
if (override === undefined)
|
|
158
264
|
return server;
|
|
265
|
+
const policy = policyFields(override);
|
|
159
266
|
if (override.config !== undefined)
|
|
160
267
|
server = override.config;
|
|
161
268
|
if (server.type === 'stdio') {
|
|
162
269
|
return {
|
|
163
270
|
...server,
|
|
271
|
+
...policy,
|
|
164
272
|
...(override.args !== undefined ? { args: override.args } : {}),
|
|
165
273
|
...(override.env !== undefined ? { env: override.env } : {})
|
|
166
274
|
};
|
|
167
275
|
}
|
|
168
276
|
return {
|
|
169
277
|
...server,
|
|
278
|
+
...policy,
|
|
170
279
|
...(override.url !== undefined ? { url: override.url } : {}),
|
|
171
280
|
...(override.headers !== undefined ? { headers: override.headers } : {}),
|
|
172
281
|
...(override.auth !== undefined ? { auth: override.auth } : {})
|
|
173
282
|
};
|
|
174
283
|
}
|
|
284
|
+
/** The user-owned policy fields of one override, in server shape. */
|
|
285
|
+
function policyFields(override) {
|
|
286
|
+
return {
|
|
287
|
+
...(override.toolCallTimeoutMs === undefined ? {} : { toolCallTimeoutMs: override.toolCallTimeoutMs }),
|
|
288
|
+
...(override.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: override.startupTimeoutMs }),
|
|
289
|
+
...(override.disabledTools === undefined ? {} : { disabledTools: override.disabledTools })
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* One server with its client policy fields removed: the shape the portable
|
|
294
|
+
* `mcp.json` schema accepts. The service editor renders and saves this shape,
|
|
295
|
+
* so a timeout never reaches a document that has no seat for it.
|
|
296
|
+
*/
|
|
297
|
+
export function withoutPolicyFields(server) {
|
|
298
|
+
const portable = { ...server };
|
|
299
|
+
delete portable['enabledTools'];
|
|
300
|
+
delete portable['disabledTools'];
|
|
301
|
+
delete portable['startupTimeoutMs'];
|
|
302
|
+
delete portable['toolCallTimeoutMs'];
|
|
303
|
+
return portable;
|
|
304
|
+
}
|
|
175
305
|
function sanitizeFileName(raw) {
|
|
176
306
|
return raw.replace(/[^A-Za-z0-9_-]+/g, '_');
|
|
177
307
|
}
|
|
@@ -45,6 +45,14 @@ export function redactUrl(raw) {
|
|
|
45
45
|
.join('&');
|
|
46
46
|
return safeQuery === '' ? base : `${base}?${safeQuery}`;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Redact every URL embedded in an error message before the message reaches a
|
|
50
|
+
* log line or a diagnostic: connection failures echo the server's endpoint,
|
|
51
|
+
* and endpoints carry credential-bearing queries.
|
|
52
|
+
*/
|
|
53
|
+
export function redactErrorMessage(raw) {
|
|
54
|
+
return raw.replace(/https?:\/\/[^\s"'<>]+/gi, value => redactUrl(value));
|
|
55
|
+
}
|
|
48
56
|
function splitOnce(value, separator) {
|
|
49
57
|
const index = value.indexOf(separator);
|
|
50
58
|
if (index === -1)
|
|
@@ -28,7 +28,7 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
|
|
|
28
28
|
if (suite.mcp === undefined || suite.installedAt === undefined || !suite.enabled)
|
|
29
29
|
continue;
|
|
30
30
|
const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
|
|
31
|
-
for (const { serverKey, server: effective, override, enabled, credentialRefs: refs } of effectiveMcpServers(suite, overrides.get(suiteKey))) {
|
|
31
|
+
for (const { serverKey, server: effective, override, enabled, credentialRefs: refs, policy } of effectiveMcpServers(suite, overrides.get(suiteKey))) {
|
|
32
32
|
const serverName = deriveServerName(suite.id, serverKey);
|
|
33
33
|
const tools = observedByServer.get(serverName) ?? [];
|
|
34
34
|
claimedServers.add(serverName);
|
|
@@ -69,10 +69,13 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
|
|
|
69
69
|
transport: effective.type,
|
|
70
70
|
endpoint: endpointOf(effective),
|
|
71
71
|
config: redactMcpConfig(effective),
|
|
72
|
-
tools: disabled && !orphaned ? [] : tools
|
|
72
|
+
tools: disabled && !orphaned ? [] : observedTools(tools),
|
|
73
73
|
advertisedTools: tools.length > 0,
|
|
74
74
|
retryable: diagnostic?.code === 'mount-failed' || diagnostic?.code === 'unmount-failed',
|
|
75
75
|
...(effective.type === 'stdio' || effective.auth !== undefined ? {} : { oauthDefault: true }),
|
|
76
|
+
...(policy.enabledTools === undefined ? {} : { suiteEnabledTools: policy.enabledTools }),
|
|
77
|
+
...(policy.suiteDisabledTools === undefined ? {} : { suiteDisabledTools: policy.suiteDisabledTools }),
|
|
78
|
+
...(override?.disabledTools === undefined ? {} : { userDisabledTools: override.disabledTools }),
|
|
76
79
|
...(diagnostic?.code === undefined ? {} : { code: diagnostic.code }),
|
|
77
80
|
...(reason === undefined ? {} : { reason }),
|
|
78
81
|
...(credentialRefs.length === 0 ? {} : { credentialRefs })
|
|
@@ -97,7 +100,7 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
|
|
|
97
100
|
transport: stale.server.type,
|
|
98
101
|
endpoint: endpointOf(stale.server),
|
|
99
102
|
config: redactMcpConfig(stale.server),
|
|
100
|
-
tools: tools
|
|
103
|
+
tools: observedTools(tools),
|
|
101
104
|
reason: 'MCP tools remain after this plugin was disabled or uninstalled',
|
|
102
105
|
...(staleRefs.length === 0 ? {} : { credentialRefs: staleRefs })
|
|
103
106
|
});
|
|
@@ -109,7 +112,7 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
|
|
|
109
112
|
kind: 'direct',
|
|
110
113
|
state: 'connected',
|
|
111
114
|
transport: 'observed',
|
|
112
|
-
tools: tools
|
|
115
|
+
tools: observedTools(tools)
|
|
113
116
|
});
|
|
114
117
|
}
|
|
115
118
|
const totals = {
|
|
@@ -124,6 +127,15 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
|
|
|
124
127
|
};
|
|
125
128
|
return { entries, observedAt: new Date().toISOString(), totals, directObservationOnly: true };
|
|
126
129
|
}
|
|
130
|
+
/** One tool projection for the status wire: the input schema rides along only while it stays small. */
|
|
131
|
+
const MAX_SCHEMA_CHARS = 20_000;
|
|
132
|
+
function observedTools(tools) {
|
|
133
|
+
return tools.map(tool => ({
|
|
134
|
+
name: tool.name,
|
|
135
|
+
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
136
|
+
...(tool.parameters === undefined || JSON.stringify(tool.parameters).length > MAX_SCHEMA_CHARS ? {} : { parameters: tool.parameters })
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
127
139
|
function groupObservedTools(observed, knownServerNames) {
|
|
128
140
|
const grouped = new Map();
|
|
129
141
|
for (const tool of observed) {
|
|
@@ -150,7 +162,11 @@ function groupObservedTools(observed, knownServerNames) {
|
|
|
150
162
|
if (serverName === undefined || rawName === undefined || rawName === '')
|
|
151
163
|
continue;
|
|
152
164
|
const list = grouped.get(serverName) ?? [];
|
|
153
|
-
list.push({
|
|
165
|
+
list.push({
|
|
166
|
+
name: rawName,
|
|
167
|
+
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
168
|
+
...(tool.parameters === undefined ? {} : { parameters: tool.parameters })
|
|
169
|
+
});
|
|
154
170
|
grouped.set(serverName, list);
|
|
155
171
|
}
|
|
156
172
|
return grouped;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { CommandMountRegistry } from './commands-mounts.js';
|
|
2
|
+
import { expandPluginPaths, pluginRootOf } from '../catalog/plugin-variables.js';
|
|
3
|
+
import { suiteDataDir } from '../catalog/paths.js';
|
|
2
4
|
import { McpMountRegistry } from './mcp-mounts.js';
|
|
3
5
|
import { HooksMountRegistry } from './hooks-mounts.js';
|
|
4
6
|
import { readFile } from 'node:fs/promises';
|
|
5
7
|
import { stripFrontmatter } from '../catalog/skills-parse.js';
|
|
6
8
|
import { parseFrontmatterRecord } from './user-store.js';
|
|
7
9
|
/** Mount under existing and newly created agents; the caller owns refresh and teardown. */
|
|
8
|
-
export function mountProjectCommands(ctx, catalog, t) {
|
|
9
|
-
return mountProjectSurface(ctx, catalog, 'commands', scope => new CommandMountRegistry(scope, t));
|
|
10
|
+
export function mountProjectCommands(ctx, catalog, t, dataRoot) {
|
|
11
|
+
return mountProjectSurface(ctx, catalog, 'commands', scope => new CommandMountRegistry(scope, t, dataRoot));
|
|
10
12
|
}
|
|
11
13
|
/** MCP uses a separate injected child so network startup cannot delay local commands. */
|
|
12
14
|
export function mountProjectMcp(ctx, catalog, dataRoot) {
|
|
@@ -21,14 +23,18 @@ export function mountProjectHooks(ctx, catalog) {
|
|
|
21
23
|
return mountProjectSurface(ctx, catalog, ['shell', 'sessionProjections'], scope => new HooksMountRegistry(scope));
|
|
22
24
|
}
|
|
23
25
|
/** Kimi's startup skill and system-prompt declarations use the existing scoped prompt service. */
|
|
24
|
-
export function mountSuiteInstructions(ctx, catalog) {
|
|
25
|
-
return mountProjectSurface(ctx, catalog, 'systemPrompt', scope => {
|
|
26
|
+
export function mountSuiteInstructions(ctx, catalog, dataRoot) {
|
|
27
|
+
return mountProjectSurface(ctx, catalog, 'systemPrompt', (scope, agent) => {
|
|
26
28
|
let text = '';
|
|
27
29
|
const host = scope;
|
|
28
30
|
const dispose = host.systemPrompt.section({ name: 'agent-plugins:instructions', order: 500, text: () => text });
|
|
29
31
|
return {
|
|
30
32
|
async reconcile(projectSuites) {
|
|
31
|
-
const
|
|
33
|
+
const projectDir = agent.session.header.cwd;
|
|
34
|
+
const result = await suiteInstructions([...(await catalog.enabledUserSuites()), ...projectSuites], {
|
|
35
|
+
...(dataRoot === undefined ? {} : { dataRoot }),
|
|
36
|
+
...(projectDir === undefined ? {} : { projectDir })
|
|
37
|
+
});
|
|
32
38
|
text = result.text;
|
|
33
39
|
return result.errors;
|
|
34
40
|
},
|
|
@@ -39,14 +45,22 @@ export function mountSuiteInstructions(ctx, catalog) {
|
|
|
39
45
|
};
|
|
40
46
|
}, true);
|
|
41
47
|
}
|
|
42
|
-
export async function suiteInstructions(suites) {
|
|
48
|
+
export async function suiteInstructions(suites, paths = {}) {
|
|
43
49
|
const chunks = [];
|
|
44
50
|
const errors = [];
|
|
45
51
|
for (const suite of suites) {
|
|
46
52
|
if (!suite.enabled || suite.activeSurfaces.skills === false)
|
|
47
53
|
continue;
|
|
54
|
+
// Startup instructions are author-written suite text like any other
|
|
55
|
+
// surface: the paths they name have to resolve before the model reads them.
|
|
56
|
+
const root = pluginRootOf(suite);
|
|
57
|
+
const context = {
|
|
58
|
+
...(root === undefined ? {} : { root, ...(paths.dataRoot === undefined ? {} : { data: suiteDataDir(paths.dataRoot, suite.sourceId, suite.id) }) }),
|
|
59
|
+
...(paths.projectDir === undefined ? {} : { projectDir: paths.projectDir })
|
|
60
|
+
};
|
|
61
|
+
const expand = (text) => expandPluginPaths(text, context);
|
|
48
62
|
if (suite.systemPrompt !== undefined)
|
|
49
|
-
chunks.push(suite.systemPrompt);
|
|
63
|
+
chunks.push(expand(suite.systemPrompt));
|
|
50
64
|
if (suite.manifest.startupSkill === undefined)
|
|
51
65
|
continue;
|
|
52
66
|
const skill = suite.skills.find(skill => skill.name === suite.manifest.startupSkill);
|
|
@@ -58,9 +72,9 @@ export async function suiteInstructions(suites) {
|
|
|
58
72
|
const content = await readFile(skill.file, 'utf8');
|
|
59
73
|
if (parseFrontmatterRecord(content).disabled === true)
|
|
60
74
|
continue;
|
|
61
|
-
chunks.push(stripFrontmatter(content));
|
|
75
|
+
chunks.push(expand(stripFrontmatter(content)));
|
|
62
76
|
if (suite.manifest.skillInstructions !== undefined)
|
|
63
|
-
chunks.push(suite.manifest.skillInstructions);
|
|
77
|
+
chunks.push(expand(suite.manifest.skillInstructions));
|
|
64
78
|
}
|
|
65
79
|
catch {
|
|
66
80
|
errors.push({ suiteId: `${suite.sourceId}/${suite.id}`, reason: `startup skill ${skill.name} is unreadable` });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coalesce } from './timer-seat.js';
|
|
1
2
|
/** Credential updates are batched for this long before one reconcile runs. */
|
|
2
3
|
const CREDENTIAL_DEBOUNCE_MS = 150;
|
|
3
4
|
export class ReconcileScheduler {
|
|
@@ -15,13 +16,13 @@ export class ReconcileScheduler {
|
|
|
15
16
|
this.runtime = runtime;
|
|
16
17
|
this.host = host;
|
|
17
18
|
const eventHost = ctx;
|
|
19
|
+
// One reconcile per burst of credential updates: the host fires one event
|
|
20
|
+
// per reference, and every mount that reads them must see the whole set.
|
|
21
|
+
this.credentialFlush = coalesce(ctx, () => this.flushCredentialUpdates(), CREDENTIAL_DEBOUNCE_MS);
|
|
18
22
|
this.releaseCredentialUpdates =
|
|
19
23
|
eventHost.on?.('credentials/reference-updated', ref => {
|
|
20
24
|
this.pendingCredentialRefs.add(ref);
|
|
21
|
-
|
|
22
|
-
clearTimeout(this.credentialFlush);
|
|
23
|
-
this.credentialFlush = setTimeout(() => this.flushCredentialUpdates(), CREDENTIAL_DEBOUNCE_MS);
|
|
24
|
-
this.credentialFlush.unref?.();
|
|
25
|
+
this.credentialFlush();
|
|
25
26
|
}) ?? (() => { });
|
|
26
27
|
}
|
|
27
28
|
/** Reconcile now, joining the pass in flight and remembering one follow-up for it. */
|
|
@@ -46,9 +47,7 @@ export class ReconcileScheduler {
|
|
|
46
47
|
dispose() {
|
|
47
48
|
this.disposed = true;
|
|
48
49
|
this.releaseCredentialUpdates();
|
|
49
|
-
|
|
50
|
-
clearTimeout(this.credentialFlush);
|
|
51
|
-
this.credentialFlush = undefined;
|
|
50
|
+
this.credentialFlush.dispose();
|
|
52
51
|
this.pendingCredentialRefs.clear();
|
|
53
52
|
}
|
|
54
53
|
async reconcileOnce() {
|
|
@@ -86,7 +85,6 @@ export class ReconcileScheduler {
|
|
|
86
85
|
* mid-reconcile is never dropped.
|
|
87
86
|
*/
|
|
88
87
|
flushCredentialUpdates() {
|
|
89
|
-
this.credentialFlush = undefined;
|
|
90
88
|
const refs = [...this.pendingCredentialRefs];
|
|
91
89
|
this.pendingCredentialRefs.clear();
|
|
92
90
|
// Re-read the catalog first: an unknown ref means the snapshot predates
|
|
@@ -12,9 +12,10 @@ export class RuntimeReconciler {
|
|
|
12
12
|
disposed = false;
|
|
13
13
|
constructor(ctx, dataRoot, t = bindHostLocale(undefined)) {
|
|
14
14
|
this.mcp = new McpMountRegistry(ctx, dataRoot);
|
|
15
|
-
this.commands = new CommandMountRegistry(ctx, t);
|
|
15
|
+
this.commands = new CommandMountRegistry(ctx, t, dataRoot);
|
|
16
16
|
this.hooks = new HooksMountRegistry(ctx);
|
|
17
17
|
this.lspRegistry = new LspMountRegistry(ctx);
|
|
18
|
+
this.lspRegistry.setPluginDataRoot(dataRoot);
|
|
18
19
|
}
|
|
19
20
|
/** The LSP mount registry, consumed by the LSP status surface. */
|
|
20
21
|
get lsp() {
|
package/lib/runtime/regions.js
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Download regions for GitHub acquisition, modeled on dshmarket's regions.js:
|
|
3
|
-
* one setting answers "where are you" instead of a row of mirror toggles.
|
|
4
|
-
*
|
|
5
|
-
* The China route proxies github.com clones through a public prefix proxy —
|
|
6
|
-
* `git clone https://gh-proxy.com/https://github.com/owner/repo` — rather
|
|
7
|
-
* than rewriting the hostname, which is what lets one prefix cover clone,
|
|
8
|
-
* fetch, and release downloads without a mapping table per service. Public
|
|
9
|
-
* proxies come and go, so the route carries an environment escape hatch
|
|
10
|
-
* (`DSH_MARKET_GITHUB_PROXY`), and the region itself is a setting the user
|
|
11
|
-
* can flip at any time; sources already cloned keep their proxied origin and
|
|
12
|
-
* follow it on refresh.
|
|
13
|
-
*
|
|
14
|
-
* @module runtime/regions
|
|
15
|
-
*/
|
|
16
1
|
/** The China-route GitHub prefix proxy, no trailing slash. */
|
|
17
2
|
const DEFAULT_GITHUB_PROXY = 'https://gh-proxy.com';
|
|
18
|
-
/** Narrow an untrusted stored value to a setting; anything else reads as auto. */
|
|
19
|
-
export function narrowDownloadRegion(value) {
|
|
20
|
-
return value === 'global' || value === 'china' ? value : 'auto';
|
|
21
|
-
}
|
|
22
3
|
/**
|
|
23
4
|
* Resolve the effective region: an explicit choice wins; `auto` follows the
|
|
24
5
|
* interface language — a Chinese-language user defaults to the China route.
|
|
@@ -5,6 +5,24 @@ import { MCP_SCHEMA_ID, validateAgainstSchema, validateMcpJson } from '../catalo
|
|
|
5
5
|
import { parseLspServers } from '../catalog/lsp-spec.js';
|
|
6
6
|
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
7
7
|
import { redactMcpConfig } from './mcp-redaction.js';
|
|
8
|
+
/**
|
|
9
|
+
* The errors the closed-set package shape produces. A user-owned service does
|
|
10
|
+
* not obey that shape, and the per-transport checks still require what it needs.
|
|
11
|
+
*/
|
|
12
|
+
const PACKAGE_SHAPE_KEYWORDS = new Set(['additionalProperties', 'oneOf', 'required', 'const']);
|
|
13
|
+
/**
|
|
14
|
+
* A rejected MCP configuration. The message keeps the flat form the API has
|
|
15
|
+
* always returned, and `fields` carries the same reasons keyed by field so the
|
|
16
|
+
* editor can put each one beside its input.
|
|
17
|
+
*/
|
|
18
|
+
export class McpConfigError extends Error {
|
|
19
|
+
fields;
|
|
20
|
+
constructor(fields) {
|
|
21
|
+
super(`invalid MCP configuration: ${fields.map(entry => (entry.field === '' ? entry.message : `${entry.field} ${entry.message}`)).join('; ')}`);
|
|
22
|
+
this.fields = fields;
|
|
23
|
+
this.name = 'McpConfigError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
8
26
|
/** Restore only unchanged redacted leaves, rejecting invented masked values. */
|
|
9
27
|
export function restoreRedactedConfig(input, original, redacted = redactMcpConfig(original)) {
|
|
10
28
|
if (typeof input === 'string' && input.includes('[redacted]')) {
|
|
@@ -21,15 +39,38 @@ export function restoreRedactedConfig(input, original, redacted = redactMcpConfi
|
|
|
21
39
|
}
|
|
22
40
|
return input;
|
|
23
41
|
}
|
|
24
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Validate one service definition before it is stored.
|
|
44
|
+
*
|
|
45
|
+
* A service the user declared themselves is local data rather than a
|
|
46
|
+
* distributable package: keys this client does not know are kept as written
|
|
47
|
+
* instead of being rejected by the closed-set package schema, and the
|
|
48
|
+
* package-only rules (bare command names, plugin-relative paths) stay off.
|
|
49
|
+
*/
|
|
50
|
+
export async function validateServerMcp(root, key, config, options) {
|
|
51
|
+
const userOwned = options?.userOwned === true;
|
|
25
52
|
const document = { $schema: MCP_SCHEMA_ID, mcpServers: { [key]: config } };
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
// A JSON pointer needs its own escapes so a server key containing `/` or `~`
|
|
54
|
+
// still yields the right prefix for the field paths below.
|
|
55
|
+
const pointer = `/mcpServers/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
56
|
+
const structural = (await validateAgainstSchema(MCP_SCHEMA_ID, document))
|
|
57
|
+
.filter(error => error.instancePath === pointer || error.instancePath.startsWith(`${pointer}/`))
|
|
58
|
+
// The closed-set package shape reports `additionalProperties` plus the
|
|
59
|
+
// `oneOf`/`required` pair its failure produces; user-owned data does not
|
|
60
|
+
// obey it, and the per-transport checks below still require what it needs.
|
|
61
|
+
.filter(error => !(userOwned && PACKAGE_SHAPE_KEYWORDS.has(error.keyword)))
|
|
62
|
+
.map(error => ({
|
|
63
|
+
field: error.instancePath.slice(pointer.length).replace(/^\//, '').replaceAll('/', '.'),
|
|
64
|
+
message: error.message
|
|
65
|
+
}));
|
|
66
|
+
if (structural.length > 0)
|
|
67
|
+
throw new McpConfigError(structural);
|
|
68
|
+
const result = await validateMcpJson(root, document, userOwned ? { packageRules: false } : undefined);
|
|
69
|
+
if (result.config === undefined || result.errors.length > 0)
|
|
70
|
+
throw new McpConfigError(result.errors.map(message => ({ field: '', message })));
|
|
71
|
+
const server = result.config.servers[key];
|
|
72
|
+
if (server === undefined)
|
|
73
|
+
throw new McpConfigError([{ field: '', message: `server "${key}" was rejected` }]);
|
|
33
74
|
return server;
|
|
34
75
|
}
|
|
35
76
|
export function validateServerLsp(key, config) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { MARKET_SETTINGS_NAMESPACE, resolveMarketSettings } from '../contracts/settings.js';
|
|
1
2
|
import { FEEDBACK_TOOL_NAME, mountFeedbackTool } from './feedback-tool.js';
|
|
2
|
-
import {
|
|
3
|
-
import { narrowDownloadRegion } from './regions.js';
|
|
3
|
+
import { MarketSettingsSchema, readMcpBackend } from './mcp-backend.js';
|
|
4
4
|
export class MarketSettingsNamespace {
|
|
5
5
|
ctx;
|
|
6
6
|
dataRoot;
|
|
@@ -28,7 +28,7 @@ export class MarketSettingsNamespace {
|
|
|
28
28
|
}
|
|
29
29
|
/** The persisted MCP backend choice; the built-in client until registration lands. */
|
|
30
30
|
async backend() {
|
|
31
|
-
return this.
|
|
31
|
+
return this.settings().mcpEnhanced ? 'builtin' : 'host';
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Persist a backend choice.
|
|
@@ -41,7 +41,17 @@ export class MarketSettingsNamespace {
|
|
|
41
41
|
}
|
|
42
42
|
/** The persisted download-region setting. */
|
|
43
43
|
async downloadRegion() {
|
|
44
|
-
return
|
|
44
|
+
return this.settings().downloadRegion;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The resolved settings, whether or not the namespace is registered yet.
|
|
48
|
+
*
|
|
49
|
+
* Every reader goes through here so "the field is absent" has exactly one
|
|
50
|
+
* answer — {@link resolveMarketSettings} — instead of each caller inventing
|
|
51
|
+
* its own fallback for the window before registration lands.
|
|
52
|
+
*/
|
|
53
|
+
settings() {
|
|
54
|
+
return resolveMarketSettings(this.scope?.get());
|
|
45
55
|
}
|
|
46
56
|
/** Release every watcher and unmount the feedback tool. */
|
|
47
57
|
dispose() {
|
|
@@ -54,9 +64,7 @@ export class MarketSettingsNamespace {
|
|
|
54
64
|
try {
|
|
55
65
|
this.ctx.logger?.info?.('[dsh-agent-plugins-market] settings inject resolved — registering namespace');
|
|
56
66
|
const settings = settingsCtx.settings;
|
|
57
|
-
const scope = settings.register(
|
|
58
|
-
base: { mcpEnhanced: true, downloadRegion: 'auto', feedbackEnabled: true, scanProjectLayouts: false }
|
|
59
|
-
});
|
|
67
|
+
const scope = settings.register(MARKET_SETTINGS_NAMESPACE, MarketSettingsSchema);
|
|
60
68
|
this.scope = scope;
|
|
61
69
|
this.ctx.logger?.info?.('[dsh-agent-plugins-market] settings namespace registered — plugin-config card will serve');
|
|
62
70
|
this.syncProjectLayouts();
|
|
@@ -68,9 +76,9 @@ export class MarketSettingsNamespace {
|
|
|
68
76
|
if (backend === 'host')
|
|
69
77
|
void scope.update({ mcpEnhanced: false }).catch(() => { });
|
|
70
78
|
});
|
|
71
|
-
let previousBackend =
|
|
79
|
+
let previousBackend = this.settings().mcpEnhanced;
|
|
72
80
|
this.watchers.push(scope.watch(() => {
|
|
73
|
-
const backend =
|
|
81
|
+
const backend = this.settings().mcpEnhanced;
|
|
74
82
|
if (backend === previousBackend)
|
|
75
83
|
return;
|
|
76
84
|
previousBackend = backend;
|
|
@@ -89,23 +97,23 @@ export class MarketSettingsNamespace {
|
|
|
89
97
|
this.ctx.logger?.error?.(`[dsh-agent-plugins-market] settings namespace registration failed — the plugin-config card will be hidden this boot: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
|
-
/** Apply the background source-update switch
|
|
100
|
+
/** Apply the background source-update switch. */
|
|
93
101
|
syncAutoUpdateSources() {
|
|
94
102
|
try {
|
|
95
|
-
this.host.setAutoUpdateSources(this.
|
|
103
|
+
this.host.setAutoUpdateSources(this.settings().autoUpdateSources);
|
|
96
104
|
}
|
|
97
105
|
catch (error) {
|
|
98
106
|
this.ctx.logger?.error?.(`[dsh-agent-plugins-market] background source update switch failed: ${String(error)}`);
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
109
|
syncProjectLayouts() {
|
|
102
|
-
void this.host.setScanProjectLayouts(this.
|
|
110
|
+
void this.host.setScanProjectLayouts(this.settings().scanProjectLayouts).catch(error => {
|
|
103
111
|
this.ctx.logger?.error?.(`[dsh-agent-plugins-market] project layout reconciliation failed: ${String(error)}`);
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
syncFeedbackTool() {
|
|
107
115
|
try {
|
|
108
|
-
const wanted = this.
|
|
116
|
+
const wanted = this.settings().feedbackEnabled;
|
|
109
117
|
if (!wanted) {
|
|
110
118
|
if (this.feedbackDisposer !== undefined) {
|
|
111
119
|
this.feedbackDisposer();
|