dsh-m 0.8.4 → 0.9.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/lib/host.js CHANGED
@@ -4,6 +4,7 @@ import { createApiDispatcher } from './core/host-api.js';
4
4
  import { bindLoaderHost } from './core/live-plugin.js';
5
5
  import { createRegistryController } from './core/registry-controller.js';
6
6
  import { wireRegistrySettings, unwrapConfig } from './core/settings-compat.js';
7
+ import { resolveActiveProfile } from './core/active-profile.js';
7
8
  import { registerTools } from './tools.js';
8
9
  import { appExitFromContext, scheduleRestart } from './core/restart.js';
9
10
  const require = createRequire(import.meta.url);
@@ -30,17 +31,21 @@ export const Config = Schema.object({
30
31
  export function apply(ctx, config) {
31
32
  // 卸载前的 live-disable 依赖 loader(skillhub 同款)
32
33
  bindLoaderHost(ctx);
34
+ // 0.9.0 双 profile(ADR-0005):宿主当前 profile 单一事实源——apply 期解析一次,
35
+ // host 生命周期内不可变;GUI/工具/HTTP/缓存分段全部消费同一对象。
36
+ const profile = resolveActiveProfile(ctx);
37
+ ctx.logger?.info?.('dsh-m: active profile %s (dir=%s, source=%s)', profile.name, profile.dir, profile.source);
33
38
  // registry controller:active config / configured / pending / rejected 分离 + generation fence;
34
39
  // tools 与 Host API 共用同一 active config object(apply 原地更新字段,live 生效)。
35
40
  // unwrapConfig:0.1.7 上 volatile 字段是 cosmokit Volatile 引用,先解包成 plain 值再进 controller。
36
- const controller = createRegistryController(unwrapConfig(config));
41
+ const controller = createRegistryController(unwrapConfig(config), { profile: profile.name });
37
42
  // DSH 0.1.2-rc.1 / 0.1.5-rc.1 / 0.1.7-rc.1 / 0.1.7-rc.2 都经 dsh-cmdline 暴露 appExit;
38
43
  // using it avoids guessing the service unit from release-specific cgroups.
39
44
  const appExit = appExitFromContext(ctx);
40
45
  const restart = (port = null) => scheduleRestart(port, { appExit });
41
46
  // 开关委派的官方服务探测(ADR-0001):运行时按服务存在性探测,不判 DSH 版本号
42
47
  const getService = () => ctx.get?.('pluginManager');
43
- registerTools(ctx, controller.config, { restart });
48
+ registerTools(ctx, controller.config, { restart }, profile);
44
49
  // 设置页接线(双代兼容,详见 core/settings-compat.ts):
45
50
  // - ≤0.1.5:settings.register('dshm', …) scope(get/update/watch);
46
51
  // - 0.1.7-rc.1 / rc.2:Config `.volatile()` 字段自动生成设置页(autoGenerate 默认开,无需 configure),
@@ -67,15 +72,21 @@ export function apply(ctx, config) {
67
72
  ctx.logger?.warn?.(err);
68
73
  }
69
74
  });
70
- // 本地 API:单路由 + method 分发(防护与状态映射在 core/host-api.ts)
75
+ // 本地 API:单路由 + method 分发(信任检查与状态映射在 core/host-api.ts)。
76
+ // 0.9.0(ADR-0005):入口信任检查全量委派官方 connection.requestRejection——
77
+ // Desktop 桥剥 Origin 的请求按官方语义(trustedHosts/loopback/cross-site/Origin:null)判定,
78
+ // 服务缺席或抛错一律 fail-closed 403(warn 一次)。
71
79
  ctx.inject(['webServer'], (c) => {
72
80
  const server = c.webServer;
81
+ const rejectRequest = createRequestRejection(ctx);
73
82
  const handleApi = createApiDispatcher({
74
83
  controller,
75
84
  pkg,
85
+ profile,
76
86
  deps: {
77
87
  scheduleRestart: restart,
78
88
  getService,
89
+ rejectRequest,
79
90
  },
80
91
  });
81
92
  server.register({
@@ -87,3 +98,30 @@ export function apply(ctx, config) {
87
98
  });
88
99
  });
89
100
  }
101
+ /** 守卫式官方 requestRejection 委派:缺席/抛错 fail-closed 403(warn 一次,不刷日志)。 */
102
+ function createRequestRejection(ctx) {
103
+ let warned = false;
104
+ const warnOnce = (err) => {
105
+ if (warned)
106
+ return;
107
+ warned = true;
108
+ ctx.logger?.warn?.('dsh-m: 官方 connection.requestRejection 不可用,/dshm 全入口 fail-closed 403(ADR-0005)');
109
+ if (err)
110
+ ctx.logger?.warn?.(err);
111
+ };
112
+ return (req) => {
113
+ try {
114
+ const connection = ctx.get?.('connection');
115
+ const reject = connection?.requestRejection;
116
+ if (typeof reject !== 'function') {
117
+ warnOnce(undefined);
118
+ return 403;
119
+ }
120
+ return reject.call(connection, { headers: req.headers }) ?? undefined;
121
+ }
122
+ catch (err) {
123
+ warnOnce(err);
124
+ return 403;
125
+ }
126
+ };
127
+ }
package/lib/tools.js CHANGED
@@ -4,6 +4,8 @@ import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin
4
4
  import { COMMUNITY_CATEGORY_LABELS } from './core/community.js';
5
5
  import { appExitFromContext, scheduleRestart } from './core/restart.js';
6
6
  import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
7
+ import { resolveActiveProfile, assertWriteAllowed } from './core/active-profile.js';
8
+ import { desktopInstallFromRegistry, desktopToggle } from './core/profile-ops.js';
7
9
  export const CATEGORY_LABELS = {
8
10
  market: '市场',
9
11
  tools: '工具',
@@ -55,7 +57,7 @@ function cloneJson(value) {
55
57
  function summaryOf(state) {
56
58
  return { isDefault: state.isDefault, status: state.status, stale: state.stale };
57
59
  }
58
- export function registerTools(ctx, cfg, deps = {}) {
60
+ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfile(ctx)) {
59
61
  const timeoutMs = cfg.timeoutMs ?? 20_000;
60
62
  const m = {
61
63
  listMarket: deps.listMarket ?? listMarket,
@@ -63,6 +65,8 @@ export function registerTools(ctx, cfg, deps = {}) {
63
65
  installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
64
66
  uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
65
67
  upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
68
+ desktopInstall: deps.desktopInstall ?? desktopInstallFromRegistry,
69
+ desktopToggle: deps.desktopToggle ?? desktopToggle,
66
70
  };
67
71
  const restart = deps.restart ?? ((port = null) => scheduleRestart(port, { appExit: appExitFromContext(ctx) }));
68
72
  const toggle = deps.togglePlugin ?? coreTogglePlugin;
@@ -132,6 +136,8 @@ export function registerTools(ctx, cfg, deps = {}) {
132
136
  namespace: 'host',
133
137
  deadlineMs: SEARCH_CORE_DEADLINE_MS,
134
138
  signal: exec?.signal,
139
+ profileDir: profile.dir,
140
+ profile: profile.name,
135
141
  });
136
142
  // 安装标注唯一来源:listMarket 的单次 profile 快照。
137
143
  // 状态不完整且存在可被误标的条目时 fail-closed——不把未知安装状态呈现成未安装;
@@ -173,7 +179,7 @@ export function registerTools(ctx, cfg, deps = {}) {
173
179
  }));
174
180
  ctx.tools.register(defineTool({
175
181
  name: 'dshm_list',
176
- description: 'List plugins installed in the DSH web profile, annotated with 市场安装/非市场安装, sources, and outdated flags. Use when the user asks what plugins are installed or wants to manage local plugins.',
182
+ description: 'List plugins installed in the current DSH profile (web or desktop), annotated with 市场安装/非市场安装, sources, and outdated flags. Use when the user asks what plugins are installed or wants to manage local plugins.',
177
183
  parameters: {},
178
184
  output: {
179
185
  schema: { type: 'object', additionalProperties: true },
@@ -192,6 +198,8 @@ export function registerTools(ctx, cfg, deps = {}) {
192
198
  namespace: 'host',
193
199
  deadlineMs: INSTALLED_CORE_DEADLINE_MS,
194
200
  signal: exec?.signal,
201
+ profileDir: profile.dir,
202
+ profile: profile.name,
195
203
  });
196
204
  return cloneJson({
197
205
  registry: summaryOf(result.registryState),
@@ -239,6 +247,11 @@ export function registerTools(ctx, cfg, deps = {}) {
239
247
  throw new Error('缺少收录 id');
240
248
  const version = typeof args.version === 'string' && args.version.trim() ? args.version.trim() : undefined;
241
249
  const force = args.force === true;
250
+ // 0.9.0:desktop → 官方 pluginManager 委派(新包);未知 profile → 结构化拒绝
251
+ if (profile.kind !== 'web') {
252
+ assertWriteAllowed(profile, 'install');
253
+ return cloneJson(await m.desktopInstall(id, cfg, { version, forceIncompatible: force, namespace: 'host', profile: profile.name }));
254
+ }
242
255
  try {
243
256
  return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host' }));
244
257
  }
@@ -280,12 +293,17 @@ export function registerTools(ctx, cfg, deps = {}) {
280
293
  throw new Error('缺少 pkg');
281
294
  if (typeof args.enabled !== 'boolean')
282
295
  throw new Error('缺少 enabled(boolean)');
296
+ // 0.9.0:desktop → 官方管理器委派(服务缺席结构化拒绝);web → 既有委派/降级
297
+ if (profile.kind !== 'web') {
298
+ assertWriteAllowed(profile, 'set-enabled');
299
+ return cloneJson(await m.desktopToggle(target, args.enabled, { getService: getService, profileDir: profile.dir }));
300
+ }
283
301
  return cloneJson(await toggle(target, args.enabled, { getService: getService }));
284
302
  },
285
303
  }));
286
304
  ctx.tools.register(defineTool({
287
305
  name: 'dshm_uninstall',
288
- description: 'Uninstall a DSH plugin from the web profile by package name (pkg from dshm_list). Confirm with the user BEFORE calling. Does not delete plugin data; reports leftover paths instead.',
306
+ description: 'Uninstall a DSH plugin from the current DSH profile by package name (pkg from dshm_list). Confirm with the user BEFORE calling. Does not delete plugin data; reports leftover paths instead.',
289
307
  parameters: {
290
308
  pkg: { type: 'string', required: true, description: '包名 from dshm_list, e.g. dsh-web-search' },
291
309
  },
@@ -305,6 +323,7 @@ export function registerTools(ctx, cfg, deps = {}) {
305
323
  const target = String(args.pkg || '').trim();
306
324
  if (!target)
307
325
  throw new Error('缺少 pkg');
326
+ assertWriteAllowed(profile, 'uninstall');
308
327
  return cloneJson(await m.uninstallPlugin(target, cfg, { namespace: 'host' }));
309
328
  },
310
329
  }));
@@ -338,6 +357,8 @@ export function registerTools(ctx, cfg, deps = {}) {
338
357
  namespace: 'host',
339
358
  deadlineMs: INSTALLED_CORE_DEADLINE_MS,
340
359
  signal: exec?.signal,
360
+ profileDir: profile.dir,
361
+ profile: profile.name,
341
362
  });
342
363
  const items = result.items.map((it) => ({
343
364
  pkg: it.pkg,
@@ -385,6 +406,7 @@ export function registerTools(ctx, cfg, deps = {}) {
385
406
  if (!target)
386
407
  throw new Error('缺少 pkg');
387
408
  const force = args.force === true;
409
+ assertWriteAllowed(profile, 'upgrade');
388
410
  try {
389
411
  return cloneJson(await m.upgradePlugin(target, cfg, { forceIncompatible: force, namespace: 'host' }));
390
412
  }
@@ -416,6 +438,7 @@ export function registerTools(ctx, cfg, deps = {}) {
416
438
  }),
417
439
  timeoutMs: 15_000,
418
440
  async execute() {
441
+ assertWriteAllowed(profile, 'restart');
419
442
  return cloneJson(restart(null));
420
443
  },
421
444
  }));
@@ -431,6 +454,7 @@ export function registerTools(ctx, cfg, deps = {}) {
431
454
  'For installed plugins, call dshm_list / dshm_outdated. Upgrade only after the user confirms which one: dshm_upgrade. Uninstall only after confirmation: dshm_uninstall.',
432
455
  'dshm_toggle switches a plugin on/off: confirm first unless the user already said it in the same message (把 skins 关掉 → execute directly); report live vs restart-required accordingly. ',
433
456
  'dshm_restart only after the user agrees to restart; afterwards tell them to refresh once the page recovers.',
457
+ 'dsh-m operates on the current host profile (web or desktop, per ping.profile): on desktop only read-only market + install-new-package + toggle work; upgrade/uninstall/self-upgrade/restart return structured refusals with guidance — relay them instead of retrying.',
434
458
  ].join(' '),
435
459
  });
436
460
  });
