Bladeren bron

refactor: 技能生效目录移到顶层 managed-skills/

- 第三方与自定义技能的生效目录从 data/ 移到 managed-skills/{community,custom},启动时自动搬迁旧目录
- systemd ReadWritePaths 加入 managed-skills;deploy.sh 负责建目录并在 unit 变化时更新
- 文件管理增加只读位置“已安装技能”

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 1 week geleden
bovenliggende
commit
b171b4d36d

+ 22 - 1
apps/server/src/app.ts

@@ -1,6 +1,8 @@
 // 服务容器:组装数据库、各个 service 与引擎
 // 服务容器:组装数据库、各个 service 与引擎
+import fs from 'node:fs'
+import path from 'node:path'
 import type { ProviderId } from '@agent-server/shared'
 import type { ProviderId } from '@agent-server/shared'
-import { config, dbPath } from './config'
+import { communitySkillsDir, config, customSkillsDir, dbPath } from './config'
 import { createDb, migrateDb, type DB } from './db'
 import { createDb, migrateDb, type DB } from './db'
 import { ClaudeProvider } from './providers/claude/adapter'
 import { ClaudeProvider } from './providers/claude/adapter'
 import { CodexProvider } from './providers/codex/adapter'
 import { CodexProvider } from './providers/codex/adapter'
@@ -52,6 +54,7 @@ export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<R
   const mcpUpstream = new McpUpstream()
   const mcpUpstream = new McpUpstream()
   const mcpServers = new McpServerService(db, mcpUpstream, () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
   const mcpServers = new McpServerService(db, mcpUpstream, () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
   const adminAudit = (summary: string, detail?: unknown) => audit.log({ actor: 'admin', kind: 'admin', summary, detail })
   const adminAudit = (summary: string, detail?: unknown) => audit.log({ actor: 'admin', kind: 'admin', summary, detail })
+  migrateManagedSkillDirs(log)
   const customSkills = new CustomSkillService(db, adminAudit)
   const customSkills = new CustomSkillService(db, adminAudit)
   // 按数据库重建生效目录(缺失或被改动的技能目录重新发布)
   // 按数据库重建生效目录(缺失或被改动的技能目录重新发布)
   customSkills.reconcile(msg => log.warn({}, msg))
   customSkills.reconcile(msg => log.warn({}, msg))
@@ -77,3 +80,21 @@ export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<R
 }
 }
 
 
 export type App = ReturnType<typeof createApp>
 export type App = ReturnType<typeof createApp>
+
+/** 技能生效目录从 data/ 下移到顶层 managed-skills/ 后,一次性搬走旧目录(同一文件系统,直接改名) */
+function migrateManagedSkillDirs(log: AppLogger) {
+  const moves: [string, string][] = [
+    [path.join(config.dataDir, 'data', 'community-skills'), communitySkillsDir()],
+    [path.join(config.dataDir, 'data', 'custom-skills'), customSkillsDir()],
+  ]
+  for (const [from, to] of moves) {
+    if (!fs.existsSync(from)) continue
+    if (fs.existsSync(to)) {
+      log.warn({ from, to }, '旧的技能目录仍然存在,新目录已有内容,保留旧目录不处理')
+      continue
+    }
+    fs.mkdirSync(path.dirname(to), { recursive: true })
+    fs.renameSync(from, to)
+    log.info({ from, to }, '技能目录已迁移')
+  }
+}

+ 8 - 2
apps/server/src/config.ts

@@ -39,11 +39,17 @@ export const APP_VERSION = '0.1.0'
 
 
 export const dbPath = () => path.join(config.dataDir, 'data', 'agent.db')
 export const dbPath = () => path.join(config.dataDir, 'data', 'agent.db')
 
 
+/**
+ * 服务管理的技能生效目录:服务可写、Agent 只读(ADR-0022)。单独放在顶层,不与数据库等放在 data 下;
+ * 这里只放生效的技能,文件库、上传暂存与目录缓存仍在 data 下(Agent 不可见)
+ */
+export const managedSkillsDir = () => path.join(config.dataDir, 'managed-skills')
+
 /** 第三方技能目录(Claude 插件格式,两个引擎共用,ADR-0017) */
 /** 第三方技能目录(Claude 插件格式,两个引擎共用,ADR-0017) */
