dsh-skill-hub 0.3.21 → 0.3.22

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/index.js CHANGED
@@ -2391,6 +2391,15 @@ function isLoopbackRequest(request) {
2391
2391
  return false;
2392
2392
  }
2393
2393
  }
2394
+ /**
2395
+ * Fallback fence for a host that mounts no Connection service (unit tests, and
2396
+ * any composition serving the webserver alone): the historical literal-loopback
2397
+ * check, so a missing service degrades to the previous behaviour instead of an
2398
+ * open or a dead route family.
2399
+ */
2400
+ function loopbackOnlyFence(request) {
2401
+ return isLoopbackRequest(request) ? void 0 : 403;
2402
+ }
2394
2403
  /** One JSON response. */
2395
2404
  function writeJson(res, status, body) {
2396
2405
  const payload = JSON.stringify(body);
@@ -2863,17 +2872,24 @@ function toDetail(skill) {
2863
2872
  //#region src/routes/helpers.ts
2864
2873
  /**
2865
2874
  * Wrap one handler in the shared request fences. Order matches the
2866
- * original per-handler prologue: loopback trust → HTTP method → master
2875
+ * original per-handler prologue: trust fence → HTTP method → master
2867
2876
  * switch → JSON body. The outer catch maps errors onto the JSON error
2868
2877
  * body (preserving RepoFetchError status codes).
2878
+ *
2879
+ * The trust fence is the host's `/api` model (`deps.fence`) whenever the
2880
+ * composition mounts Connection, so a reverse-proxied deployment is judged by
2881
+ * the same authorities dsh already trusts; without that service the historical
2882
+ * literal-loopback check stands in.
2869
2883
  */
2870
2884
  function createRoute(deps, spec) {
2885
+ const fence = deps.fence ?? loopbackOnlyFence;
2871
2886
  return {
2872
2887
  kind: spec.kind ?? "exact",
2873
2888
  path: spec.path,
2874
2889
  handler: async (req, res) => {
2875
- if (!isLoopbackRequest(req)) {
2876
- writeError(res, 403, "forbidden: loopback-only");
2890
+ const rejection = fence(req);
2891
+ if (rejection !== void 0) {
2892
+ writeError(res, rejection, rejection === 401 ? "unauthorized: browser session required" : "forbidden: untrusted authority");
2877
2893
  return;
2878
2894
  }
2879
2895
  if (!spec.methods.includes(req.method)) {
@@ -4507,8 +4523,45 @@ async function sweepOrphanTrash(deps, home, failed) {
4507
4523
  //#endregion
4508
4524
  //#region src/routes.ts
4509
4525
  /**
4526
+ * Build the family's trust fence from the host's `/api` model.
4527
+ *
4528
+ * `ctx.connection.requestRejection` is the documented hook for fencing another
4529
+ * Web route; with no Connection service (a webserver-only composition, or a
4530
+ * unit test) the literal-loopback check stands in, so the fallback is the old
4531
+ * behaviour rather than an open route family.
4532
+ *
4533
+ * The service is read **per request**, not once at mount: plugins start in
4534
+ * composition order, so a one-shot read at `apply()` time would latch the
4535
+ * fallback whenever this plugin happens to come up before Connection. A
4536
+ * persistent absence is almost certainly an upstream composition change rather
4537
+ * than a transient startup order, so the first miss fires `onMissing` once —
4538
+ * without that signal the fallback would silently reproduce issue #17 after
4539
+ * any future upstream rename of the service.
4540
+ * @param read - resolves `ctx.connection`, or `undefined` when absent.
4541
+ * @param onMissing - called once, the first time the service reads absent.
4542
+ * @returns the fence every route in this family runs first.
4543
+ */
4544
+ function hostTrustFence(read, onMissing) {
4545
+ let missingReported = false;
4546
+ return (request) => {
4547
+ const connection = read();
4548
+ if (connection === void 0) {
4549
+ if (!missingReported) {
4550
+ missingReported = true;
4551
+ onMissing?.();
4552
+ }
4553
+ return loopbackOnlyFence(request);
4554
+ }
4555
+ try {
4556
+ return connection.requestRejection(request);
4557
+ } catch {
4558
+ return 403;
4559
+ }
4560
+ };
4561
+ }
4562
+ /**
4510
4563
  * Build every /api/skill-hub route.
4511
- * @param deps - skill registry view + sidecar store.
4564
+ * @param deps - skill registry view + sidecar store + trust fence.
4512
4565
  * @returns the exact-path routes plus the family's 404 catch-all.
4513
4566
  */
4514
4567
  function makeRoutes(deps) {
@@ -5014,7 +5067,7 @@ const CONFIG_FIELDS = [
5014
5067
  /** Order of the announcement section within the tool-guidance band. */
5015
5068
  const SECTION_ORDER = 152;
5016
5069
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
5017
- const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由仅回环可访问。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。", "The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → \"Skills\" is the management page; the plugin's configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with \"check all\" and \"update all\" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; routes are loopback-only. When the user mentions \"skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill\", this plugin is what they mean — collaborate accordingly."].join("\n\n");
5070
+ const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由受 dsh 自身的 /api 信任栅栏保护(回环、监听器绑定地址、--trusted-host 声明的 authority,外加浏览器会话认证),因此反代部署下与 GUI 同一套放行规则。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。", "The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → \"Skills\" is the management page; the plugin's configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with \"check all\" and \"update all\" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; the routes sit behind dsh's own /api trust fence (loopback, the listener's bind address, any --trusted-host authority, plus browser-session authentication), so a reverse-proxied deployment is judged exactly like the GUI around it. When the user mentions \"skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill\", this plugin is what they mean — collaborate accordingly."].join("\n\n");
5018
5071
  /**
5019
5072
  * Mount the skill hub routes and announcement.
5020
5073
  * @param ctx - host plugin context carrying webServer/skills/systemPrompt/settings.
@@ -5105,6 +5158,7 @@ function apply(ctx, config) {
5105
5158
  const disposers = makeRoutes({
5106
5159
  skills: ctx.skills,
5107
5160
  store,
5161
+ fence: hostTrustFence(() => ctx.get("connection"), () => ctx.logger.warn("[dsh-skill-hub] Connection service absent: routes fall back to literal-loopback; reverse-proxy deployments will 403 until it mounts")),
5108
5162
  invalidate: () => {
5109
5163
  providerControl?.invalidate();
5110
5164
  },
@@ -21,7 +21,7 @@ export declare const zhSettings: {
21
21
  readonly 'settings.statsScanMinutes': "自动统计间隔(分钟)";
22
22
  readonly 'settings.statsScanMinutesHint': "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。";
23
23
  readonly 'settings.githubToken': "GitHub Token";
24
- readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅回环可读;留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
24
+ readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
25
25
  readonly 'settings.githubTokenPlaceholder': "ghp_… / github_pat_…(留空为匿名)";
26
26
  readonly 'settings.inherit': "继承";
27
27
  readonly 'settings.overridden': "已覆盖";
@@ -26,7 +26,7 @@ export declare const zh: {
26
26
  readonly 'settings.statsScanMinutes': "自动统计间隔(分钟)";
27
27
  readonly 'settings.statsScanMinutesHint': "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。";
28
28
  readonly 'settings.githubToken': "GitHub Token";
29
- readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅回环可读;留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
29
+ readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
30
30
  readonly 'settings.githubTokenPlaceholder': "ghp_… / github_pat_…(留空为匿名)";
31
31
  readonly 'settings.inherit': "继承";
32
32
  readonly 'settings.overridden': "已覆盖";
@@ -57,7 +57,7 @@ export type HubSettingsValue = {
57
57
  statsWindowDays?: number;
58
58
  /** 自动统计扫描间隔(分钟)。 */
59
59
  statsScanMinutes?: number;
60
- /** GitHub token(明文存本地设置文档,仅回环可读;缺省为匿名)。 */
60
+ /** GitHub token(明文存本地设置文档,仅经 dsh 的 /api 信任栅栏可读;缺省为匿名)。 */
61
61
  githubToken?: string;
62
62
  };
63
63
  /**
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * routes 共享层 · 依赖视图:路由依赖接口、配置解析与可写技能解析(HTTP
3
- * 围栏在 ./http.ts)。从 helpers.ts 原样搬出,行为不变。
3
+ * 围栏在 ./http.ts)。
4
4
  */
5
5
  import type { ServerResponse } from 'node:http';
6
6
  import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill';
7
7
  import { type HubConfig, type WritableRoot } from '../protocol.ts';
8
8
  import { type SkillHubStore } from '../store.ts';
9
+ import { type RouteFence } from './http.ts';
9
10
  /** Sources the hub may toggle (the user-level filesystem roots). */
10
11
  export declare function isWritableSource(source: string): source is WritableRoot;
11
12
  /** Lookup options the hub forwards to the registry (cwd selects project roots). */
@@ -22,6 +23,12 @@ export interface SkillHubRouteDeps {
22
23
  get(name: string, options?: SkillLookupLike): Promise<SkillDefinition | undefined>;
23
24
  };
24
25
  store: SkillHubStore;
26
+ /**
27
+ * Request fence for the whole family: returns the rejection status (401/403)
28
+ * or `undefined` to proceed. Production passes the host's own `/api` fence
29
+ * (`ctx.connection.requestRejection`); absent falls back to literal loopback.
30
+ */
31
+ fence?: RouteFence;
25
32
  /** DSH home override (tests isolate the writable roots; defaults to ~/.dsh). */
26
33
  home?: string;
27
34
  /** Invalidate the registry catalog cache after hub-driven mutations. */
@@ -1,15 +1,15 @@
1
1
  /**
2
- * routes 共享层:请求围栏(回环信任/方法/总开关/JSON 体)、统一错误映射、
3
- * 可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
4
- * 从 routes.ts 原样搬出,行为不变。
5
- * 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(围栏/错误映射/读体)、
2
+ * routes 共享层:请求栅栏(宿主 /api 信任模型 → 方法 → 总开关 → JSON 体)、
3
+ * 统一错误映射、可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
4
+ * 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(栅栏/错误映射/读体)、
6
5
  * ./deps.ts(依赖视图/可写解析)、./collection.ts(集合装配)、
7
6
  * ./catalog-data.ts(目录装配);对外导出面保持不变。
8
7
  */
9
8
  import type { IncomingMessage, ServerResponse } from 'node:http';
10
9
  import type { WebRoute, WebRouteKind } from '@deepseek-ai/dsh-host-webserver';
11
10
  import { type SkillHubRouteDeps } from './deps.ts';
12
- export { MAX_JSON_BODY_BYTES, STORE_ERROR_STATUS, isLoopbackRequest, pathExists, queryParam, readJsonBody, readString, readStrings, writeError, writeJson, writeRouteError, } from './http.ts';
11
+ export { MAX_JSON_BODY_BYTES, STORE_ERROR_STATUS, isLoopbackRequest, loopbackOnlyFence, pathExists, queryParam, readJsonBody, readString, readStrings, writeError, writeJson, writeRouteError, } from './http.ts';
12
+ export type { RouteFence } from './http.ts';
13
13
  export { configOf, disabledGate, homeOf, isWritableSource, resolveWritableSkill, savedOf, } from './deps.ts';
14
14
  export type { SkillHubRouteDeps, SkillLookupLike, WritableSkill, WritableSkillRefusal, WritableSkillResult, } from './deps.ts';
15
15
  export { buildCollections, buildGroups } from './collection.ts';
@@ -45,8 +45,13 @@ export interface RouteSpec {
45
45
  }
46
46
  /**
47
47
  * Wrap one handler in the shared request fences. Order matches the
48
- * original per-handler prologue: loopback trust → HTTP method → master
48
+ * original per-handler prologue: trust fence → HTTP method → master
49
49
  * switch → JSON body. The outer catch maps errors onto the JSON error
50
50
  * body (preserving RepoFetchError status codes).
51
+ *
52
+ * The trust fence is the host's `/api` model (`deps.fence`) whenever the
53
+ * composition mounts Connection, so a reverse-proxied deployment is judged by
54
+ * the same authorities dsh already trusts; without that service the historical
55
+ * literal-loopback check stands in.
51
56
  */
52
57
  export declare function createRoute(deps: SkillHubRouteDeps, spec: RouteSpec): WebRoute;
@@ -1,12 +1,37 @@
1
1
  /**
2
- * routes 共享层 · HTTP 围栏:回环信任检查、JSON 响应与错误映射、请求体读取、
3
- * 查询/字段读取与文件存在性检查。从 helpers.ts 原样搬出,行为不变。
2
+ * routes 共享层 · HTTP 围栏:请求信任栅栏(复用宿主 `/api` 模型,见 RouteFence)、
3
+ * JSON 响应与错误映射、请求体读取、查询/字段读取与文件存在性检查。
4
4
  */
5
5
  import type { IncomingMessage, ServerResponse } from 'node:http';
6
6
  /** Cap on JSON request bodies (toggle/create payloads are tiny). */
7
7
  export declare const MAX_JSON_BODY_BYTES: number;
8
8
  /** Loopback literal check plus browser same-origin markers. */
9
9
  export declare function isLoopbackRequest(request: IncomingMessage): boolean;
10
+ /**
11
+ * The route family's request fence: the HTTP status a request is rejected
12
+ * with (`401` unauthenticated / `403` untrusted authority), or `undefined`
13
+ * when it may reach the handler.
14
+ *
15
+ * dsh owns **one** browser-trust model for the whole `/api` tree — loopback,
16
+ * the listener's own bind address, every `--trusted-host` authority, then the
17
+ * browser session — and publishes it as `ctx.connection.requestRejection`,
18
+ * whose contract is written for exactly this case ("apply Connection's
19
+ * Host/Origin checks ... to another Web route"). Our paths are exact routes
20
+ * registered beside the shared `/api` prefix, so the host's own fence never
21
+ * sees them; asking its entry point instead of re-deriving the rule keeps a
22
+ * single trust model rather than a second, stricter one. Issue #17 is what the
23
+ * second one cost: behind a reverse proxy the literals checkable in this file
24
+ * are the gateway address and the public domain, so every route answered 403
25
+ * while the rest of the GUI worked.
26
+ */
27
+ export type RouteFence = (request: IncomingMessage) => number | undefined;
28
+ /**
29
+ * Fallback fence for a host that mounts no Connection service (unit tests, and
30
+ * any composition serving the webserver alone): the historical literal-loopback
31
+ * check, so a missing service degrades to the previous behaviour instead of an
32
+ * open or a dead route family.
33
+ */
34
+ export declare function loopbackOnlyFence(request: IncomingMessage): number | undefined;
10
35
  /** One JSON response. */
11
36
  export declare function writeJson(res: ServerResponse, status: number, body: unknown): void;
12
37
  /** One JSON error response. */
@@ -3,20 +3,45 @@
3
3
  * official registry + hub-disabled skills + discovery diagnostics), skill
4
4
  * detail, enable/disable toggle, new-skill scaffold, user groups (tags +
5
5
  * origin collections), and upstream source tracking (check/sync/follow
6
- * upstream deletion into a restorable trash). Every route carries a
7
- * loopback-only trust fence — these endpoints rename files under the user's
8
- * skill roots, so LAN-exposed dsh web deployments must not serve them.
6
+ * upstream deletion into a restorable trash). Every route carries the host's
7
+ * own `/api` trust fence (loopback, the listener's bind address, each
8
+ * `--trusted-host` authority, then the browser session) — these endpoints
9
+ * rename files under the user's skill roots, so they must never be laxer than
10
+ * the GUI around them (issue #17).
9
11
  *
10
12
  * 按域拆分后的聚合入口:各域 handler 在 ./routes/<domain>.ts,共享围栏在
11
13
  * ./routes/helpers.ts,节流/任务状态在 ./routes/route-state.ts。
12
14
  * 老 `from './routes.ts'`(makeRoutes + SkillHubRouteDeps)保持可用。
13
15
  */
16
+ import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection';
14
17
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
15
18
  import { type SkillHubRouteDeps } from './routes/helpers.ts';
19
+ import { type RouteFence } from './routes/http.ts';
16
20
  export type { SkillHubRouteDeps, SkillLookupLike } from './routes/helpers.ts';
21
+ export type { RouteFence } from './routes/http.ts';
22
+ /**
23
+ * Build the family's trust fence from the host's `/api` model.
24
+ *
25
+ * `ctx.connection.requestRejection` is the documented hook for fencing another
26
+ * Web route; with no Connection service (a webserver-only composition, or a
27
+ * unit test) the literal-loopback check stands in, so the fallback is the old
28
+ * behaviour rather than an open route family.
29
+ *
30
+ * The service is read **per request**, not once at mount: plugins start in
31
+ * composition order, so a one-shot read at `apply()` time would latch the
32
+ * fallback whenever this plugin happens to come up before Connection. A
33
+ * persistent absence is almost certainly an upstream composition change rather
34
+ * than a transient startup order, so the first miss fires `onMissing` once —
35
+ * without that signal the fallback would silently reproduce issue #17 after
36
+ * any future upstream rename of the service.
37
+ * @param read - resolves `ctx.connection`, or `undefined` when absent.
38
+ * @param onMissing - called once, the first time the service reads absent.
39
+ * @returns the fence every route in this family runs first.
40
+ */
41
+ export declare function hostTrustFence(read: () => HostConnectionHandle | undefined, onMissing?: () => void): RouteFence;
17
42
  /**
18
43
  * Build every /api/skill-hub route.
19
- * @param deps - skill registry view + sidecar store.
44
+ * @param deps - skill registry view + sidecar store + trust fence.
20
45
  * @returns the exact-path routes plus the family's 404 catch-all.
21
46
  */
22
47
  export declare function makeRoutes(deps: SkillHubRouteDeps): WebRoute[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-hub",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -21,7 +21,7 @@ export const zhSettings = {
21
21
  'settings.statsScanMinutes': '自动统计间隔(分钟)',
22
22
  'settings.statsScanMinutesHint': '后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。',
23
23
  'settings.githubToken': 'GitHub Token',
24
- 'settings.githubTokenHint': '市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅回环可读;留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。',
24
+ 'settings.githubTokenHint': '市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。',
25
25
  'settings.githubTokenPlaceholder': 'ghp_… / github_pat_…(留空为匿名)',
26
26
  'settings.inherit': '继承',
27
27
  'settings.overridden': '已覆盖',
@@ -59,7 +59,7 @@ export const enSettings: Record<keyof typeof zhSettings, string> = {
59
59
  'settings.statsScanMinutes': 'Auto stats interval (minutes)',
60
60
  'settings.statsScanMinutesHint': 'How often the background session-log scan runs (min 1); heavy scans automatically stretch it.',
61
61
  'settings.githubToken': 'GitHub token',
62
- 'settings.githubTokenHint': 'Market/sources use the GitHub API: 60 req/h anonymous, 5000/hr with a token. A fine-grained token needs no permissions. Stored plaintext in local settings, loopback-only; empty means anonymous (or GITHUB_TOKEN env).',
62
+ 'settings.githubTokenHint': 'Market/sources use the GitHub API: 60 req/h anonymous, 5000/hr with a token. A fine-grained token needs no permissions. Stored plaintext in local settings, reachable only through dsh\'s /api trust fence (loopback / bind address / --trusted-host); empty means anonymous (or GITHUB_TOKEN env).',
63
63
  'settings.githubTokenPlaceholder': 'ghp_… / github_pat_… (empty = anonymous)',
64
64
  'settings.inherit': 'Inherit',
65
65
  'settings.overridden': 'Overridden',
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ import type { SettingsForms, SettingsNamespace, SettingsPathOp } from '@deepseek
14
14
  // loader's `fiber.runtime.Config` handling) only exist here. Every official
15
15
  // plugin imports it under this name.
16
16
  import z from '@deepseek-ai/schemastery'
17
+ // Type-only, for the `ctx.connection` augmentation (the `/api` trust fence).
18
+ import type {} from '@deepseek-ai/dsh-client-connection'
17
19
  import type {} from '@deepseek-ai/dsh-host-webserver'
18
20
  import type {} from '@deepseek-ai/dsh-skill'
19
21
  import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -21,7 +23,7 @@ import type {} from '@deepseek-ai/dsh-session-query'
21
23
  import type {} from '@deepseek-ai/dsh-settings'
22
24
  import { HUB_CONFIG_DEFAULTS, HUB_ENTRY_ID, HEX_COLOR_RE, resolveHubConfig, type HubConfig } from './protocol.ts'
23
25
  import { SkillHubProvider } from './provider.ts'
24
- import { makeRoutes } from './routes.ts'
26
+ import { hostTrustFence, makeRoutes } from './routes.ts'
25
27
  import { createSkillStatsReader, asPersistenceSeam, type SessionPersistenceLike, type SessionQueryLike, type SkillStatsReader } from './stats.ts'
26
28
  import { SkillHubStore } from './store.ts'
27
29
  import { reconcileDisabledSkills, pruneStaleDisabledSkills } from './reconcile.ts'
@@ -132,8 +134,8 @@ const SECTION_ORDER = 152
132
134
 
133
135
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
134
136
  export const SKILL_HUB_GUIDANCE = [
135
- '本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由仅回环可访问。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。',
136
- 'The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → "Skills" is the management page; the plugin\'s configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with "check all" and "update all" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; routes are loopback-only. When the user mentions "skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill", this plugin is what they mean — collaborate accordingly.'
137
+ '本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由受 dsh 自身的 /api 信任栅栏保护(回环、监听器绑定地址、--trusted-host 声明的 authority,外加浏览器会话认证),因此反代部署下与 GUI 同一套放行规则。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。',
138
+ 'The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → "Skills" is the management page; the plugin\'s configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with "check all" and "update all" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; the routes sit behind dsh\'s own /api trust fence (loopback, the listener\'s bind address, any --trusted-host authority, plus browser-session authentication), so a reverse-proxied deployment is judged exactly like the GUI around it. When the user mentions "skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill", this plugin is what they mean — collaborate accordingly.'
137
139
  ].join('\n\n')
138
140
 
139
141
  /**
@@ -262,6 +264,16 @@ export function apply(ctx: Context, config?: Config): void {
262
264
  const disposers = makeRoutes({
263
265
  skills: ctx.skills,
264
266
  store,
267
+ // The host's own `/api` trust fence (loopback ∪ bind address ∪
268
+ // --trusted-host ∪ browser session). Read per request: Connection
269
+ // may be provided after this plugin starts, and a one-shot read here
270
+ // would latch the literal-loopback fallback for the whole process.
271
+ // A *persistent* absence means the upstream composition changed —
272
+ // say so once instead of silently re-creating issue #17.
273
+ fence: hostTrustFence(
274
+ () => ctx.get('connection'),
275
+ () => ctx.logger.warn('[dsh-skill-hub] Connection service absent: routes fall back to literal-loopback; reverse-proxy deployments will 403 until it mounts'),
276
+ ),
265
277
  invalidate: () => { providerControl?.invalidate() },
266
278
  stats,
267
279
  config: current,
@@ -60,7 +60,7 @@ export type HubSettingsValue = {
60
60
  statsWindowDays?: number
61
61
  /** 自动统计扫描间隔(分钟)。 */
62
62
  statsScanMinutes?: number
63
- /** GitHub token(明文存本地设置文档,仅回环可读;缺省为匿名)。 */
63
+ /** GitHub token(明文存本地设置文档,仅经 dsh 的 /api 信任栅栏可读;缺省为匿名)。 */
64
64
  githubToken?: string
65
65
  }
66
66
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * routes 共享层 · 依赖视图:路由依赖接口、配置解析与可写技能解析(HTTP
3
- * 围栏在 ./http.ts)。从 helpers.ts 原样搬出,行为不变。
3
+ * 围栏在 ./http.ts)。
4
4
  */
5
5
 
6
6
  import type { ServerResponse } from 'node:http'
@@ -8,7 +8,7 @@ import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill'
8
8
  import { resolveHubConfig, type HubConfig, type WritableRoot } from '../protocol.ts'
9
9
  import { rootOfPath } from '../skillfs.ts'
10
10
  import { dshHome, type SkillHubStore } from '../store.ts'
11
- import { writeError } from './http.ts'
11
+ import { writeError, type RouteFence } from './http.ts'
12
12
 
13
13
  /** Sources the hub may toggle (the user-level filesystem roots). */
14
14
  export function isWritableSource(source: string): source is WritableRoot {
@@ -27,6 +27,12 @@ export interface SkillHubRouteDeps {
27
27
  get(name: string, options?: SkillLookupLike): Promise<SkillDefinition | undefined>
28
28
  }
29
29
  store: SkillHubStore
30
+ /**
31
+ * Request fence for the whole family: returns the rejection status (401/403)
32
+ * or `undefined` to proceed. Production passes the host's own `/api` fence
33
+ * (`ctx.connection.requestRejection`); absent falls back to literal loopback.
34
+ */
35
+ fence?: RouteFence
30
36
  /** DSH home override (tests isolate the writable roots; defaults to ~/.dsh). */
31
37
  home?: string
32
38
  /** Invalidate the registry catalog cache after hub-driven mutations. */
@@ -1,8 +1,7 @@
1
1
  /**
2
- * routes 共享层:请求围栏(回环信任/方法/总开关/JSON 体)、统一错误映射、
3
- * 可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
4
- * 从 routes.ts 原样搬出,行为不变。
5
- * 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(围栏/错误映射/读体)、
2
+ * routes 共享层:请求栅栏(宿主 /api 信任模型 → 方法 → 总开关 → JSON 体)、
3
+ * 统一错误映射、可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
4
+ * 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(栅栏/错误映射/读体)、
6
5
  * ./deps.ts(依赖视图/可写解析)、./collection.ts(集合装配)、
7
6
  * ./catalog-data.ts(目录装配);对外导出面保持不变。
8
7
  */
@@ -10,12 +9,13 @@
10
9
  import type { IncomingMessage, ServerResponse } from 'node:http'
11
10
  import type { WebRoute, WebRouteKind } from '@deepseek-ai/dsh-host-webserver'
12
11
  import { disabledGate, type SkillHubRouteDeps } from './deps.ts'
13
- import { isLoopbackRequest, readJsonBody, writeError, writeRouteError } from './http.ts'
12
+ import { loopbackOnlyFence, readJsonBody, writeError, writeRouteError } from './http.ts'
14
13
 
15
14
  export {
16
15
  MAX_JSON_BODY_BYTES,
17
16
  STORE_ERROR_STATUS,
18
17
  isLoopbackRequest,
18
+ loopbackOnlyFence,
19
19
  pathExists,
20
20
  queryParam,
21
21
  readJsonBody,
@@ -25,6 +25,7 @@ export {
25
25
  writeJson,
26
26
  writeRouteError,
27
27
  } from './http.ts'
28
+ export type { RouteFence } from './http.ts'
28
29
  export {
29
30
  configOf,
30
31
  disabledGate,
@@ -83,16 +84,26 @@ export interface RouteSpec {
83
84
 
84
85
  /**
85
86
  * Wrap one handler in the shared request fences. Order matches the
86
- * original per-handler prologue: loopback trust → HTTP method → master
87
+ * original per-handler prologue: trust fence → HTTP method → master
87
88
  * switch → JSON body. The outer catch maps errors onto the JSON error
88
89
  * body (preserving RepoFetchError status codes).
90
+ *
91
+ * The trust fence is the host's `/api` model (`deps.fence`) whenever the
92
+ * composition mounts Connection, so a reverse-proxied deployment is judged by
93
+ * the same authorities dsh already trusts; without that service the historical
94
+ * literal-loopback check stands in.
89
95
  */
90
96
  export function createRoute(deps: SkillHubRouteDeps, spec: RouteSpec): WebRoute {
97
+ const fence = deps.fence ?? loopbackOnlyFence
91
98
  return {
92
99
  kind: spec.kind ?? 'exact',
93
100
  path: spec.path,
94
101
  handler: async (req, res) => {
95
- if (!isLoopbackRequest(req)) { writeError(res, 403, 'forbidden: loopback-only'); return }
102
+ const rejection = fence(req)
103
+ if (rejection !== undefined) {
104
+ writeError(res, rejection, rejection === 401 ? 'unauthorized: browser session required' : 'forbidden: untrusted authority')
105
+ return
106
+ }
96
107
  if (!spec.methods.includes(req.method as 'GET' | 'POST')) {
97
108
  writeError(res, 405, 'method not allowed: ' + (req.method ?? ''))
98
109
  return
@@ -1,6 +1,6 @@
1
1
  /**
2
- * routes 共享层 · HTTP 围栏:回环信任检查、JSON 响应与错误映射、请求体读取、
3
- * 查询/字段读取与文件存在性检查。从 helpers.ts 原样搬出,行为不变。
2
+ * routes 共享层 · HTTP 围栏:请求信任栅栏(复用宿主 `/api` 模型,见 RouteFence)、
3
+ * JSON 响应与错误映射、请求体读取、查询/字段读取与文件存在性检查。
4
4
  */
5
5
 
6
6
  import { stat } from 'node:fs/promises'
@@ -35,6 +35,35 @@ export function isLoopbackRequest(request: IncomingMessage): boolean {
35
35
  }
36
36
  }
37
37
 
38
+ /**
39
+ * The route family's request fence: the HTTP status a request is rejected
40
+ * with (`401` unauthenticated / `403` untrusted authority), or `undefined`
41
+ * when it may reach the handler.
42
+ *
43
+ * dsh owns **one** browser-trust model for the whole `/api` tree — loopback,
44
+ * the listener's own bind address, every `--trusted-host` authority, then the
45
+ * browser session — and publishes it as `ctx.connection.requestRejection`,
46
+ * whose contract is written for exactly this case ("apply Connection's
47
+ * Host/Origin checks ... to another Web route"). Our paths are exact routes
48
+ * registered beside the shared `/api` prefix, so the host's own fence never
49
+ * sees them; asking its entry point instead of re-deriving the rule keeps a
50
+ * single trust model rather than a second, stricter one. Issue #17 is what the
51
+ * second one cost: behind a reverse proxy the literals checkable in this file
52
+ * are the gateway address and the public domain, so every route answered 403
53
+ * while the rest of the GUI worked.
54
+ */
55
+ export type RouteFence = (request: IncomingMessage) => number | undefined
56
+
57
+ /**
58
+ * Fallback fence for a host that mounts no Connection service (unit tests, and
59
+ * any composition serving the webserver alone): the historical literal-loopback
60
+ * check, so a missing service degrades to the previous behaviour instead of an
61
+ * open or a dead route family.
62
+ */
63
+ export function loopbackOnlyFence(request: IncomingMessage): number | undefined {
64
+ return isLoopbackRequest(request) ? undefined : 403
65
+ }
66
+
38
67
  /** One JSON response. */
39
68
  export function writeJson(res: ServerResponse, status: number, body: unknown): void {
40
69
  const payload = JSON.stringify(body)