@@ -541,6 +565,17 @@ function renderOutdated(out) {
541
565
  return head.join('\n');
542
566
  }
543
567
  function renderUpgrade(out) {
568
+ // 0.8.5 修复:升级工具会命中装后守卫拦截(复用 installEntry,link/file prior 不可回退等),
569
+ // execute 返回 guard 形态结果——旧 render 没有该分支,把拦截渲染成「✅ undefined 已升级」假成功。
570
+ if (out.guard === true) {
571
+ return [
572
+ `⛔ 升级被装后守卫拦截:${out.message ?? ''}`,
573
+ `终态:${out.compensation?.status ?? '—'}(${out.compensation?.note ?? ''})`,
574
+ out.repairBasis ? `修复依据:${out.repairBasis}` : null,
575
+ out.restartSafe === true ? '可以重启 DSH Web。' : '修复后再重启(不要现在一键重启)。',
576
+ '对用户如实说明守卫拦截与终态,不得说「已升级成功」或「已恢复原版本」。',
577
+ ].filter(Boolean).join('\n');
578
+ }
544
579
  const from = out.fromVersion ? `v${out.fromVersion} → ` : '';
545
580
  const to = out.version ? `v${out.version}` : out.sha ? out.sha.slice(0, 7) : '最新';
546
581
  const extra = buildsNote(out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",