@michengai/dsh-skills-manager 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,7 +124,9 @@ Escape closes only the frontmost upload or confirmation dialog and leaves Settin
124
124
 
125
125
  - Enable, disable, and delete accept only one ordinary skill-name path segment.
126
126
  - Replacements copy to a temporary sibling path first and keep the original until that succeeds.
127
- - Write endpoints require JSON and the DSH client request marker, so cross-site browser requests cannot trigger local file operations.
127
+ - All endpoints, including GET `/state`, accept only loopback `Host` values (`localhost`, `127.0.0.1`, `[::1]`).
128
+ - Write endpoints also require JSON and the DSH client request marker, so cross-site browser requests cannot trigger local file operations.
129
+ - Import accepts the local path the user selected. The HTTP API trusts loopback callers only; do not expose the host web server beyond this machine.
128
130
 
129
131
  ## Secondary development
130
132
 
package/README.zh-CN.md CHANGED
@@ -124,7 +124,9 @@ dsh --profile web --dump-config
124
124
 
125
125
  - 启用、停用和删除只接受单个普通技能名称,目录穿越名称会被拒绝。
126
126
  - 覆盖前先复制到同目录临时路径;复制成功前不会改动现有技能。
127
- - 写入接口要求 JSON 与 DSH 客户端请求标记,跨站浏览器请求不能触发本地文件操作。
127
+ - 全部接口(含 GET `/state`)只接受 loopback `Host`(`localhost`、`127.0.0.1`、`[::1]`)。
128
+ - 写入接口还要求 JSON 与 DSH 客户端请求标记,跨站浏览器请求不能触发本地文件操作。
129
+ - 导入接受用户选定的本机路径。HTTP 接口只信任 loopback 来源,不要把宿主 webServer 暴露到非本机地址。
128
130
 
129
131
  ## 二次开发
130
132
 
