dsh-desktop-kit 2.2.1 → 2.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/CHANGELOG.md +80 -0
- package/README.md +113 -27
- package/client.js +3121 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +99 -0
- package/plugins/dsh-agent-extensions/index.js +167 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +9 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret detection and redaction for the guardrails scanner.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure and local: cached regexes over in-memory text. A
|
|
5
|
+
* finding never carries the raw secret — only a masked placeholder, a rule id
|
|
6
|
+
* and a line/column. `redactText` is the one function every other module uses
|
|
7
|
+
* before a scanned value reaches a log or a tool result.
|
|
8
|
+
*
|
|
9
|
+
* Node built-ins only; no I/O and no network.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Hard cap for one scan; larger inputs are truncated before regex evaluation. */
|
|
13
|
+
const MAX_SCAN_CHARS = 2 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/** Values that look like secrets but are obviously placeholders. */
|
|
16
|
+
const PLACEHOLDER_VALUE =
|
|
17
|
+
/^(?:your|my|some|example|sample|placeholder|replace[_-]?me|changeme|change[_-]?me|dummy|fake|test|todo|secret|password|token|undefined|null|true|false)(?:[_-].*)?$/iu;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Known secret shapes, most specific first. Later rules never re-report a span
|
|
21
|
+
* an earlier rule already claimed, which keeps `sk-ant-…` an Anthropic key and
|
|
22
|
+
* `sk-<32 hex>` a DeepSeek key instead of a generic `sk-…` match.
|
|
23
|
+
*/
|
|
24
|
+
export const SECRET_RULES = [
|
|
25
|
+
{
|
|
26
|
+
id: "private-key",
|
|
27
|
+
severity: "critical",
|
|
28
|
+
label: "PEM private key block",
|
|
29
|
+
pattern: "-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----[\\s\\S]{0,8192}?-----END (?:[A-Z0-9]+ )*PRIVATE KEY-----",
|
|
30
|
+
flags: "gu",
|
|
31
|
+
block: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "anthropic-key",
|
|
35
|
+
severity: "critical",
|
|
36
|
+
label: "Anthropic API key",
|
|
37
|
+
pattern: "\\bsk-ant-[A-Za-z0-9_-]{16,}",
|
|
38
|
+
flags: "gu",
|
|
39
|
+
maskPrefix: 6,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "deepseek-key",
|
|
43
|
+
severity: "critical",
|
|
44
|
+
label: "DeepSeek API key",
|
|
45
|
+
pattern: "\\bsk-[0-9a-f]{32}\\b",
|
|
46
|
+
flags: "gu",
|
|
47
|
+
maskPrefix: 3,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "github-pat",
|
|
51
|
+
severity: "critical",
|
|
52
|
+
label: "GitHub fine-grained token",
|
|
53
|
+
pattern: "\\bgithub_pat_[A-Za-z0-9_]{20,}",
|
|
54
|
+
flags: "gu",
|
|
55
|
+
maskPrefix: 10,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "github-token",
|
|
59
|
+
severity: "critical",
|
|
60
|
+
label: "GitHub token",
|
|
61
|
+
pattern: "\\bgh[pousr]_[A-Za-z0-9]{30,}",
|
|
62
|
+
flags: "gu",
|
|
63
|
+
maskPrefix: 4,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "openai-key",
|
|
67
|
+
severity: "critical",
|
|
68
|
+
label: "OpenAI API key",
|
|
69
|
+
pattern: "\\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}",
|
|
70
|
+
flags: "gu",
|
|
71
|
+
maskPrefix: 3,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "aws-access-key",
|
|
75
|
+
severity: "high",
|
|
76
|
+
label: "AWS access key id",
|
|
77
|
+
pattern: "\\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\\b",
|
|
78
|
+
flags: "gu",
|
|
79
|
+
maskPrefix: 4,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "aws-secret-key",
|
|
83
|
+
severity: "critical",
|
|
84
|
+
label: "AWS secret access key",
|
|
85
|
+
pattern:
|
|
86
|
+
"(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)[ \\t]*[:=][ \\t]*[\"']?([A-Za-z0-9/+=]{32,})[\"']?",
|
|
87
|
+
flags: "gu",
|
|
88
|
+
valueGroup: 1,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "generic-assignment",
|
|
92
|
+
severity: "high",
|
|
93
|
+
label: "high-entropy secret assignment",
|
|
94
|
+
pattern:
|
|
95
|
+
"(?:api[_-]?key|access[_-]?key|secret(?:[_-]?key)?|client[_-]?secret|auth[_-]?token|token|password|passwd|pwd)[ \\t]*[:=][ \\t]*[\"']?([A-Za-z0-9_+/=-]{16,})[\"']?",
|
|
96
|
+
flags: "giu",
|
|
97
|
+
valueGroup: 1,
|
|
98
|
+
entropyMin: 3,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const compiledRules = new Map();
|
|
103
|
+
|
|
104
|
+
function regexFor(rule) {
|
|
105
|
+
const key = `${rule.id}\u0000${rule.pattern}\u0000${rule.flags ?? ""}`;
|
|
106
|
+
const cached = compiledRules.get(key);
|
|
107
|
+
if (cached !== undefined) return cached;
|
|
108
|
+
const flags = new Set((rule.flags ?? "").split(""));
|
|
109
|
+
flags.add("g");
|
|
110
|
+
flags.add("d");
|
|
111
|
+
const regex = new RegExp(rule.pattern, [...flags].join(""));
|
|
112
|
+
compiledRules.set(key, regex);
|
|
113
|
+
return regex;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clip(text) {
|
|
117
|
+
const source = typeof text === "string" ? text : String(text ?? "");
|
|
118
|
+
return source.length <= MAX_SCAN_CHARS ? source : source.slice(0, MAX_SCAN_CHARS);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Shannon entropy in bits per character; low values mark repeated padding. */
|
|
122
|
+
export function shannonEntropy(value) {
|
|
123
|
+
const text = String(value ?? "");
|
|
124
|
+
if (text.length === 0) return 0;
|
|
125
|
+
const counts = new Map();
|
|
126
|
+
for (const character of text) counts.set(character, (counts.get(character) ?? 0) + 1);
|
|
127
|
+
let entropy = 0;
|
|
128
|
+
for (const count of counts.values()) {
|
|
129
|
+
const probability = count / text.length;
|
|
130
|
+
entropy -= probability * Math.log2(probability);
|
|
131
|
+
}
|
|
132
|
+
return entropy;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether a captured value passes the rule's own plausibility filters. */
|
|
136
|
+
function acceptsValue(rule, value) {
|
|
137
|
+
const text = String(value ?? "").trim();
|
|
138
|
+
if (text.length === 0) return false;
|
|
139
|
+
if (rule.block === true) return true;
|
|
140
|
+
if (rule.id === "generic-assignment") {
|
|
141
|
+
if (text.length < 16) return false;
|
|
142
|
+
if (text.startsWith("$") || text.includes("${") || text.includes("<")) return false;
|
|
143
|
+
// An all-caps identifier is an environment-variable reference, not a value.
|
|
144
|
+
if (/^[A-Z][A-Z0-9_]*$/u.test(text)) return false;
|
|
145
|
+
if (PLACEHOLDER_VALUE.test(text)) return false;
|
|
146
|
+
return shannonEntropy(text) >= (rule.entropyMin ?? 3);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function overlaps(spans, start, end) {
|
|
152
|
+
for (const span of spans) {
|
|
153
|
+
if (start < span.end && end > span.start) return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Locate every accepted secret span in `text`.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} text - scanned text.
|
|
162
|
+
* @param {{rules?: object[], maxSpans?: number}} [options] - rules override and span cap.
|
|
163
|
+
* @returns {{rule: object, start: number, end: number, value: string}[]} accepted non-overlapping spans in text order.
|
|
164
|
+
*/
|
|
165
|
+
export function collectSecretSpans(text, options = {}) {
|
|
166
|
+
const rules = Array.isArray(options.rules) ? options.rules : SECRET_RULES;
|
|
167
|
+
const maxSpans = Number.isFinite(options.maxSpans) ? Math.max(1, Number(options.maxSpans)) : 200;
|
|
168
|
+
const source = clip(text);
|
|
169
|
+
const spans = [];
|
|
170
|
+
for (const rule of rules) {
|
|
171
|
+
const regex = regexFor(rule);
|
|
172
|
+
regex.lastIndex = 0;
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = regex.exec(source)) !== null) {
|
|
175
|
+
if (match[0].length === 0) {
|
|
176
|
+
regex.lastIndex += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const group = rule.valueGroup ?? 0;
|
|
180
|
+
const indices = match.indices?.[group];
|
|
181
|
+
if (indices === undefined) continue;
|
|
182
|
+
const [start, end] = indices;
|
|
183
|
+
const value = source.slice(start, end);
|
|
184
|
+
if (!acceptsValue(rule, value)) continue;
|
|
185
|
+
if (overlaps(spans, start, end)) continue;
|
|
186
|
+
spans.push({ rule, start, end, value });
|
|
187
|
+
if (spans.length >= maxSpans) break;
|
|
188
|
+
}
|
|
189
|
+
if (spans.length >= maxSpans) break;
|
|
190
|
+
}
|
|
191
|
+
return spans.sort((left, right) => left.start - right.start);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Masked, log-safe rendering of one detected secret. Never includes the raw value. */
|
|
195
|
+
export function maskSecret(rule, value) {
|
|
196
|
+
const text = String(value ?? "");
|
|
197
|
+
if (rule?.block === true) return "[REDACTED:private-key]";
|
|
198
|
+
const prefixChars = Number(rule?.maskPrefix ?? 0);
|
|
199
|
+
if (prefixChars > 0 && text.length > prefixChars) {
|
|
200
|
+
return `${text.slice(0, prefixChars)}…[redacted ${text.length} chars]`;
|
|
201
|
+
}
|
|
202
|
+
return `[REDACTED:${rule?.id ?? "secret"}]`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function lineStartsOf(source) {
|
|
206
|
+
const starts = [0];
|
|
207
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
208
|
+
if (source.charCodeAt(index) === 10) starts.push(index + 1);
|
|
209
|
+
}
|
|
210
|
+
return starts;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function positionOf(offset, starts) {
|
|
214
|
+
let low = 0;
|
|
215
|
+
let high = starts.length - 1;
|
|
216
|
+
while (low < high) {
|
|
217
|
+
const middle = (low + high + 1) >> 1;
|
|
218
|
+
if (starts[middle] <= offset) low = middle;
|
|
219
|
+
else high = middle - 1;
|
|
220
|
+
}
|
|
221
|
+
return { line: low + 1, column: offset - starts[low] + 1 };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Scan text for secrets. The result contains only masked values, so it is safe
|
|
226
|
+
* to place directly in a tool result or a log line.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} text - text to scan.
|
|
229
|
+
* @param {{maxFindings?: number, includeGeneric?: boolean}} [options] - result cap and generic-assignment switch.
|
|
230
|
+
* @returns {{rule: string, severity: string, label: string, line: number, column: number, masked: string}[]} findings.
|
|
231
|
+
*/
|
|
232
|
+
export function scanTextForSecrets(text, options = {}) {
|
|
233
|
+
const maxFindings = Number.isFinite(options.maxFindings) ? Math.max(1, Number(options.maxFindings)) : 200;
|
|
234
|
+
const includeGeneric = options.includeGeneric !== false;
|
|
235
|
+
const rules = includeGeneric ? SECRET_RULES : SECRET_RULES.filter((rule) => rule.id !== "generic-assignment");
|
|
236
|
+
const spans = collectSecretSpans(text, { rules, maxSpans: maxFindings });
|
|
237
|
+
const limited = clip(text);
|
|
238
|
+
const starts = lineStartsOf(limited);
|
|
239
|
+
return spans.map((span) => {
|
|
240
|
+
const position = positionOf(span.start, starts);
|
|
241
|
+
return {
|
|
242
|
+
rule: span.rule.id,
|
|
243
|
+
severity: span.rule.severity,
|
|
244
|
+
label: span.rule.label,
|
|
245
|
+
line: position.line,
|
|
246
|
+
column: position.column,
|
|
247
|
+
masked: maskSecret(span.rule, span.value),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Replace every detected secret with a typed marker. Used for command
|
|
254
|
+
* previews, scan messages and any text that may leave the Host.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} value - text to redact.
|
|
257
|
+
* @returns {string} text with secret spans replaced by `[REDACTED:<rule>]`.
|
|
258
|
+
*/
|
|
259
|
+
export function redactText(value) {
|
|
260
|
+
const text = typeof value === "string" ? value : String(value ?? "");
|
|
261
|
+
if (text.length === 0) return text;
|
|
262
|
+
const limited = clip(text);
|
|
263
|
+
const tail = text.length > MAX_SCAN_CHARS ? text.slice(MAX_SCAN_CHARS) : "";
|
|
264
|
+
const spans = collectSecretSpans(limited, { maxSpans: 1000 });
|
|
265
|
+
if (spans.length === 0) return `${limited}${tail}`;
|
|
266
|
+
let output = "";
|
|
267
|
+
let cursor = 0;
|
|
268
|
+
for (const span of spans) {
|
|
269
|
+
output += limited.slice(cursor, span.start);
|
|
270
|
+
output += `[REDACTED:${span.rule.id}]`;
|
|
271
|
+
cursor = span.end;
|
|
272
|
+
}
|
|
273
|
+
return `${output}${limited.slice(cursor)}${tail}`;
|
|
274
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-guardrails",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Host guardrails for DSH: deny/ask policy for dangerous shell commands, protected-path write/edit/delete hooks, and a bounded read-only secret/worktree scan with an inspectable JSON policy.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./lib": "./lib/scan.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"lib/**",
|
|
16
|
+
"tools/**",
|
|
17
|
+
"cordis.patch.yml",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"dsh": {
|
|
21
|
+
"manifestVersion": 1,
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# dsh-mcp-manager
|
|
2
|
+
|
|
3
|
+
MCP-серверы для DSH без SDK-зависимости: stdio и streamable HTTP, tools, resources, trust-гейт и премиальный экран в Settings.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Settings UI
|
|
8
|
+
|
|
9
|
+
Откройте **Настройки → MCP**:
|
|
10
|
+
|
|
11
|
+
- живые карточки серверов: статус, транспорт, число tools, ошибка;
|
|
12
|
+
- переключатели «Включён» и «Доверен»;
|
|
13
|
+
- «Проверить» — переподключение и health;
|
|
14
|
+
- «Tools» — список инструментов с описаниями;
|
|
15
|
+
- «Добавить сервер» — пресеты GitHub / PostgreSQL / Filesystem или ручная форма;
|
|
16
|
+
- токен сохраняется в `~/.dsh/mcp.secrets.json` (0600), а в `mcp.json` остаётся только `${secret:…}`;
|
|
17
|
+
- новые tools появляются в **новом чате** после подключения.
|
|
18
|
+
|
|
19
|
+
Пресеты — это один клик плюс токен, если он нужен серверу. Filesystem не требует токена.
|
|
20
|
+
|
|
21
|
+
<p><img src="screenshots/settings-390.png" alt="MCP Settings на 390px" width="390"> <img src="screenshots/modal-390.png" alt="Форма добавления MCP-сервера" width="390"></p>
|
|
22
|
+
|
|
23
|
+
## Конфиг
|
|
24
|
+
|
|
25
|
+
`~/.dsh/mcp.json` — глобальные серверы, стартуют автоматически при `trusted: true`.
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"github": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
33
|
+
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" },
|
|
34
|
+
"trusted": true
|
|
35
|
+
},
|
|
36
|
+
"postgres": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://readonly@localhost/db"],
|
|
39
|
+
"trusted": false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`<project>/.dsh/mcp.json` — проектные серверы. Они **не** стартуют автоматически; доступны через `/mcp` (list/tools/call/resources/read), чтобы не запускать чужой код при открытии репозитория. Проектные серверы дополнительно требуют маркер `.dsh/mcp.trusted`:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
/mcp trust
|
|
49
|
+
/mcp untrust
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `trusted` обязателен для запуска; без него сервер только отображается.
|
|
53
|
+
- `${VAR}` в `env` и `headers` подставляется из окружения; отсутствующая переменная — ошибка, а не пустая строка.
|
|
54
|
+
- `enabled: false` отключает сервер без удаления конфига.
|
|
55
|
+
- `timeoutMs` — 1–120 с.
|
|
56
|
+
- Секреты не логируются.
|
|
57
|
+
|
|
58
|
+
## Команды
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/mcp init
|
|
62
|
+
/mcp list
|
|
63
|
+
/mcp tools github
|
|
64
|
+
/mcp call github search_repositories {"query":"dsh-desktop-kit"}
|
|
65
|
+
/mcp resources postgres
|
|
66
|
+
/mcp read postgres postgres://schema
|
|
67
|
+
/mcp reload
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Глобальные tools регистрируются как `mcp__<server>__<tool>` при старте. Проектные tools не попадают в текущий toolset — для них есть `/mcp call`; после добавления в глобальный конфиг нужен перезапуск DSH.
|
|
71
|
+
|
|
72
|
+
## Ограничения
|
|
73
|
+
|
|
74
|
+
- Нет OAuth: только env/headers. Токены кладите в переменные окружения.
|
|
75
|
+
- Нет UI: управление через `/mcp` и JSON-файлы.
|
|
76
|
+
- Нет prompts и sampling; поддержаны tools/resources.
|
|
77
|
+
- HTTP-транспорт поддерживает JSON и простой SSE-ответ; интерактивный SSE-стрим с серверными запросами не реализован.
|
|
78
|
+
- MCP-сервер — произвольный процесс. Доверяйте только своим конфигам.
|