@moonquake2004/dsh-security 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/checks/index.mjs +3 -0
- package/src/checks/sl1-supply-chain.mjs +11 -6
- package/src/checks/sl4-release-compat.mjs +11 -3
- package/src/checks/sp1-dependency-audit.mjs +155 -65
- package/src/checks/sp11-patch-security-override.mjs +132 -0
- package/src/checks/sp12-config-as-code-tag.mjs +125 -0
- package/src/checks/sp13-tools-mode-sandbox.mjs +631 -0
- package/src/checks/sp3-sandbox-consistency.mjs +293 -147
- package/src/checks/sp5-permission-model.mjs +267 -53
- package/src/checks/sp8-dist-tag-health.mjs +16 -0
- package/src/checks/sp9-dual-instance-guard.mjs +241 -57
- package/src/checks/sr1-sandbox-violation.mjs +22 -23
- package/src/checks/sr2-privilege-escalation.mjs +26 -11
- package/src/checks/sr3-data-exfiltration.mjs +6 -14
- package/src/checks/sr4-isolation-verify.mjs +4 -5
- package/src/checks/ss2-pii-exposure.mjs +8 -3
- package/src/checks/ss3-sensitive-output.mjs +9 -8
- package/src/checks/ss4-session-integrity.mjs +20 -18
- package/src/dsh-config.mjs +204 -0
- package/src/index.mjs +3 -0
- package/src/install-tree.mjs +293 -0
- package/src/integrations/ecosystem.mjs +232 -48
- package/src/integrations/index.mjs +72 -24
- package/src/integrations/plugin-reducer.mjs +190 -40
- package/src/integrations/poison-guard.mjs +162 -39
- package/src/integrations/sandbox-audit.mjs +51 -47
- package/src/integrations/tool-exec.mjs +130 -0
- package/src/registry.mjs +3 -0
- package/src/session-reader.mjs +134 -0
|
@@ -1,76 +1,290 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SP5: Permission Model —
|
|
2
|
+
* SP5: Permission Model — 真实的插件能力面(不是不存在的 permissions 字段)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
4
|
+
* 2026-09 上游兼容审计 R7——旧模型(v0.1.7 及以前)的门控是对的,模型是错的:
|
|
5
|
+
* `dsh.bundle = { patch: './cordis.patch.yml' }` 确实存在(值得继续门控),
|
|
6
|
+
* 但它**声称**要建模的 capability surface 并不存在——0.1.5 的插件 manifest
|
|
7
|
+
* **没有 per-plugin `permissions` / `capabilities` 字段**。
|
|
8
|
+
* 旧实现用 `cordis.patch.yml` 正文里的 `tool-fs|str_replace_editor` + `sandbox`
|
|
9
|
+
* 字符串启发式去"检测未声明权限",匹配不到任何真实声明,恒为 PASS。
|
|
10
|
+
*
|
|
11
|
+
* 真实的能力面是三处(全部可读、可机器判定):
|
|
12
|
+
* (a) `dsh.client.inject` + `dsh.client.platform`
|
|
13
|
+
* —— 插件向 client 运行时注入的宿主模块清单(跨进程能力面)
|
|
14
|
+
* (b) `dsh.compatibility.{dsh, dshReleases, profiles}`
|
|
15
|
+
* —— 声明支持的 core 版本范围与被允许的 profile
|
|
16
|
+
* (c) 宿主侧 `permission` settings 命名空间(`$DSH_HOME/settings.yaml`
|
|
17
|
+
* → `permission.defaultPreset`;schema 仅此一键,
|
|
18
|
+
* dsh-permission-presets/lib/index.js:24,121-123)
|
|
19
|
+
* + `@deepseek-ai/dsh-base/cordis.patch.yml` 的
|
|
20
|
+
* `sandbox-policy.config.mode` / `approval.config.policy` / `permission.config.presets`
|
|
21
|
+
*
|
|
22
|
+
* 因此本检查改为**如实报告每个插件声明的能力面,以及哪些声明缺失 = 默认不受约束**:
|
|
23
|
+
* - 声明了 `dsh.client.inject` 但既无 `dsh.compatibility` 也无 profile 限制
|
|
24
|
+
* → 跨进程能力面完全无版本/配置约束(MEDIUM)
|
|
25
|
+
* - `dsh.compatibility.dsh` 范围**排除**了安装闭包实际提供的 core 版本(MEDIUM)
|
|
26
|
+
* - `dsh.compatibility.profiles` 不含当前 profile(MEDIUM)
|
|
27
|
+
* - manifest 里出现 `permissions` / `capabilities` 键:宿主无此 schema,
|
|
28
|
+
* 声明不会被强制执行(MEDIUM,避免把"写了"误当成"受限")
|
|
29
|
+
* - 无法精确判定版本归属时**不猜**(node-semver 缺失 → 近似法只在确定时给结论)
|
|
8
30
|
*
|
|
9
31
|
* Severity: MEDIUM
|
|
10
32
|
* Phase: POST_INSTALL
|
|
11
33
|
*/
|
|
12
34
|
|
|
13
|
-
import { readFileSync, existsSync
|
|
35
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
36
|
import { join } from 'node:path';
|
|
15
37
|
import { Severity } from '../protocol/severity.mjs';
|
|
16
38
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
-
import {
|
|
39
|
+
import { createResult, fail, skip } from '../protocol/check.mjs';
|
|
40
|
+
import {
|
|
41
|
+
listPackageDirs,
|
|
42
|
+
resolveDshHome,
|
|
43
|
+
resolveCoreVersions,
|
|
44
|
+
resolveInstallPrefix,
|
|
45
|
+
resolveSemver,
|
|
46
|
+
checkRange,
|
|
47
|
+
} from '../install-tree.mjs';
|
|
48
|
+
import { readPermissionSettings, readHostSandboxConfig, resolveBasePatch } from '../dsh-config.mjs';
|
|
49
|
+
|
|
50
|
+
/** 宿主 manifest 里**不存在**的能力声明键——出现即说明作者误以为它会被强制 */
|
|
51
|
+
const NONEXISTENT_DECL_KEYS = ['permissions', 'capabilities', 'allowedTools'];
|
|
52
|
+
|
|
53
|
+
/** CLI 安装根候选(用于定位 dsh-base 的 patch 与 semver) */
|
|
54
|
+
function cliRootCandidates(profileDir) {
|
|
55
|
+
const prefix = resolveInstallPrefix(profileDir);
|
|
56
|
+
return [prefix ? join(prefix, 'dsh') : null, prefix].filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 读取宿主侧真实权限面 */
|
|
60
|
+
function readHostSurface(profileDir, options = {}) {
|
|
61
|
+
const dshHome = options.dshHome !== undefined ? options.dshHome : resolveDshHome(profileDir);
|
|
62
|
+
const cliRoots = cliRootCandidates(profileDir);
|
|
63
|
+
const basePatchPath = options.basePatchPath !== undefined
|
|
64
|
+
? options.basePatchPath
|
|
65
|
+
: resolveBasePatch(profileDir, cliRoots);
|
|
66
|
+
return {
|
|
67
|
+
dshHome,
|
|
68
|
+
basePatchPath,
|
|
69
|
+
permission: readPermissionSettings(dshHome),
|
|
70
|
+
sandbox: readHostSandboxConfig(basePatchPath),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function describeHost(host) {
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (host.permission.file && host.permission.present) {
|
|
77
|
+
parts.push(`settings.yaml permission.defaultPreset=${host.permission.defaultPreset ?? '(空)'}`);
|
|
78
|
+
} else {
|
|
79
|
+
parts.push(`settings.yaml 未配置 permission.defaultPreset(宿主按 sandbox/approval 默认值推断 preset)`);
|
|
80
|
+
}
|
|
81
|
+
if (!host.sandbox) {
|
|
82
|
+
parts.push(`未找到 dsh-base/cordis.patch.yml(无法读取 sandbox-policy/approval 实际配置)`);
|
|
83
|
+
return parts.join(';');
|
|
84
|
+
}
|
|
85
|
+
const mode = host.sandbox.sandboxMode;
|
|
86
|
+
const approval = host.sandbox.approvalPolicy;
|
|
87
|
+
const modeText = mode
|
|
88
|
+
? (mode.expression
|
|
89
|
+
? `!!js ${mode.expression}${mode.literal ? `(默认字面量 ${mode.literal})` : ''}`
|
|
90
|
+
: mode.literal)
|
|
91
|
+
: '(未声明)';
|
|
92
|
+
const apprText = approval
|
|
93
|
+
? (approval.expression && !approval.decisive ? '由沙箱模式派生(danger-full-access → never,否则 ask)' : (approval.literal ?? '(空)'))
|
|
94
|
+
: '(未声明)';
|
|
95
|
+
parts.push(`sandbox-policy.mode=${modeText}`);
|
|
96
|
+
parts.push(`approval.policy=${apprText}`);
|
|
97
|
+
const presetNames = Object.keys(host.sandbox.presets || {});
|
|
98
|
+
if (presetNames.length > 0) {
|
|
99
|
+
parts.push(`presets=${presetNames.map(n => `${n}{sandbox:${host.sandbox.presets[n].sandbox},approval:${host.sandbox.presets[n].approval}}`).join(' ')}`);
|
|
100
|
+
}
|
|
101
|
+
return parts.join(';');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 单个插件的声明面 */
|
|
105
|
+
function inspectPlugin(pkgJsonPath, displayName, ctx) {
|
|
106
|
+
let pkg;
|
|
107
|
+
try { pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); } catch { return null; }
|
|
108
|
+
const dsh = pkg && pkg.dsh;
|
|
109
|
+
if (!dsh || typeof dsh !== 'object') return null;
|
|
110
|
+
|
|
111
|
+
const declared = {
|
|
112
|
+
name: pkg.name || displayName,
|
|
113
|
+
bundlePatch: null,
|
|
114
|
+
clientInject: [],
|
|
115
|
+
clientPlatform: null,
|
|
116
|
+
compatDsh: null,
|
|
117
|
+
compatReleases: [],
|
|
118
|
+
compatProfiles: [],
|
|
119
|
+
unknownKeys: [],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (dsh.bundle && dsh.bundle.patch) declared.bundlePatch = String(dsh.bundle.patch);
|
|
123
|
+
if (dsh.client && typeof dsh.client === 'object') {
|
|
124
|
+
if (Array.isArray(dsh.client.inject)) declared.clientInject = dsh.client.inject.filter(x => typeof x === 'string');
|
|
125
|
+
if (typeof dsh.client.platform === 'string') declared.clientPlatform = dsh.client.platform;
|
|
126
|
+
}
|
|
127
|
+
if (dsh.compatibility && typeof dsh.compatibility === 'object') {
|
|
128
|
+
if (typeof dsh.compatibility.dsh === 'string') declared.compatDsh = dsh.compatibility.dsh;
|
|
129
|
+
if (dsh.compatibility.dshReleases && typeof dsh.compatibility.dshReleases === 'object') {
|
|
130
|
+
declared.compatReleases = Object.keys(dsh.compatibility.dshReleases);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(dsh.compatibility.profiles)) {
|
|
133
|
+
declared.compatProfiles = dsh.compatibility.profiles.filter(x => typeof x === 'string');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const k of NONEXISTENT_DECL_KEYS) {
|
|
137
|
+
if (Object.prototype.hasOwnProperty.call(dsh, k)) declared.unknownKeys.push(k);
|
|
138
|
+
}
|
|
18
139
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
140
|
+
const findings = [];
|
|
141
|
+
const dshRoot = ctx.coreVersions['@deepseek-ai/dsh'];
|
|
142
|
+
const dshBase = ctx.coreVersions['@deepseek-ai/dsh-base'];
|
|
143
|
+
// 以 profile 侧安装闭包实际提供的版本为准(dsh-base 是 profile 的 bundle 根),
|
|
144
|
+
// CLI 自身那份仅作兜底——两者同属安装闭包,都会在诊断里如实列出。
|
|
145
|
+
const provided = dshBase?.version || dshRoot?.version || null;
|
|
146
|
+
|
|
147
|
+
if (declared.unknownKeys.length > 0) {
|
|
148
|
+
findings.push({
|
|
149
|
+
severity: 'medium',
|
|
150
|
+
type: 'unrecognized-capability-declaration',
|
|
151
|
+
detail: `${declared.name} 的 manifest 声明了 dsh.${declared.unknownKeys.join('/dsh.')}——` +
|
|
152
|
+
`该字段在 0.1.5 宿主 manifest schema 中不存在,声明不会被强制执行(若作者以此认定权限已受限,则属于误信)`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (declared.compatDsh && provided) {
|
|
157
|
+
const r = checkRange(provided, declared.compatDsh, ctx.semverMod);
|
|
158
|
+
if (r.satisfies === false) {
|
|
159
|
+
findings.push({
|
|
160
|
+
severity: 'medium',
|
|
161
|
+
type: 'declared-core-range-excludes-provided',
|
|
162
|
+
detail: `${declared.name} 声明 dsh 兼容范围「${declared.compatDsh}」,` +
|
|
163
|
+
`但安装闭包实际提供 ${provided}${r.exact ? '' : '(近似判定,node-semver 不可用)'}——该插件未声明支持当前 core 版本`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (declared.compatProfiles.length > 0 && ctx.profileName && !declared.compatProfiles.includes(ctx.profileName)) {
|
|
169
|
+
findings.push({
|
|
170
|
+
severity: 'medium',
|
|
171
|
+
type: 'profile-not-declared',
|
|
172
|
+
detail: `${declared.name} 的 dsh.compatibility.profiles=[${declared.compatProfiles.join(', ')}] 不含当前 profile「${ctx.profileName}」`,
|
|
173
|
+
});
|
|
24
174
|
}
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
175
|
+
|
|
176
|
+
if (declared.clientInject.length > 0 && !declared.compatDsh && declared.compatProfiles.length === 0) {
|
|
177
|
+
findings.push({
|
|
178
|
+
severity: 'medium',
|
|
179
|
+
type: 'cross-process-surface-unconstrained',
|
|
180
|
+
detail: `${declared.name} 向 client 运行时注入 ${declared.clientInject.length} 个宿主模块` +
|
|
181
|
+
`(${declared.clientInject.slice(0, 4).join(', ')}${declared.clientInject.length > 4 ? ' …' : ''}),` +
|
|
182
|
+
`却既无 dsh.compatibility.dsh 也无 profiles 限制——该跨进程能力面对 core 版本/配置完全无约束声明`,
|
|
183
|
+
});
|
|
28
184
|
}
|
|
29
|
-
|
|
185
|
+
|
|
186
|
+
if (!declared.bundlePatch && !dsh.client) {
|
|
187
|
+
// 无 bundle patch 且无 client 注入:该包不是可装载插件,不参与能力面统计
|
|
188
|
+
return { declared, findings, plugin: false };
|
|
189
|
+
}
|
|
190
|
+
return { declared, findings, plugin: true };
|
|
30
191
|
}
|
|
31
192
|
|
|
32
|
-
export async function run(profileDir) {
|
|
193
|
+
export async function run(profileDir, options = {}) {
|
|
33
194
|
const id = 'SP5';
|
|
34
195
|
const nmDir = join(profileDir, 'node_modules');
|
|
35
|
-
if (!existsSync(nmDir))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
for (const entry of readdirSync(nmDir, { withFileTypes: true })) {
|
|
39
|
-
if (entry.name.startsWith('.') || entry.name === '.bin') continue;
|
|
40
|
-
// scoped 包(@scope/pkg):复审修复——此前把 @scope 当包名拼路径,scoped 插件全部漏扫
|
|
41
|
-
if (entry.isDirectory() && entry.name.startsWith('@')) {
|
|
42
|
-
const scopeDir = join(nmDir, entry.name);
|
|
43
|
-
let pkgs = [];
|
|
44
|
-
try { pkgs = readdirSync(scopeDir, { withFileTypes: true }); } catch { continue; }
|
|
45
|
-
for (const pkg of pkgs) {
|
|
46
|
-
if (!pkg.isDirectory()) continue;
|
|
47
|
-
issues.push(...inspectPackage(join(scopeDir, pkg.name), join(entry.name, pkg.name)));
|
|
48
|
-
}
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (!entry.isDirectory()) continue;
|
|
52
|
-
issues.push(...inspectPackage(join(nmDir, entry.name), entry.name));
|
|
196
|
+
if (!existsSync(nmDir)) {
|
|
197
|
+
return skip(id, Severity.MEDIUM,
|
|
198
|
+
`无 node_modules(${nmDir}),未执行插件能力面审计——无法读取任何 manifest 声明`);
|
|
53
199
|
}
|
|
54
200
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
201
|
+
const host = readHostSurface(profileDir, options);
|
|
202
|
+
// 让 SP9/安装树解析出的实际 profile 名参与 profiles 一致性判定
|
|
203
|
+
const layoutName = (() => {
|
|
204
|
+
const m = /[/\\]profiles[/\\]([^/\\]+)$/.exec(String(profileDir));
|
|
205
|
+
return m ? m[1] : null;
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
const ctx = {
|
|
209
|
+
coreVersions: resolveCoreVersions(profileDir),
|
|
210
|
+
semverMod: options.semverMod !== undefined
|
|
211
|
+
? options.semverMod
|
|
212
|
+
: (resolveSemver(profileDir, cliRootCandidates(profileDir))?.mod ?? null),
|
|
213
|
+
profileName: options.profileName !== undefined ? options.profileName : layoutName,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const dirs = listPackageDirs(nmDir);
|
|
217
|
+
const plugins = [];
|
|
218
|
+
const findings = [];
|
|
219
|
+
for (const entry of dirs) {
|
|
220
|
+
const pkgJson = join(entry.dir, 'package.json');
|
|
221
|
+
if (!existsSync(pkgJson)) continue;
|
|
222
|
+
const res = inspectPlugin(pkgJson, entry.name, ctx);
|
|
223
|
+
if (!res) continue;
|
|
224
|
+
if (res.plugin) plugins.push(res.declared);
|
|
225
|
+
findings.push(...res.findings);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (plugins.length === 0) {
|
|
229
|
+
return skip(id, Severity.MEDIUM,
|
|
230
|
+
`${nmDir} 下未找到任何带 dsh 声明的插件包(0 个),未执行能力面审计——无法判定声明覆盖情况` +
|
|
231
|
+
`\n宿主侧真实权限面: ${describeHost(host)}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const declaredCount = plugins.filter(p => p.compatDsh || p.compatProfiles.length > 0).length;
|
|
235
|
+
const unconstrained = plugins.filter(p => !p.compatDsh && p.compatProfiles.length === 0);
|
|
236
|
+
const hostLine = `宿主侧真实权限面: ${describeHost(host)}`;
|
|
237
|
+
const envOverride = ['DSH_PERMISSION_MODE', 'DSH_TOOLS_MODE'].filter(k => process.env[k]);
|
|
238
|
+
const envLine = envOverride.length > 0
|
|
239
|
+
? `当前进程环境覆盖: ${envOverride.map(k => `${k}=${process.env[k]}`).join(', ')}(运行期可覆盖上面 written config)`
|
|
240
|
+
: `当前进程无 DSH_PERMISSION_MODE / DSH_TOOLS_MODE 覆盖`;
|
|
241
|
+
const coreLine = `安装闭包提供: @deepseek-ai/dsh=${ctx.coreVersions['@deepseek-ai/dsh']?.version || '?'}` +
|
|
242
|
+
`, dsh-base=${ctx.coreVersions['@deepseek-ai/dsh-base']?.version || '?'}` +
|
|
243
|
+
(ctx.semverMod ? '(版本范围判定用 node-semver)' : '(node-semver 不可用,仅近似判定,不确定一律不报)');
|
|
244
|
+
|
|
245
|
+
const perPlugin = plugins.map(p => {
|
|
246
|
+
const bits = [];
|
|
247
|
+
if (p.bundlePatch) bits.push(`bundle.patch=${p.bundlePatch}`);
|
|
248
|
+
if (p.clientInject.length > 0) bits.push(`client.inject=${p.clientInject.length}${p.clientPlatform ? `/${p.clientPlatform}` : ''}`);
|
|
249
|
+
else if (p.clientPlatform) bits.push(`client.platform=${p.clientPlatform}`);
|
|
250
|
+
if (p.compatDsh) bits.push(`compat.dsh=${p.compatDsh}`);
|
|
251
|
+
else if (p.compatReleases.length > 0) bits.push(`compat.dshReleases=${p.compatReleases.length}`);
|
|
252
|
+
if (p.compatProfiles.length > 0) bits.push(`compat.profiles=[${p.compatProfiles.join(',')}]`);
|
|
253
|
+
if (p.unknownKeys.length > 0) bits.push(`dsh.${p.unknownKeys.join('/dsh.')}=(宿主无此 schema)`);
|
|
254
|
+
const constrained = p.compatDsh || p.compatProfiles.length > 0;
|
|
255
|
+
return ` ${p.name}: ${bits.join(' ') || '(仅 dsh 存在,无声明字段)'}${constrained ? '' : ' ← 无兼容/配置约束声明'}`;
|
|
256
|
+
}).join('\n');
|
|
257
|
+
|
|
258
|
+
const summaryLine =
|
|
259
|
+
`插件能力面: 共 ${plugins.length} 个带 dsh 声明的插件,其中 ${declaredCount} 个声明了 compat.dsh/profiles,` +
|
|
260
|
+
`${unconstrained.length} 个未做任何兼容/配置约束声明` +
|
|
261
|
+
(unconstrained.length > 0 ? `(${unconstrained.slice(0, 6).map(p => p.name).join(', ')}${unconstrained.length > 6 ? ' …' : ''})` : '');
|
|
262
|
+
|
|
263
|
+
const body = `${summaryLine}\n${hostLine}\n${envLine}\n${coreLine}\n逐插件声明:\n${perPlugin}`;
|
|
264
|
+
|
|
265
|
+
if (findings.length === 0) {
|
|
266
|
+
return createResult(id, true, Severity.MEDIUM,
|
|
267
|
+
`未发现能力面声明问题(未声明 ≠ 已受限:宿主默认按 preset/sandbox 配置执行)\n${body}`);
|
|
69
268
|
}
|
|
70
269
|
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
270
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
271
|
+
const top = findings.reduce((a, b) => (order[b.severity] < order[a.severity] ? b : a), findings[0]);
|
|
272
|
+
const detail = findings.slice(0, 12).map(f => `[${f.severity}] ${f.type}: ${f.detail}`).join('\n');
|
|
273
|
+
const more = findings.length > 12 ? `\n…另有 ${findings.length - 12} 项` : '';
|
|
274
|
+
return fail(id, top.severity,
|
|
275
|
+
`检测到 ${findings.length} 项能力面声明问题:\n${detail}${more}\n${body}`,
|
|
276
|
+
'插件应在 manifest 里声明 dsh.compatibility.{dsh,profiles}(真实存在的字段);' +
|
|
277
|
+
'不要写 dsh.permissions/dsh.capabilities(宿主无此 schema,不会被强制);' +
|
|
278
|
+
'宿主侧用 settings.yaml 的 permission.defaultPreset 与 dsh-base 的 sandbox-policy/approval 收紧默认档位'
|
|
279
|
+
);
|
|
74
280
|
}
|
|
75
281
|
|
|
76
|
-
export const sp5Check = {
|
|
282
|
+
export const sp5Check = {
|
|
283
|
+
id: 'SP5',
|
|
284
|
+
name: 'permission-model',
|
|
285
|
+
severity: Severity.MEDIUM,
|
|
286
|
+
phase: CheckPhase.POST_INSTALL,
|
|
287
|
+
description: '插件能力面审计——读真实的 dsh.bundle / dsh.client.inject / dsh.compatibility 与宿主 permission/sandbox 配置(宿主无 per-plugin permissions 字段)',
|
|
288
|
+
src: 'builtin',
|
|
289
|
+
runner: (d) => run(d),
|
|
290
|
+
};
|
|
@@ -116,6 +116,22 @@ export async function run(profileDir) {
|
|
|
116
116
|
`dist-tag 健康检查通过:${plugins.length} 个插件 / ${cache.size} 个 @deepseek-ai/dsh-* 包的 latest 均正常`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// 影响门控(2026-09 审计):0.1.5 起 profile **不再从 registry 安装** @deepseek-ai/* ——
|
|
120
|
+
// 实例来自 CLI 闭包(~/.dsh/profiles/node_modules/@deepseek-ai 是 dsh 自建的 symlink 镜像)。
|
|
121
|
+
// 此时 latest 卡在旧版不会影响已装实例,故只在 profile 里存在**真实目录**安装时才判为问题。
|
|
122
|
+
let installsFromRegistry = false;
|
|
123
|
+
try {
|
|
124
|
+
for (const e of readdirSync(join(profileDir, 'node_modules', '@deepseek-ai'), { withFileTypes: true })) {
|
|
125
|
+
if (e.isDirectory() && !e.isSymbolicLink()) { installsFromRegistry = true; break; }
|
|
126
|
+
}
|
|
127
|
+
} catch { /* 目录不存在 → 不从 registry 安装 */ }
|
|
128
|
+
if (!installsFromRegistry) {
|
|
129
|
+
return pass(id, Severity.HIGH,
|
|
130
|
+
`registry 的 @deepseek-ai/* latest 标签确有异常(${affected.length} 处 plugin×peer 版本对,涉及 ${new Set(affected.map((a) => a.plugin)).size} 个插件),`
|
|
131
|
+
+ `但本 profile 的 @deepseek-ai/* 来自 CLI 闭包而非 registry 安装 → **对已装实例无影响**;`
|
|
132
|
+
+ `仅在你日后执行未固定版本的 pnpm add 时会踩到(建议显式 pin 版本)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
const details = affected.slice(0, 15).map(a =>
|
|
120
136
|
` ${a.plugin} — peer ${a.peerPkg} ${a.range} → latest=${a.brokenLatest}(broken)`
|
|
121
137
|
).join('\n');
|