-export const communitySkillsDir = () => path.join(config.dataDir, 'data', 'community-skills')
+export const communitySkillsDir = () => path.join(managedSkillsDir(), 'community')
 
 
 /** 自定义技能:生效目录(Claude 插件格式,两个引擎共用)、按内容寻址的文件库、上传的 zip 解压目录(docs/design/12-custom-skills.md) */
 /** 自定义技能:生效目录(Claude 插件格式,两个引擎共用)、按内容寻址的文件库、上传的 zip 解压目录(docs/design/12-custom-skills.md) */
-export const customSkillsDir = () => path.join(config.dataDir, 'data', 'custom-skills')
+export const customSkillsDir = () => path.join(managedSkillsDir(), 'custom')
 export const customSkillStoreDir = () => path.join(config.dataDir, 'data', 'custom-skill-store')
 export const customSkillStoreDir = () => path.join(config.dataDir, 'data', 'custom-skill-store')
 export const skillUploadsDir = () => path.join(config.dataDir, 'data', 'skill-uploads')
 export const skillUploadsDir = () => path.join(config.dataDir, 'data', 'skill-uploads')
 
 

+ 2 - 2
apps/server/src/services/access-policy.ts

@@ -136,8 +136,8 @@ function mostSpecific(target: string, list: string[]): string | null {
 }
 }
 
 
 /**
 /**
- * 服务管理的技能生效目录(第三方、自定义):位于全局禁止的 {dataDir}/data 之内,但必须只读可见,
- * 否则 Agent 读不到技能的参考文档、运行不了脚本。只放开这两个目录,data 下的其他内容(数据库、版本库、MCP 数据)仍然禁止
+ * 服务管理的技能生效目录(第三方、自定义,位于 {dataDir}/managed-skills):必须只读可见,否则 Agent 读不到技能的参考文档、
+ * 运行不了脚本。读取先于禁止项放行(即使追加的全局禁止项覆盖了它们);写入仍然禁止
  */
  */
 export const managedSkillRoots = () => [communitySkillsDir(), customSkillsDir()].map(realpathLoose)
 export const managedSkillRoots = () => [communitySkillsDir(), customSkillsDir()].map(realpathLoose)
 
 

+ 2 - 1
apps/server/src/services/file-manager.ts

@@ -6,7 +6,7 @@ import path from 'node:path'
 import { desc, eq } from 'drizzle-orm'
 import { desc, eq } from 'drizzle-orm'
 import { zipSync } from 'fflate'
 import { zipSync } from 'fflate'
 import type { AppSettings, FsEntryDto, FsRootDto, FsShortcutDto, KnowledgeRootDto } from '@agent-server/shared'
 import type { AppSettings, FsEntryDto, FsRootDto, FsShortcutDto, KnowledgeRootDto } from '@agent-server/shared'
-import { config } from '../config'
+import { config, managedSkillsDir } from '../config'
 import type { DB } from '../db'
 import type { DB } from '../db'
 import { sessions } from '../db/schema'
 import { sessions } from '../db/schema'
 import { AppError, badRequest, conflict, notFound } from '../lib/errors'
 import { AppError, badRequest, conflict, notFound } from '../lib/errors'
@@ -192,6 +192,7 @@ export class FileManagerService {
       roots.push({ id: `kb:${k.id}`, name: `资料库:${k.name}`, kind: 'knowledge', path: k.path, writable: true, description: `${k.description}${k.access === 'read' ? '(Agent 只读)' : '(Agent 可读写)'}` })
       roots.push({ id: `kb:${k.id}`, name: `资料库:${k.name}`, kind: 'knowledge', path: k.path, writable: true, description: `${k.description}${k.access === 'read' ? '(Agent 只读)' : '(Agent 可读写)'}` })
     }
     }
     roots.push({ id: 'skills', name: '本地技能目录', kind: 'readonly', path: s.paths.skillsDir, writable: false, description: '随部署同步,在线修改会被覆盖,因此只读' })
     roots.push({ id: 'skills', name: '本地技能目录', kind: 'readonly', path: s.paths.skillsDir, writable: false, description: '随部署同步,在线修改会被覆盖,因此只读' })