package/lib/client.js CHANGED
@@ -183,15 +183,15 @@ window.__ModuleLoader__.load({
183
183
  };
184
184
 
185
185
  var CSS = `
186
- .dssm-section{display:flex;min-width:0;flex-direction:column;gap:20px;color:var(--dsw-alias-label-primary)}
187
- .dssm-toolbar{display:flex;align-items:flex-start;gap:16px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
188
- .dssm-title{margin:0;font-size:20px;line-height:28px;font-weight:650;letter-spacing:-.2px}.dssm-desc{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
189
- .dssm-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.dssm-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:550;cursor:pointer;transition:opacity 180ms ease,background 180ms ease,border-color 180ms ease}
186
+ .dssm-section{box-sizing:border-box;display:flex;min-width:0;max-width:760px;width:100%;margin:0 auto;flex-direction:column;gap:16px;padding:0 0 32px;color:var(--dsw-alias-label-primary)}
187
+ .dssm-toolbar{display:flex;align-items:flex-start;gap:16px;padding-bottom:12px}
188
+ .dssm-title{margin:0;font-size:20px;line-height:28px;font-weight:650;letter-spacing:-.2px}.dssm-desc{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
189
+ .dssm-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.dssm-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 12px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:550;cursor:pointer;transition:opacity 180ms ease,background 180ms ease,border-color 180ms ease}
190
190
  .dssm-btn:hover:not(:disabled){opacity:.9}.dssm-btn:disabled{opacity:.5;cursor:default}.dssm-btn-secondary{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dssm-btn-danger{background:transparent;border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
191
191
  .dssm-btn:focus-visible,.dssm-dropzone:focus-visible,.dssm-icon-btn:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-summary{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
192
192
  .dssm-summary-count{font-size:18px;font-weight:650}.dssm-summary-label{color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-summary-separator{width:1px;height:24px;background:var(--dsw-alias-border-l2)}
193
193
  .dssm-group{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dssm-group-head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-group-title{margin:0;font-size:14px;font-weight:600}.dssm-count{color:var(--dsw-alias-label-tertiary);font-size:12px}.dssm-path{margin-left:auto;max-width:48%;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
194
- .dssm-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}.dssm-row-id{display:flex;align-items:center;gap:7px;min-width:0}.dssm-row-name{overflow:hidden;font-size:14px;font-weight:560;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.dssm-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dssm-empty{padding:28px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center}.dssm-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}
194
+ .dssm-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}.dssm-row-id{display:flex;align-items:center;gap:7px;min-width:0}.dssm-row-name{overflow:hidden;font-size:13px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.dssm-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dssm-empty{padding:28px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center}.dssm-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}
195
195
  .dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:24px;background:rgba(0,0,0,.58)}.dssm-modal{box-sizing:border-box;display:flex;width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;min-width:0;min-height:0;flex:0 0 auto!important;align-self:center!important;flex-direction:column;gap:18px;padding:24px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-upload{width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;align-self:center!important;flex:0 0 auto!important;overflow:auto}.dssm-modal-head{display:flex;align-items:center;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:650}.dssm-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:20px;line-height:1;cursor:pointer}.dssm-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
196
196
  .dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:144px;flex:none;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:18px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;transition:border-color 180ms ease,background 180ms ease}.dssm-dropzone:hover,.dssm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dssm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:560}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-file{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:13px}.dssm-file-name{overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-modal-head,.dssm-file,.dssm-modal-actions{flex:none}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}
197
197
  @media (max-width:560px){.dssm-mask{padding:12px;align-items:center}.dssm-modal{width:100%!important;height:auto!important;padding:16px;gap:12px;align-self:center!important}.dssm-modal-upload{width:100%!important;align-self:center!important}.dssm-toolbar{flex-wrap:wrap}.dssm-actions{margin-left:0}.dssm-path{display:none}.dssm-row{align-items:flex-start;flex-wrap:wrap}.dssm-row>.dssm-btn{margin-left:auto}.dssm-modal-actions{justify-content:stretch}.dssm-modal-actions .dssm-btn{flex:1 1 auto}}@media (prefers-reduced-motion:reduce){.dssm-btn,.dssm-dropzone{transition:none}}
package/lib/core.js CHANGED
@@ -116,7 +116,9 @@ function stripBom(text) {
116
116
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
117
117
  }
118
118
 
119
- /** 解析 SKILL.md 的 frontmatter。返回 { fields, map, body },map 保留键序。 */
119
+ /** 解析 SKILL.md 的 frontmatter。返回 { fields, map, body },map 保留键序。
120
+ * 只识别顶层 key: value;缩进嵌套字段(如 metadata.source)不进入 map。
121
+ * 写入走 updateInvocationPolicy 的文本级替换,因此嵌套块会被原样保留。 */
120
122
  export function parseSkillDoc(text) {
121
123
  const src = stripBom(String(text));
122
124
  const lines = src.split(/\r?\n/);
@@ -180,7 +182,8 @@ export function unquote(v) {
180
182
  return s;
181
183
  }
182
184
 
183
- /** 仅更新调用策略行,避免重写并破坏其他 YAML frontmatter。 */
185
+ /** 仅更新调用策略行,避免重写并破坏其他 YAML frontmatter。
186
+ * 启用时删除这两行以恢复缺省可调用,不还原这两行的原始文本或行内注释。 */
184
187
  function updateInvocationPolicy(text, enabled) {
185
188
  const source = String(text);
186
189
  const newline = source.includes("\r\n") ? "\r\n" : "\n";
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // dsh-skills-manager host half:设置面板的 HTTP 后端(webServer prefix 路由)
2
2
  // - 零第三方 import(仅 node: 内置 + 本地 core.js)
3
3
  // - 路由:/api/dsh-skills-manager/state | enable | disable | delete | import
4
+ // - 全部接口先校验 loopback Host;写接口再校验请求标记与 JSON
4
5
  // - $DSH_HOME\skills 可上传、删除、启停;$DSH_AGENTS_HOME\skills 仅允许查看
5
6
 
6
7
  import { appendFile, rename, rm, stat } from "node:fs/promises";
@@ -72,10 +73,17 @@ function readBody(req, limit = 1 << 20) {
72
73
  });
73
74
  }
74
75
 
75
- /** 自定义请求头使跨站 fetch 必须预检;本地接口不提供 CORS 响应。 */
76
- function validateMutationRequest(req) {
76
+ /** 只允许 loopback Host,避免 DNS rebinding 读取 /state 或改写本地技能。 */
77
+ function validateLoopbackHost(req) {
77
78
  const host = String(req.headers.host || "").toLowerCase();
78
79
  if (!/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host)) return { statusCode: 403, code: "error.proto.forbidden", error: "forbidden host" };
80
+ return null;
81
+ }
82
+
83
+ /** 写接口额外要求自定义头与 JSON;自定义头迫使跨站 fetch 预检,且本接口不回 CORS。 */
84
+ function validateMutationRequest(req) {
85
+ const hostError = validateLoopbackHost(req);
86
+ if (hostError) return hostError;
79
87
  if (req.headers[CLIENT_MARKER_HEADER] !== "1") return { statusCode: 403, code: "error.proto.forbidden", error: "forbidden mutation request" };
80
88
  const contentType = String(req.headers["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
81
89
  if (contentType !== "application/json") return { statusCode: 415, code: "error.proto.contentType", error: "content-type must be application/json" };
@@ -174,6 +182,11 @@ function apply(ctx) {
174
182
  const u = new URL(req.url, "http://localhost");
175
183
  const path = u.pathname.replace(/\/+$/, "");
176
184
  try {
185
+ const hostError = validateLoopbackHost(req);
186
+ if (hostError) {
187
+ json(res, hostError.statusCode, { ok: false, code: hostError.code, error: hostError.error });
188
+ return;
189
+ }
177
190
  if (req.method === "GET" && path === "/api/dsh-skills-manager/state") {
178
191
  return run(res, state);
179
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "NPM-installable DSH Web plugin for managing local skills and viewing shared Agent skills safely.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {