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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* notification path — have a single owner.
|
|
9
9
|
*/
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { MARKET_SETTINGS_DEFAULTS } from '../contracts/settings.js';
|
|
11
12
|
import { STATE_FILE_NAME } from '../catalog/paths.js';
|
|
12
13
|
import { settlesWithin } from '../runtime/deadline.js';
|
|
13
14
|
import { EMPTY_STATE, loadState, saveState } from '../runtime/state-store.js';
|
|
@@ -27,7 +28,13 @@ export class CatalogContext {
|
|
|
27
28
|
mutationQueue = Promise.resolve();
|
|
28
29
|
currentRevision = 0;
|
|
29
30
|
currentScanGeneration = 0;
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The project-layout switch in effect. The host settings document owns the
|
|
33
|
+
* value and pushes it here on registration and on every change; the initial
|
|
34
|
+
* value is the contract's declared default, which is what the catalog would
|
|
35
|
+
* read from the document before the settings service resolves.
|
|
36
|
+
*/
|
|
37
|
+
scanProjectLayoutsEnabled = MARKET_SETTINGS_DEFAULTS.scanProjectLayouts;
|
|
31
38
|
/** The derived refresh in flight, shared by every change that lands while it runs. */
|
|
32
39
|
refreshPass;
|
|
33
40
|
/** Whether a change landed after the running pass started; it earns one more pass. */
|
|
@@ -48,6 +48,10 @@ export class Catalog {
|
|
|
48
48
|
get userRoot() {
|
|
49
49
|
return this.context.userRoot;
|
|
50
50
|
}
|
|
51
|
+
/** The plugin storage root holding per-suite `${PLUGIN_DATA}` directories and overrides. */
|
|
52
|
+
get dataRoot() {
|
|
53
|
+
return this.context.dataRoot;
|
|
54
|
+
}
|
|
51
55
|
get sources() {
|
|
52
56
|
return this.context.state.sources;
|
|
53
57
|
}
|
|
@@ -222,13 +226,17 @@ export class Catalog {
|
|
|
222
226
|
async addMcpServer(name, server) {
|
|
223
227
|
await this.mcp.addServer(name, server);
|
|
224
228
|
}
|
|
229
|
+
/** Import several pasted services in one write; each entry reports its own outcome. */
|
|
230
|
+
async importMcpServers(servers, overwrite) {
|
|
231
|
+
return this.mcp.importServers(servers, overwrite);
|
|
232
|
+
}
|
|
225
233
|
/** Read effective service configuration without exposing credential literals. */
|
|
226
234
|
async serverConfig(kind, id) {
|
|
227
235
|
return this.mcp.serverConfig(kind, id);
|
|
228
236
|
}
|
|
229
237
|
/** Validate a complete replacement before writing; plugin checkouts remain untouched. */
|
|
230
|
-
async saveServerConfig(kind, id, config) {
|
|
231
|
-
await this.mcp.saveServerConfig(kind, id, config);
|
|
238
|
+
async saveServerConfig(kind, id, config, policy) {
|
|
239
|
+
await this.mcp.saveServerConfig(kind, id, config, policy);
|
|
232
240
|
}
|
|
233
241
|
/** One suite's persisted MCP overrides, addressed by qualified suite id. */
|
|
234
242
|
async mcpOverrides(sourceId, suiteId) {
|
|
@@ -238,6 +246,23 @@ export class Catalog {
|
|
|
238
246
|
async setMcpOverride(sourceId, suiteId, serverKey, override) {
|
|
239
247
|
await this.mcp.setOverride(sourceId, suiteId, serverKey, override);
|
|
240
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Enable or disable one declared MCP server, addressed by the same
|
|
251
|
+
* source-qualified suite id its status row carries. Disabling writes an
|
|
252
|
+
* override rather than editing the suite's `mcp.json`, so a refresh cannot
|
|
253
|
+
* clobber the user's choice.
|
|
254
|
+
*/
|
|
255
|
+
async setMcpServerEnabled(suiteKey, serverKey, enabled) {
|
|
256
|
+
await this.mcp.setServerEnabled(suiteKey, serverKey, enabled);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Allow or deny one tool of a declared MCP server, addressed by the same
|
|
260
|
+
* source-qualified suite id its status row carries. The denial lands in the
|
|
261
|
+
* override record, so the suite's own `mcp.json` stays source-owned.
|
|
262
|
+
*/
|
|
263
|
+
async setMcpServerToolEnabled(suiteKey, serverKey, tool, enabled) {
|
|
264
|
+
await this.mcp.setServerToolEnabled(suiteKey, serverKey, tool, enabled);
|
|
265
|
+
}
|
|
241
266
|
/** Re-run the MCP reconcile pass without changing any catalog state. */
|
|
242
267
|
async retryMounts() {
|
|
243
268
|
await this.mcp.retryMounts();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Suite detail and preview projections owned by the application layer. */
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { discoverLspEntries } from '../catalog/surfaces.js';
|
|
5
5
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
@@ -7,6 +7,16 @@ import { credentialRefsInServer } from '../runtime/mcp-config.js';
|
|
|
7
7
|
import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
8
8
|
import { applyOverride } from '../runtime/mcp-overrides.js';
|
|
9
9
|
import { effectiveSurfaces } from '../model/types.js';
|
|
10
|
+
/** The checkout's last modification, or null when the path cannot be read. */
|
|
11
|
+
async function rootModifiedAt(root) {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await stat(root);
|
|
14
|
+
return new Date(stats.mtimeMs).toISOString();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
/** Build the detail response for one normalized suite. */
|
|
11
21
|
export async function buildSuiteDetail(suite, installed, diagnostics, mcpOverrides = {}) {
|
|
12
22
|
const remoteUrl = suite.remote?.url;
|
|
@@ -18,6 +28,7 @@ export async function buildSuiteDetail(suite, installed, diagnostics, mcpOverrid
|
|
|
18
28
|
description: suite.manifest.description ?? null,
|
|
19
29
|
author: suite.manifest.author ?? null,
|
|
20
30
|
keywords: suite.manifest.keywords ?? [],
|
|
31
|
+
updatedAt: await rootModifiedAt(suite.root),
|
|
21
32
|
layout: suite.manifest.layout,
|
|
22
33
|
dimension: suite.dimension,
|
|
23
34
|
root: remoteUrl ?? suite.root,
|
|
@@ -66,7 +77,7 @@ export async function readSkillContent(suite, skillName) {
|
|
|
66
77
|
content = await readFile(skill.file, 'utf8');
|
|
67
78
|
}
|
|
68
79
|
catch (error) {
|
|
69
|
-
throw new Error(`skill file unreadable: ${error instanceof Error ? error.message : String(error)}
|
|
80
|
+
throw new Error(`skill file unreadable: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
70
81
|
}
|
|
71
82
|
return { name: skill.name, description: skill.description, content, path: skill.file };
|
|
72
83
|
}
|
|
@@ -77,7 +77,7 @@ export class InstallStore {
|
|
|
77
77
|
await this.context.notifyChanged(true);
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
-
/** Append
|
|
80
|
+
/** Append seeded sources missing from user state and persist them. */
|
|
81
81
|
async mergeSources(sources) {
|
|
82
82
|
const existing = new Set(this.context.state.sources.map(source => source.id));
|
|
83
83
|
const additions = sources.filter(source => !existing.has(source.id));
|
|
@@ -11,13 +11,17 @@ import { isDirectory } from '../catalog/fs-probes.js';
|
|
|
11
11
|
import { discoverSuitesInSource } from '../catalog/suite-scanner.js';
|
|
12
12
|
import { readLocalePreference } from '../runtime/host-locale.js';
|
|
13
13
|
import { probeHostMcpClient } from '../runtime/mcp-backend.js';
|
|
14
|
+
import { declaredMcpPolicy, namespaceMcpPolicy, resolveMcpPolicy } from '../runtime/mcp-config.js';
|
|
14
15
|
import { loadLspServers, saveLspServers } from '../runtime/lsp-direct-config.js';
|
|
15
|
-
import { addUserMcpServer, loadUserMcpSuite, USER_MCP_SOURCE, USER_MCP_SUITE } from '../runtime/mcp-direct-config.js';
|
|
16
|
-
import { applyOverride, loadSuiteOverrides, mergeOverridePatch, saveSuiteOverrides } from '../runtime/mcp-overrides.js';
|
|
16
|
+
import { addUserMcpServer, importUserMcpServers, loadUserMcpSuite, USER_MCP_SOURCE, USER_MCP_SUITE } from '../runtime/mcp-direct-config.js';
|
|
17
|
+
import { applyOverride, loadSuiteOverrides, mergeOverridePatch, parseMcpPolicyPatch, saveSuiteOverrides, withoutPolicyFields } from '../runtime/mcp-overrides.js';
|
|
17
18
|
import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
18
19
|
import { buildMcpStatus } from '../runtime/mcp-status.js';
|
|
19
20
|
import { resolveRegion } from '../runtime/regions.js';
|
|
20
21
|
import { applyLspOverrides, lspConfig, restoreRedactedConfig, saveLspOverride, validateServerLsp, validateServerMcp } from '../runtime/server-config.js';
|
|
22
|
+
/** The host compatibility client mounts servers without a startup timeout or tool filters. */
|
|
23
|
+
const HOST_STARTUP_TIMEOUT_UNSUPPORTED = 'the host MCP backend cannot enforce a startup timeout; switch the MCP backend to the built-in client to set one';
|
|
24
|
+
const HOST_TOOL_FILTER_UNSUPPORTED = 'the host MCP backend cannot enforce tool filters; switch the MCP backend to the built-in client to choose tools';
|
|
21
25
|
export class McpService {
|
|
22
26
|
context;
|
|
23
27
|
ports;
|
|
@@ -39,7 +43,9 @@ export class McpService {
|
|
|
39
43
|
entry.kind = 'direct';
|
|
40
44
|
entry.managed = true;
|
|
41
45
|
}
|
|
42
|
-
const
|
|
46
|
+
const backend = await this.backend();
|
|
47
|
+
payload.backend = backend;
|
|
48
|
+
const oauthBackend = backend === 'builtin' && this.reauthorizeAvailable();
|
|
43
49
|
for (const entry of payload.entries) {
|
|
44
50
|
const auth = entry.config?.auth;
|
|
45
51
|
entry.canReauthorize =
|
|
@@ -58,21 +64,71 @@ export class McpService {
|
|
|
58
64
|
await this.context.notifyChanged(true);
|
|
59
65
|
});
|
|
60
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Import several pasted services in one write. Each entry is checked on its
|
|
69
|
+
* own and reports back with its own outcome, so a paste of many services
|
|
70
|
+
* never fails as a block.
|
|
71
|
+
*/
|
|
72
|
+
async importServers(servers, overwrite) {
|
|
73
|
+
if (!Array.isArray(servers))
|
|
74
|
+
throw new Error('servers must be an array');
|
|
75
|
+
const entries = servers.map(entry => {
|
|
76
|
+
if (typeof entry !== 'object' || entry === null)
|
|
77
|
+
throw new Error('each server must be an object');
|
|
78
|
+
const { name, config } = entry;
|
|
79
|
+
if (typeof name !== 'string' || name === '')
|
|
80
|
+
throw new Error('each server needs a name');
|
|
81
|
+
return { name, server: config };
|
|
82
|
+
});
|
|
83
|
+
return this.context.enqueue(async () => {
|
|
84
|
+
const result = await importUserMcpServers(this.context.agentsRoot, entries, overwrite);
|
|
85
|
+
if (result.imported.length > 0)
|
|
86
|
+
await this.context.notifyChanged(true);
|
|
87
|
+
return result;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
61
90
|
/** Read effective service configuration without exposing credential literals. */
|
|
62
91
|
async serverConfig(kind, id) {
|
|
63
92
|
const config = await this.resolveServerConfig(kind, id);
|
|
64
|
-
|
|
93
|
+
const payload = { kind, id, key: config.key, editable: true, config: redactMcpConfig(config.value) };
|
|
94
|
+
if (kind === 'mcp') {
|
|
95
|
+
payload.backend = await this.backend();
|
|
96
|
+
if (config.policy !== undefined)
|
|
97
|
+
payload.policy = serverPolicyPayload(config.policy, config.override, config.declaredAuth);
|
|
98
|
+
}
|
|
99
|
+
return payload;
|
|
65
100
|
}
|
|
66
|
-
/**
|
|
67
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Validate a complete replacement before writing; plugin checkouts remain
|
|
103
|
+
* untouched. `policy` carries what the portable document has no seat for —
|
|
104
|
+
* the two timeouts, the user's tool denials and the OAuth opt-in; an absent
|
|
105
|
+
* field keeps its stored value, `null` clears it.
|
|
106
|
+
*/
|
|
107
|
+
async saveServerConfig(kind, id, config, policy) {
|
|
68
108
|
return this.context.enqueue(async () => {
|
|
69
109
|
const current = await this.resolveServerConfig(kind, id);
|
|
70
110
|
const value = restoreRedactedConfig(config, current.value);
|
|
71
111
|
if (kind === 'mcp') {
|
|
72
|
-
const
|
|
112
|
+
const backend = await this.backend();
|
|
113
|
+
const patch = parseMcpPolicyPatch(policy);
|
|
114
|
+
if (backend === 'host' && patch.startupTimeoutMs !== undefined && patch.startupTimeoutMs !== null)
|
|
115
|
+
throw new Error(HOST_STARTUP_TIMEOUT_UNSUPPORTED);
|
|
116
|
+
if (backend === 'host' && patch.disabledTools !== undefined)
|
|
117
|
+
throw new Error(HOST_TOOL_FILTER_UNSUPPORTED);
|
|
118
|
+
const server = await validateServerMcp(current.root, current.key, value, { userOwned: isUserOwnedMcp(current.suiteKey) });
|
|
73
119
|
const overrides = await loadSuiteOverrides(this.context.dataRoot, current.suiteKey);
|
|
74
|
-
const
|
|
75
|
-
|
|
120
|
+
const existing = overrides[current.key] ?? {};
|
|
121
|
+
// The complete configuration already carries every connection input, so
|
|
122
|
+
// a leftover url/headers/env/args field would shadow what was just
|
|
123
|
+
// saved. Enablement and the policy fields the save did not touch survive.
|
|
124
|
+
const next = { config: server };
|
|
125
|
+
if (existing.enabled !== undefined)
|
|
126
|
+
next.enabled = existing.enabled;
|
|
127
|
+
applyAuth(next, patch.auth, existing.auth);
|
|
128
|
+
applyToolList(next, patch.disabledTools, existing.disabledTools);
|
|
129
|
+
applyTimeout(next, 'toolCallTimeoutMs', patch.toolCallTimeoutMs, existing.toolCallTimeoutMs);
|
|
130
|
+
applyTimeout(next, 'startupTimeoutMs', patch.startupTimeoutMs, existing.startupTimeoutMs);
|
|
131
|
+
overrides[current.key] = next;
|
|
76
132
|
await saveSuiteOverrides(this.context.dataRoot, current.suiteKey, overrides);
|
|
77
133
|
}
|
|
78
134
|
else {
|
|
@@ -104,6 +160,53 @@ export class McpService {
|
|
|
104
160
|
* in the suite's parsed mcp.json.
|
|
105
161
|
*/
|
|
106
162
|
async setOverride(sourceId, suiteId, serverKey, override) {
|
|
163
|
+
if (override !== null && (override.startupTimeoutMs !== undefined || override.disabledTools !== undefined) && (await this.backend()) === 'host')
|
|
164
|
+
throw new Error(override.startupTimeoutMs !== undefined ? HOST_STARTUP_TIMEOUT_UNSUPPORTED : HOST_TOOL_FILTER_UNSUPPORTED);
|
|
165
|
+
// Merge onto the stored record: the UI never receives literal secret
|
|
166
|
+
// values, so a verbatim write would drop keys it simply redacted.
|
|
167
|
+
await this.updateServerOverride(sourceId, suiteId, serverKey, current => (override === null ? null : mergeOverridePatch(current ?? {}, override)));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Enable or disable one declared server, addressed by the source-qualified
|
|
171
|
+
* suite id the status rows carry. The qualified id is split here rather than
|
|
172
|
+
* by callers so the separator stays a server-side detail.
|
|
173
|
+
*/
|
|
174
|
+
async setServerEnabled(suiteKey, serverKey, enabled) {
|
|
175
|
+
const { sourceId, suiteId } = splitSuiteKey(suiteKey);
|
|
176
|
+
await this.setOverride(sourceId, suiteId, serverKey, { enabled });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Allow or deny one tool of a declared server, addressed by the
|
|
180
|
+
* source-qualified suite id the status rows carry. The stored record lists
|
|
181
|
+
* only the tools the user turned off, so a server with nothing denied
|
|
182
|
+
* carries no tool field at all.
|
|
183
|
+
*/
|
|
184
|
+
async setServerToolEnabled(suiteKey, serverKey, tool, enabled) {
|
|
185
|
+
const { sourceId, suiteId } = splitSuiteKey(suiteKey);
|
|
186
|
+
if (tool === '')
|
|
187
|
+
throw new Error('tool name is required');
|
|
188
|
+
if ((await this.backend()) === 'host')
|
|
189
|
+
throw new Error(HOST_TOOL_FILTER_UNSUPPORTED);
|
|
190
|
+
await this.updateServerOverride(sourceId, suiteId, serverKey, current => {
|
|
191
|
+
const denied = new Set(current?.disabledTools ?? []);
|
|
192
|
+
if (enabled)
|
|
193
|
+
denied.delete(tool);
|
|
194
|
+
else
|
|
195
|
+
denied.add(tool);
|
|
196
|
+
const next = { ...(current ?? {}) };
|
|
197
|
+
if (denied.size === 0)
|
|
198
|
+
delete next.disabledTools;
|
|
199
|
+
else
|
|
200
|
+
next.disabledTools = [...denied];
|
|
201
|
+
return Object.keys(next).length === 0 ? null : next;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Rewrite one declared server's override record and remount it. The mutate
|
|
206
|
+
* callback receives the stored record — absent when none is persisted — and
|
|
207
|
+
* returns the replacement, or `null` to drop the record entirely.
|
|
208
|
+
*/
|
|
209
|
+
async updateServerOverride(sourceId, suiteId, serverKey, mutate) {
|
|
107
210
|
return this.context.enqueue(async () => {
|
|
108
211
|
const suiteKey = qualifiedSuiteId(sourceId, suiteId);
|
|
109
212
|
const source = this.context.state.sources.find(entry => entry.id === sourceId);
|
|
@@ -129,16 +232,19 @@ export class McpService {
|
|
|
129
232
|
// Override files are keyed by the qualified id so two sources' same-named
|
|
130
233
|
// suites never share one override record.
|
|
131
234
|
const overrides = await loadSuiteOverrides(this.context.dataRoot, suiteKey);
|
|
132
|
-
|
|
235
|
+
const next = mutate(overrides[serverKey]);
|
|
236
|
+
if (next === null)
|
|
133
237
|
delete overrides[serverKey];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Merge onto the stored record: the UI never receives literal secret
|
|
137
|
-
// values, so a verbatim write would drop keys it simply redacted.
|
|
138
|
-
overrides[serverKey] = mergeOverridePatch(overrides[serverKey] ?? {}, override);
|
|
139
|
-
}
|
|
238
|
+
else
|
|
239
|
+
overrides[serverKey] = next;
|
|
140
240
|
await saveSuiteOverrides(this.context.dataRoot, suiteKey, overrides);
|
|
141
241
|
await this.context.notifyChanged(true);
|
|
242
|
+
// The status surface reads the live tool registry, so an immediate read
|
|
243
|
+
// races the reconciler's teardown: a just-disabled server still carries
|
|
244
|
+
// its observed tools and reports the orphaned state with its switch on
|
|
245
|
+
// until the pass finishes. Joining the pass here is bounded — one stuck
|
|
246
|
+
// mount costs the request its deadline, never the plugin.
|
|
247
|
+
await this.context.refreshSettled();
|
|
142
248
|
});
|
|
143
249
|
}
|
|
144
250
|
/**
|
|
@@ -214,7 +320,11 @@ export class McpService {
|
|
|
214
320
|
}
|
|
215
321
|
return map;
|
|
216
322
|
}
|
|
217
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Resolve one editable server: its key, owning suite, root, and the service
|
|
325
|
+
* document the editor renders. An MCP document carries the portable shape
|
|
326
|
+
* only; the client policy behind it rides `policy`.
|
|
327
|
+
*/
|
|
218
328
|
async resolveServerConfig(kind, id) {
|
|
219
329
|
if (kind === 'lsp' && id.startsWith('direct/')) {
|
|
220
330
|
const key = id.slice(7);
|
|
@@ -230,8 +340,17 @@ export class McpService {
|
|
|
230
340
|
for (const [key, server] of Object.entries(suite.mcp?.servers ?? {})) {
|
|
231
341
|
if (id !== `plugin:${suiteKey}/${key}`)
|
|
232
342
|
continue;
|
|
233
|
-
const
|
|
234
|
-
|
|
343
|
+
const override = (await loadSuiteOverrides(this.context.dataRoot, suiteKey))[key];
|
|
344
|
+
const declared = namespaceMcpPolicy(suite, key);
|
|
345
|
+
return {
|
|
346
|
+
key,
|
|
347
|
+
suiteKey,
|
|
348
|
+
root: suite.root,
|
|
349
|
+
value: { ...withoutPolicyFields(applyOverride(server, override)) },
|
|
350
|
+
policy: resolveMcpPolicy(declaredMcpPolicy(server, declared), override),
|
|
351
|
+
...(override === undefined ? {} : { override }),
|
|
352
|
+
...(declared?.auth?.enabled === undefined ? {} : { declaredAuth: declared.auth.enabled })
|
|
353
|
+
};
|
|
235
354
|
}
|
|
236
355
|
}
|
|
237
356
|
else {
|
|
@@ -244,3 +363,56 @@ export class McpService {
|
|
|
244
363
|
throw new Error('service configuration is not managed by this plugin');
|
|
245
364
|
}
|
|
246
365
|
}
|
|
366
|
+
/** Split one source-qualified suite id; the separator stays a server-side detail. */
|
|
367
|
+
function splitSuiteKey(suiteKey) {
|
|
368
|
+
const separator = suiteKey.indexOf('/');
|
|
369
|
+
if (separator <= 0)
|
|
370
|
+
throw new Error(`invalid suite id "${suiteKey}"`);
|
|
371
|
+
return { sourceId: suiteKey.slice(0, separator), suiteId: suiteKey.slice(separator + 1) };
|
|
372
|
+
}
|
|
373
|
+
/** Project one resolved policy onto the wire shape the editor renders. */
|
|
374
|
+
function serverPolicyPayload(policy, override, declaredAuth) {
|
|
375
|
+
const userDenied = override?.disabledTools ?? null;
|
|
376
|
+
const userAuth = override?.auth?.enabled ?? null;
|
|
377
|
+
const suiteAuth = declaredAuth ?? null;
|
|
378
|
+
return {
|
|
379
|
+
toolCallTimeout: policy.toolCallTimeout,
|
|
380
|
+
startupTimeout: policy.startupTimeout,
|
|
381
|
+
deniedTools: { user: userDenied, suite: policy.suiteDisabledTools ?? null, effective: policy.disabledTools ?? [] },
|
|
382
|
+
auth: { user: userAuth, suite: suiteAuth, effective: userAuth ?? suiteAuth ?? true }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** The user-owned MCP declaration suite: its file is local data, not a package. */
|
|
386
|
+
function isUserOwnedMcp(suiteKey) {
|
|
387
|
+
return suiteKey === `${USER_MCP_SOURCE}/${USER_MCP_SUITE}`;
|
|
388
|
+
}
|
|
389
|
+
/** Write the OAuth opt-in from a save: absent keeps the stored value, `null` clears it. */
|
|
390
|
+
function applyAuth(target, patch, stored) {
|
|
391
|
+
if (patch === undefined) {
|
|
392
|
+
if (stored !== undefined)
|
|
393
|
+
target.auth = stored;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (patch !== null)
|
|
397
|
+
target.auth = patch;
|
|
398
|
+
}
|
|
399
|
+
/** Write the user's deny list: absent keeps it, `null` clears it, entries replace it. */
|
|
400
|
+
function applyToolList(target, patch, stored) {
|
|
401
|
+
if (patch === undefined) {
|
|
402
|
+
if (stored !== undefined)
|
|
403
|
+
target.disabledTools = stored;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (patch !== null && patch.length > 0)
|
|
407
|
+
target.disabledTools = patch;
|
|
408
|
+
}
|
|
409
|
+
/** Write one timeout from a save: absent keeps the stored value, `null` clears it, a number sets it. */
|
|
410
|
+
function applyTimeout(target, field, patch, stored) {
|
|
411
|
+
if (patch === undefined) {
|
|
412
|
+
if (stored !== undefined)
|
|
413
|
+
target[field] = stored;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (patch !== null)
|
|
417
|
+
target[field] = patch;
|
|
418
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Installed suite and user resources share one inventory; paths never come from HTTP callers. */
|
|
2
|
-
import { realpath, unlink } from 'node:fs/promises';
|
|
2
|
+
import { realpath, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
4
4
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
5
|
-
import {
|
|
5
|
+
import { pluginRootOf } from '../catalog/plugin-variables.js';
|
|
6
|
+
import { isWithin, suiteDataDir } from '../catalog/paths.js';
|
|
6
7
|
import { stripFrontmatter } from '../catalog/skills-parse.js';
|
|
7
8
|
import { parseFrontmatterRecord } from '../runtime/user-store.js';
|
|
8
9
|
/**
|
|
@@ -68,11 +69,22 @@ class PanelResources {
|
|
|
68
69
|
rawText,
|
|
69
70
|
content: stripFrontmatter(rawText),
|
|
70
71
|
path: file,
|
|
71
|
-
description: typeof metadata.description === 'string' ? metadata.description : ''
|
|
72
|
+
description: typeof metadata.description === 'string' ? metadata.description : '',
|
|
73
|
+
...(pluginRootOf(suite) === undefined ? {} : { suiteRoot: suite.root, suiteData: suiteDataDir(this.catalog.dataRoot, suite.sourceId, suite.id) })
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
// One pass stamps each entry's own last modification: the panel reads every
|
|
78
|
+
// file's text anyway, and both origins get the same treatment here.
|
|
79
|
+
return await Promise.all(entries.map(async (entry) => {
|
|
80
|
+
try {
|
|
81
|
+
const stats = await stat(entry.path);
|
|
82
|
+
return { ...entry, updatedAt: new Date(stats.mtimeMs).toISOString() };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
76
88
|
}
|
|
77
89
|
async get(id) {
|
|
78
90
|
return (await this.list()).find(entry => (entry.id ?? entry.name) === id);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Resolve read-only project roles from the calling session, never from tool-supplied paths. */
|
|
2
2
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
3
|
+
import { pluginRootOf } from '../catalog/plugin-variables.js';
|
|
4
|
+
import { suiteDataDir } from '../catalog/paths.js';
|
|
3
5
|
import { parseAgentRole } from '../runtime/agent-role-router.js';
|
|
4
6
|
export async function projectAgentRoles(catalog, parent) {
|
|
5
7
|
const cwd = parent?.session?.header?.cwd;
|
|
@@ -10,6 +12,8 @@ export async function projectAgentRoles(catalog, parent) {
|
|
|
10
12
|
for (const suite of snapshot.enabledSuites) {
|
|
11
13
|
if (suite.activeSurfaces.agents === false)
|
|
12
14
|
continue;
|
|
15
|
+
const suiteRoot = pluginRootOf(suite);
|
|
16
|
+
const suiteData = suiteRoot === undefined ? undefined : suiteDataDir(catalog.dataRoot, suite.sourceId, suite.id);
|
|
13
17
|
for (const resource of suite.resources?.agents ?? (await defaultMarkdownResources(suite.root, 'agents'))) {
|
|
14
18
|
const path = resource.file;
|
|
15
19
|
const rawText = await resourceText(resource).catch(error => {
|
|
@@ -27,7 +31,9 @@ export async function projectAgentRoles(catalog, parent) {
|
|
|
27
31
|
rawText,
|
|
28
32
|
title: policy.title ?? resource.name.replace(/\.agent$/, ''),
|
|
29
33
|
description: policy.description ?? `${suite.manifest.name}: ${resource.name}`,
|
|
30
|
-
disabled: policy.disabled
|
|
34
|
+
disabled: policy.disabled,
|
|
35
|
+
...(suiteRoot === undefined ? {} : { suiteRoot }),
|
|
36
|
+
...(suiteData === undefined ? {} : { suiteData })
|
|
31
37
|
});
|
|
32
38
|
}
|
|
33
39
|
catch {
|
package/lib/catalog/archive.js
CHANGED
|
@@ -43,6 +43,21 @@ export async function downloadArchive(url, tempFile, options = {}) {
|
|
|
43
43
|
const response = await fetch(trimmed, { signal: AbortSignal.timeout(timeoutMs), redirect: 'follow' });
|
|
44
44
|
if (!response.ok)
|
|
45
45
|
throw new Error(`archive download failed: HTTP ${response.status} for ${trimmed}`);
|
|
46
|
+
// Fetch follows redirects. The origin was checked above, but the final URL
|
|
47
|
+
// must preserve the HTTPS-only policy as well; otherwise a trusted HTTPS URL
|
|
48
|
+
// could downgrade the payload request to plain HTTP.
|
|
49
|
+
if (options.allowHttp !== true) {
|
|
50
|
+
const finalUrl = response.url === '' ? trimmed : response.url;
|
|
51
|
+
let finalProtocol;
|
|
52
|
+
try {
|
|
53
|
+
finalProtocol = new URL(finalUrl).protocol;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error('archive redirected to an invalid URL');
|
|
57
|
+
}
|
|
58
|
+
if (finalProtocol !== 'https:')
|
|
59
|
+
throw new Error('archive redirected to non-https URL');
|
|
60
|
+
}
|
|
46
61
|
const declared = Number(response.headers.get('content-length') ?? '0');
|
|
47
62
|
if (Number.isFinite(declared) && declared > ARCHIVE_MAX_BYTES) {
|
|
48
63
|
throw new Error(`archive exceeds the ${ARCHIVE_MAX_BYTES} byte limit: ${declared}`);
|
|
@@ -269,7 +284,7 @@ async function assertBoundedTree(root) {
|
|
|
269
284
|
stack.push(path);
|
|
270
285
|
continue;
|
|
271
286
|
}
|
|
272
|
-
let size
|
|
287
|
+
let size;
|
|
273
288
|
try {
|
|
274
289
|
size = (await stat(path)).size;
|
|
275
290
|
}
|