+    roots.push({ id: 'managed-skills', name: '已安装技能', kind: 'readonly', path: managedSkillsDir(), writable: false, description: '第三方技能与已发布的自定义技能,由服务生成,在"技能"页管理,因此只读' })
     roots.push({ id: 'runtime', name: 'Python 运行环境', kind: 'readonly', path: s.paths.pythonRuntime, writable: false, description: '随部署安装,只读' })
     roots.push({ id: 'runtime', name: 'Python 运行环境', kind: 'readonly', path: s.paths.pythonRuntime, writable: false, description: '随部署安装,只读' })
     return roots
     return roots
   }
   }

+ 2 - 2
apps/server/test/file-manager.test.ts

@@ -30,9 +30,9 @@ beforeAll(async () => {
 const kbRoot = () => app.fileManager.roots().find(r => r.kind === 'knowledge')!.id
 const kbRoot = () => app.fileManager.roots().find(r => r.kind === 'knowledge')!.id
 
 
 describe('后台文件管理', () => {
 describe('后台文件管理', () => {
-  it('位置:会话工作区与资料库可写,本地技能与运行环境只读;常用目录包含最近的会话与资料库', () => {
+  it('位置:会话工作区与资料库可写,本地技能、已安装技能与运行环境只读;常用目录包含最近的会话与资料库', () => {
     const roots = app.fileManager.roots()
     const roots = app.fileManager.roots()
-    expect(roots.map(r => [r.id.split(':')[0], r.writable])).toEqual([['workspaces', true], ['kb', true], ['skills', false], ['runtime', false]])
+    expect(roots.map(r => [r.id.split(':')[0], r.writable])).toEqual([['workspaces', true], ['kb', true], ['skills', false], ['managed-skills', false], ['runtime', false]])
     const sc = app.fileManager.shortcuts()
     const sc = app.fileManager.shortcuts()
     expect(sc[0]).toMatchObject({ rootId: 'workspaces', path: '' })
     expect(sc[0]).toMatchObject({ rootId: 'workspaces', path: '' })
     expect(sc.some(s => s.rootId === 'workspaces' && s.path === sid)).toBe(true)
     expect(sc.some(s => s.rootId === 'workspaces' && s.path === sid)).toBe(true)

+ 22 - 0
apps/server/test/managed-skills-migration.test.ts

@@ -0,0 +1,22 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import pino from 'pino'
+import { describe, expect, it } from 'vitest'
+import { createApp } from '../src/app'
+import { communitySkillsDir, config, customSkillsDir } from '../src/config'
+
+describe('技能生效目录迁移', () => {
+  it('启动时把 data/ 下的旧目录搬到 managed-skills/,内容不变', () => {
+    const old = path.join(config.dataDir, 'data', 'community-skills', 'skills', 'demo')
+    fs.mkdirSync(old, { recursive: true })
+    fs.writeFileSync(path.join(old, 'SKILL.md'), '---\nname: demo\n---\n')
+    fs.mkdirSync(path.join(config.dataDir, 'data', 'custom-skills', 'skills'), { recursive: true })
+
+    createApp(pino({ level: 'silent' }), { providers: {} })
+
+    expect(fs.existsSync(path.join(config.dataDir, 'data', 'community-skills'))).toBe(false)
+    expect(fs.existsSync(path.join(config.dataDir, 'data', 'custom-skills'))).toBe(false)
+    expect(fs.readFileSync(path.join(communitySkillsDir(), 'skills', 'demo', 'SKILL.md'), 'utf8')).toContain('name: demo')
+    expect(customSkillsDir()).toBe(path.join(config.dataDir, 'managed-skills', 'custom'))
+  })
+})

+ 1 - 1
deploy/agent-server.service

@@ -25,7 +25,7 @@ RestartSec=3
 # 文件系统:整盘只读,home 不可见,只有服务数据可写;程序与运行环境由 root 所有、只读
 # 文件系统:整盘只读,home 不可见,只有服务数据可写;程序与运行环境由 root 所有、只读
 ProtectSystem=strict
 ProtectSystem=strict
 ProtectHome=yes
 ProtectHome=yes
-ReadWritePaths=/srv/agent-server/home /srv/agent-server/data /srv/agent-server/workspaces /srv/agent-server/logs /srv/agent-server/knowledge
+ReadWritePaths=/srv/agent-server/home /srv/agent-server/data /srv/agent-server/managed-skills /srv/agent-server/workspaces /srv/agent-server/logs /srv/agent-server/knowledge
 PrivateTmp=yes
 PrivateTmp=yes
 UMask=0077
 UMask=0077
 # 纵深防御:即使权限配置出错,也看不到这些高权限 socket
 # 纵深防御:即使权限配置出错,也看不到这些高权限 socket

+ 7 - 0
deploy/deploy.sh

@@ -22,6 +22,13 @@ sudo rsync -a --delete skills/ "$ROOT/skills/"
 sudo chown -R root:root "$ROOT/app" "$ROOT/skills"
 sudo chown -R root:root "$ROOT/app" "$ROOT/skills"
 sudo chmod -R a+rX "$ROOT/app" "$ROOT/skills"
 sudo chmod -R a+rX "$ROOT/app" "$ROOT/skills"
 
 
+# 服务可写目录与 unit 文件(新增目录或修改 unit 后,不必重跑 install.sh)
+sudo install -d -o agentsvc -g agentsvc -m 0700 "$ROOT/managed-skills"
+if ! sudo cmp -s deploy/agent-server.service /etc/systemd/system/agent-server.service; then
+  sudo install -m 0644 deploy/agent-server.service /etc/systemd/system/agent-server.service
+  sudo systemctl daemon-reload
+fi
+
 sudo systemctl restart agent-server
 sudo systemctl restart agent-server
 for _ in $(seq 1 20); do
 for _ in $(seq 1 20); do
   if curl -sf http://127.0.0.1:8787/healthz >/dev/null; then
   if curl -sf http://127.0.0.1:8787/healthz >/dev/null; then

+ 1 - 1
deploy/install.sh

@@ -18,7 +18,7 @@ chown -R root:root "$ROOT/app" "$ROOT/runtime" "$ROOT/skills"
 chmod -R a+rX "$ROOT/runtime" "$ROOT/skills"
 chmod -R a+rX "$ROOT/runtime" "$ROOT/skills"
 
 
 # 服务数据:只有 agentsvc 可访问(含 CLI 登录态、数据库、会话工作区)
 # 服务数据:只有 agentsvc 可访问(含 CLI 登录态、数据库、会话工作区)
-for d in home data workspaces logs; do
+for d in home data managed-skills workspaces logs; do
   install -d -o "$SVC" -g "$SVC" -m 0700 "$ROOT/$d"
   install -d -o "$SVC" -g "$SVC" -m 0700 "$ROOT/$d"
   chown -R "$SVC:$SVC" "$ROOT/$d"
   chown -R "$SVC:$SVC" "$ROOT/$d"
 done
 done

+ 1 - 1
docs/adr/0017-community-skills.md

@@ -6,7 +6,7 @@
 官方技能仓库只覆盖少量场景。[skills.sh](https://skills.sh) 是社区技能目录,收录 GitHub 仓库中的 SKILL.md 技能,提供搜索接口(`GET https://skills.sh/api/search?q=…`,返回 `source`=owner/repo、`skillId`、`installs`)。第三方技能是通用格式,两个引擎都能用。
 官方技能仓库只覆盖少量场景。[skills.sh](https://skills.sh) 是社区技能目录,收录 GitHub 仓库中的 SKILL.md 技能,提供搜索接口(`GET https://skills.sh/api/search?q=…`,返回 `source`=owner/repo、`skillId`、`installs`)。第三方技能是通用格式,两个引擎都能用。
 
 
 ## 决策
 ## 决策
-- **一个目录,两个引擎**:第三方技能安装在 `<dataDir>/data/community-skills`(服务可写的数据目录;在沙箱中单独开放只读),采用 Claude 插件格式(`.claude-plugin/plugin.json`,name = `community-skills`;技能在 `skills/<name>/`)。
+- **一个目录,两个引擎**:第三方技能安装在 `<dataDir>/managed-skills/community`(服务可写的数据目录;在沙箱中单独开放只读),采用 Claude 插件格式(`.claude-plugin/plugin.json`,name = `community-skills`;技能在 `skills/<name>/`)。
   - Claude:运行时按路径作为本地插件加载
   - Claude:运行时按路径作为本地插件加载
   - Codex:通过 `skills/extraRoots/set` 加入。已验证(codex 0.164):Codex 会读取上级目录的 plugin.json,技能名显示为 `community-skills:<name>`
   - Codex:通过 `skills/extraRoots/set` 加入。已验证(codex 0.164):Codex 会读取上级目录的 plugin.json,技能名显示为 `community-skills:<name>`
   - 两个引擎的技能 ID 一致:`community-skills:<name>`
   - 两个引擎的技能 ID 一致:`community-skills:<name>`

+ 3 - 0
docs/adr/0022-managed-skill-dirs-readable-in-sandbox.md

@@ -24,3 +24,6 @@
 - 第三方技能与自定义技能的参考文档、脚本、模版在两个引擎中都可用
 - 第三方技能与自定义技能的参考文档、脚本、模版在两个引擎中都可用
 - 宽松模式下 Codex 可以使用本地技能目录与 Python 运行环境
 - 宽松模式下 Codex 可以使用本地技能目录与 Python 运行环境
 - 自定义技能的版本库(`data/custom-skill-store`)、数据库等仍不可见
 - 自定义技能的版本库(`data/custom-skill-store`)、数据库等仍不可见
+
+## 后续(2026-10-26)
+技能生效目录已从 `data/` 移到顶层 `{dataDir}/managed-skills/{community,custom}`,不再位于全局禁止的 `data/` 之内(systemd `ReadWritePaths` 相应加入该目录,启动时自动搬迁旧目录)。文件库、上传暂存与技能来源缓存仍在 `data/` 下,对 Agent 不可见。`checkAccess` 的只读放行保留:即使追加的全局禁止项覆盖了这些目录,技能仍然可读。

+ 1 - 1
docs/design/10-deployment.md

@@ -36,7 +36,7 @@
   - `AGENT_SERVER_TRUST_PROXY=100.96.0.2`:只信任这个地址的 X-Forwarded-*,服务记录真实客户端 IP,登录限流按真实 IP 生效
   - `AGENT_SERVER_TRUST_PROXY=100.96.0.2`:只信任这个地址的 X-Forwarded-*,服务记录真实客户端 IP,登录限流按真实 IP 生效
   - 已验证(2026-10-23):SSE 不缓冲(心跳按时到达)、20 MB 上传正常
   - 已验证(2026-10-23):SSE 不缓冲(心跳按时到达)、20 MB 上传正常
   - 代理的隧道地址变化时,需要同步修改 unit 中的 `AGENT_SERVER_TRUST_PROXY`
   - 代理的隧道地址变化时,需要同步修改 unit 中的 `AGENT_SERVER_TRUST_PROXY`
-- `User=agentsvc`;`ProtectSystem=strict` + 只有 home/data/workspaces/logs/knowledge 可写;`ProtectHome=yes`;`PrivateTmp=yes`;`UMask=0077`
+- `User=agentsvc`;`ProtectSystem=strict` + 只有 home/data/managed-skills/workspaces/logs/knowledge 可写;`ProtectHome=yes`;`PrivateTmp=yes`;`UMask=0077`
 - `NoNewPrivileges=yes`;`InaccessiblePaths=` docker.sock 与 LXD socket(纵深防御,agentsvc 本身也没有权限)
 - `NoNewPrivileges=yes`;`InaccessiblePaths=` docker.sock 与 LXD socket(纵深防御,agentsvc 本身也没有权限)
 - 不能开启 `RestrictNamespaces`(bubblewrap 需要)
 - 不能开启 `RestrictNamespaces`(bubblewrap 需要)
 - 资源:`MemoryMax=8G`、`CPUQuota=600%`、`TasksMax=1024`
 - 资源:`MemoryMax=8G`、`CPUQuota=600%`、`TasksMax=1024`

+ 3 - 3
docs/design/12-custom-skills.md

@@ -11,7 +11,7 @@
 | 本地技能目录(local) | 设置项 `paths.skillsDir`,生产为 `/srv/agent-server/skills` | AgentServer 仓库 `skills/` 经 `deploy.sh` 的 `rsync --delete` 发布 | `agent-skills:<name>` | 两个(Claude 按路径加载插件,Codex 经 `skills/extraRoots/set`) | 包含在“全部技能”中 |
 | 本地技能目录(local) | 设置项 `paths.skillsDir`,生产为 `/srv/agent-server/skills` | AgentServer 仓库 `skills/` 经 `deploy.sh` 的 `rsync --delete` 发布 | `agent-skills:<name>` | 两个(Claude 按路径加载插件,Codex 经 `skills/extraRoots/set`) | 包含在“全部技能”中 |
 | Claude 插件(plugin) | Claude Code 自己的插件目录 | 后台“技能与插件”调用 `claude plugin` 安装官方市场插件 | `document-skills:xlsx` 等 | 仅 Claude(ADR-0013) | 包含在“全部” |
 | Claude 插件(plugin) | Claude Code 自己的插件目录 | 后台“技能与插件”调用 `claude plugin` 安装官方市场插件 | `document-skills:xlsx` 等 | 仅 Claude(ADR-0013) | 包含在“全部” |
 | Codex 官方(official) | `CODEX_HOME/skills/<name>` | 从 openai/skills 固定提交复制 | `openai-skills:<name>` | 仅 Codex(ADR-0016) | 包含在“全部” |
 | Codex 官方(official) | `CODEX_HOME/skills/<name>` | 从 openai/skills 固定提交复制 | `openai-skills:<name>` | 仅 Codex(ADR-0016) | 包含在“全部” |
-| 第三方(community) | `<dataDir>/data/community-skills` | skills.sh 搜索 → 预览 → 按 GitHub 固定提交安装,只收开源许可 | `community-skills:<name>` | 两个 | 需单独勾选(optIn) |
+| 第三方(community) | `<dataDir>/managed-skills/community` | skills.sh 搜索 → 预览 → 按 GitHub 固定提交安装,只收开源许可 | `community-skills:<name>` | 两个 | 需单独勾选(optIn) |
 | 引擎自带(system) | 引擎内部 | — | — | — | — |
 | 引擎自带(system) | 引擎内部 | — | — | — | — |
 
 
 ### 1.2 缺口
 ### 1.2 缺口
@@ -40,7 +40,7 @@
 所有自定义技能都存放在 AgentServer 数据目录内,采用 Claude 插件格式(与第三方技能相同):
 所有自定义技能都存放在 AgentServer 数据目录内,采用 Claude 插件格式(与第三方技能相同):
 
 
 ```
 ```
-<dataDir>/data/custom-skills/
+<dataDir>/managed-skills/custom/
   .claude-plugin/plugin.json        { "name": "custom-skills" }
   .claude-plugin/plugin.json        { "name": "custom-skills" }
   skills/<name>/SKILL.md            生效版本(运行时只读这里)
   skills/<name>/SKILL.md            生效版本(运行时只读这里)
   skills/<name>/references/…
   skills/<name>/references/…
@@ -107,7 +107,7 @@
 
 
 - Claude:与第三方技能相同,`custom-skills` 目录作为本地插件按路径加载(`PluginRef`,`optIn: true`),由 `resolveSkills` 按 Profile 选择。每次对话都会启动新的 CLI 进程,发布后下一轮即生效。
 - Claude:与第三方技能相同,`custom-skills` 目录作为本地插件按路径加载(`PluginRef`,`optIn: true`),由 `resolveSkills` 按 Profile 选择。每次对话都会启动新的 CLI 进程,发布后下一轮即生效。
 - Codex:把 `custom-skills/skills` 加入 `skills/extraRoots/set`(目前只在 app-server 启动时设置一次,`providers/codex/adapter.ts`)。需要在自定义技能发布后重新设置 extraRoots;已加载的会话按 ADR-0020 的方式处理:把自定义技能的版本指纹放入会话配置,变化时分叉,使新版本生效。`skillsToDisable` 按 Profile 选择禁用未勾选的自定义技能。
 - Codex:把 `custom-skills/skills` 加入 `skills/extraRoots/set`(目前只在 app-server 启动时设置一次,`providers/codex/adapter.ts`)。需要在自定义技能发布后重新设置 extraRoots;已加载的会话按 ADR-0020 的方式处理:把自定义技能的版本指纹放入会话配置,变化时分叉,使新版本生效。`skillsToDisable` 按 Profile 选择禁用未勾选的自定义技能。
-- 沙箱:`custom-skills` 目录在两个引擎中只读可见(与 `communitySkillsDir()` 同样加入 `resolveAccess` 的基线可读路径);`custom-skill-store` 不可见。
+- 沙箱:`managed-skills/custom` 目录在两个引擎中只读可见(与 `communitySkillsDir()` 同样加入 `resolveAccess` 的基线可读路径);`custom-skill-store` 不可见。
 
 
 ### FR-6 审计
 ### FR-6 审计