feat: 建立统一目录元数据与获取契约 (#5)

* feat: 建立 Registry 目录元数据验证基础

* feat: 迁移 Registry 应用目录元数据

* test: 解耦目录迁移进度断言

* feat: 迁移 Registry 服务目录元数据

* fix(catalog): 强制 Registry 目录元数据

* fix(catalog): 兼容 Registry v4 外部集成
This commit is contained in:
2026-08-31 04:03:38 -04:00
committed by GitHub
parent 67aeb72b20
commit 223be1f04b
31 changed files with 3169 additions and 10 deletions

View File

@@ -19,9 +19,13 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Validate registry - name: Validate Registry v4
run: npm test run: npm test
- name: Run validator tests
run: node --test scripts/catalog/*.test.mjs
- name: Validate all Registry catalog sidecars
run: node scripts/catalog/validate-registry.mjs --require-sidecars

View File

@@ -12,14 +12,13 @@ DesireCore 客户端启动时会克隆此仓库,并定期同步更新。用户
├── SCHEMA_VERSION # 数据格式版本号(当前 4.0.0 ├── SCHEMA_VERSION # 数据格式版本号(当前 4.0.0
├── manifest.json # 仓库元数据(版本、统计、维护者) ├── manifest.json # 仓库元数据(版本、统计、维护者)
├── package.json # Registry 校验入口 ├── package.json # Registry 校验入口
├── schemas/ ├── schemas/ # legacy entry、仓库 manifest 与 catalog sidecar Schema
│ └── registry-entry.schema.json # Draft-07 严格判别 Schema ├── scripts/ # Registry v4 校验入口与回归测试
├── scripts/ ├── scripts/catalog/ # 零依赖离线 validator 与 node:test
│ ├── validate-registry.mjs # 仓库语义校验
│ └── validate-registry.test.mjs # 失败关闭回归测试
├── entries/ # 🔑 所有注册表条目(统一格式) ├── entries/ # 🔑 所有注册表条目(统一格式)
│ ├── n8n/ # 示例Docker 应用 │ ├── n8n/ # 示例Docker 应用
│ │ ├── manifest.json # 条目元数据(必需) │ │ ├── manifest.json # 条目元数据(必需)
│ │ ├── catalog-metadata.v1.json # 统一目录元数据(迁移窗口内可选)
│ │ ├── install.md # 安装说明Agent 使用,可选) │ │ ├── install.md # 安装说明Agent 使用,可选)
│ │ └── usage.md # 使用说明Agent 使用,可选) │ │ └── usage.md # 使用说明Agent 使用,可选)
│ ├── playwright-mcp/ # 示例MCP 服务 │ ├── playwright-mcp/ # 示例MCP 服务
@@ -54,7 +53,7 @@ DesireCore 客户端启动时会克隆此仓库,并定期同步更新。用户
| `id` | string | ✅ | 唯一标识,与目录名一致 | | `id` | string | ✅ | 唯一标识,与目录名一致 |
| `name` | string | ✅ | 显示名称 | | `name` | string | ✅ | 显示名称 |
| `type` | string | ✅ | 条目类型:`docker-app` / `mcp` / `http-api` / `external-integration` | | `type` | string | ✅ | 条目类型:`docker-app` / `mcp` / `http-api` / `external-integration` |
| `version` | string | ✅ | 语义版本号 | | `version` | string | ✅ | 上游原始版本字符串;可为 SemVer、CalVer 或不透明版本 |
| `description` | string | ✅ | 一行功能摘要 | | `description` | string | ✅ | 一行功能摘要 |
| `author` | string | | 作者或组织 | | `author` | string | | 作者或组织 |
| `tags` | string[] | | 搜索标签 | | `tags` | string[] | | 搜索标签 |
@@ -150,6 +149,64 @@ external integration 目录必须恰好包含一个常规文件 `manifest.json`
`external-integration` 不允许 `usage.md` 或其他附加文件;所有用户可见披露必须是经过 Schema 约束的 manifest 字段。 `external-integration` 不允许 `usage.md` 或其他附加文件;所有用户可见披露必须是经过 Schema 约束的 manifest 字段。
### catalog-metadata.v1.json迁移窗口内可选
固定路径为 `entries/<id>/catalog-metadata.v1.json`Schema 为
`schemas/catalog-metadata.v1.schema.json`。它给新客户端提供统一目录读模型所需的条目事实,
不会替换 `manifest.json`;旧客户端继续只读 legacy manifest新客户端在 sidecar 缺失时也必须
回退到 legacy adapter。
Sidecar 只允许声明条目自身事实:
- `identity``app` / `service` 与来源内 ID。不得写 `sourceId``catalogSourceId`
- `presentation`:默认语言和真实 i18n 文案。未翻译文本只能保留一个 locale不能复制成伪双语。
- `release`:显式 `known` / `unknown`;已知时保留原始版本与 `semver` / `calver` / `opaque` 解释方式。
- `timestamps`:目录更新、内容发布、治理审核和上游观察时间相互独立;客户端同步时间不写入。
- `provenance.content`:内容上游及其不可变 ref/digestCatalog 仓库来源由客户端受信上下文注入。
- `governance`listing-only/installable、维护者、许可证、品牌和审核证据。
- `compatibility`:平台必须显式区分 `known` / `all` / `unknown`
- `spec`App 分类、入口类型与放置策略,或 Service 协议、鉴权类型、能力与工具数安装命令、endpoint 和凭据不进入统一目录元数据。
示例(证据不足的条目必须失败关闭为 listing-only
```json
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": { "kind": "app", "id": "my-app" },
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": { "name": "My App", "summary": "应用摘要" }
},
"tags": ["demo"]
},
"release": { "state": "known", "version": "1.0.0", "versionScheme": "semver" },
"timestamps": {
"catalogUpdatedAt": { "state": "unknown" },
"releasePublishedAt": { "state": "unknown" },
"reviewedAt": { "state": "unknown" },
"upstreamObservedAt": { "state": "unknown" }
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": { "state": "unknown" },
"redistribution": "verify-package-terms"
},
"compatibility": { "platforms": { "state": "unknown" } },
"spec": { "kind": "app", "category": "tools" }
}
```
`official` 不是条目可以自报的属性。即使本仓库由 DesireCore 维护,第三方应用或服务也不能因此
自动成为 DesireCore 官方内容。正文中的 `sourceId``catalogSourceId``official` 声明会被
validator 拒绝;受信 Provider 身份只能由客户端同步器注入。
只有同时具备 HTTPS 不可变内容来源、已知许可证、分发策略、双维护者身份、品牌依据以及绑定到
同一 ref 的审核记录时,`availability` 才能写 `installable`。Git 分支名、`latest`、浮动包版本或
缺少 SHA-256 的制品只能保持 `listing-only`
## 添加新条目 ## 添加新条目
### 添加 Docker 应用 ### 添加 Docker 应用
@@ -212,6 +269,10 @@ cat > entries/my-app/usage.md << 'EOF'
EOF EOF
``` ```
新条目必须添加 `catalog-metadata.v1.json`。3.1 的全量迁移已经完成,根
`manifest.json#catalogMetadata.required` 为 `true`CI 会阻断缺失 sidecar 的条目;
`legacyFallback` 继续保留,供尚未退出兼容窗口的旧客户端读取原始 manifest。
### 添加 MCP 服务 ### 添加 MCP 服务
```bash ```bash
@@ -307,6 +368,26 @@ npm test
校验包含 JSON Schema、目录与 ID、全局唯一性、根版本、统计、来源注入字段、external 单文件布局、固定 Kimi ID、完整官方 URL、扩展 ID、组件/权限/准入集合、真实日历日期和不可变供应链审核记录。 校验包含 JSON Schema、目录与 ID、全局唯一性、根版本、统计、来源注入字段、external 单文件布局、固定 Kimi ID、完整官方 URL、扩展 ID、组件/权限/准入集合、真实日历日期和不可变供应链审核记录。
Schema v4 同时保留 `catalog-metadata.v1.json` 统一目录 sidecar、strict validator 与 legacy fallback。
## 本地验证
本仓库的验证器只依赖 Node.js 22不联网、不读取 Git 历史,也不会把当前时间伪造成目录事实:
```bash
# 逐条验证根 stats、目录 ID、legacy manifest 与全部 sidecar
node scripts/catalog/validate-registry.mjs --require-sidecars
# 机器可读报告
node scripts/catalog/validate-registry.mjs --json
# 正反 fixture 与当前目录兼容回归
node --test scripts/catalog/*.test.mjs
```
证据路径必须是安全相对路径,禁止绝对路径和 `..` 穿越。离线 validator 只证明字段结构、路径安全
和 ref/digest 一致性,不把网络不可达或未验证的远端事实伪造成通过。
## 同步机制 ## 同步机制
DesireCore 客户端的同步流程: DesireCore 客户端的同步流程:
@@ -334,6 +415,7 @@ DesireCore 客户端的同步流程:
3. 按上述格式添加 `manifest.json`,旧三类按需添加 `install.md``usage.md` 3. 按上述格式添加 `manifest.json`,旧三类按需添加 `install.md``usage.md`
4. 更新根目录 `manifest.json` 中的 `stats` 统计 4. 更新根目录 `manifest.json` 中的 `stats` 统计
5. 执行 `npm ci && npm test` 5. 执行 `npm ci && npm test`
并运行 `node --test scripts/catalog/*.test.mjs``node scripts/catalog/validate-registry.mjs --require-sidecars`
6. 提交 PR 并描述变更内容、来源和验证结果 6. 提交 PR 并描述变更内容、来源和验证结果
7. 等待审核合并 7. 等待审核合并

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "anythingllm"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "AnythingLLM",
"summary": "全能型 AI 桌面应用,支持 RAG、Agent 和文档对话",
"description": "AnythingLLM 是一个全能型 AI 应用,可以将任何文档、资源或内容转化为上下文,供任何 LLM 在聊天中使用。支持多用户管理、权限控制和嵌入式对话。"
},
"en-US": {
"name": "AnythingLLM",
"summary": "All-in-one AI desktop application with RAG, agents, and document chat",
"description": "AnythingLLM is an all-in-one AI application that turns documents, resources, or other content into context for use with any LLM in chat. It supports multi-user management, access control, and embedded chat."
}
},
"tags": [
"RAG",
"Desktop",
"文档对话"
]
},
"release": {
"state": "known",
"version": "1.11.1",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-14",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/Mintplex-Labs/anything-llm"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Mintplex Labs",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "tools"
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "baidu-map"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "百度地图 API",
"summary": "地理位置服务,支持地理编码、路线规划和 POI 搜索"
},
"en-US": {
"name": "Baidu Maps API",
"summary": "Geolocation service with geocoding, route planning, and POI search"
}
},
"tags": [
"地图",
"地理位置",
"POI"
]
},
"release": {
"state": "known",
"version": "3.0",
"versionScheme": "opaque"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "百度",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "http",
"authType": "unknown",
"capabilities": [
"geocoding",
"route_plan",
"poi_search",
"distance_calc"
]
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "coze"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Coze",
"summary": "字节跳动 AI Bot 开发平台(开源社区版)",
"description": "Coze 是一个 AI 聊天机器人和应用开发平台,提供 LLM、知识库、插件和工作流等能力。支持快速构建、测试和部署 AI Bot无需编程经验。"
},
"en-US": {
"name": "Coze",
"summary": "ByteDance AI bot development platform (open-source community edition)",
"description": "Coze is an AI chatbot and application development platform that provides LLM, knowledge base, plugin, and workflow capabilities. It supports rapidly building, testing, and deploying AI bots without requiring programming experience."
}
},
"tags": [
"Bot",
"LLM",
"插件"
]
},
"release": {
"state": "known",
"version": "0.5.1",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-16",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/coze-dev/coze-studio"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "coze-dev",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "ai-platform"
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "db-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Database MCP",
"summary": "PostgreSQL 数据库查询与管理,支持 SQL 执行和 Schema 浏览"
},
"en-US": {
"name": "Database MCP",
"summary": "PostgreSQL database querying and management with SQL execution and schema browsing"
}
},
"tags": [
"数据库",
"PostgreSQL",
"SQL"
]
},
"release": {
"state": "known",
"version": "0.6.2",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://www.npmjs.com/package/@modelcontextprotocol/server-postgres"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Anthropic",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"sql_query",
"schema_browse",
"data_export"
],
"toolCount": 1
}
}

View File

@@ -0,0 +1,79 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "deepl-translate"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "DeepL 翻译",
"summary": "高质量多语言翻译服务,支持 35+ 语言互译和文档翻译"
},
"en-US": {
"name": "DeepL Translation",
"summary": "High-quality multilingual translation service supporting 35+ languages and document translation"
}
},
"tags": [
"翻译",
"多语言",
"NLP"
]
},
"release": {
"state": "known",
"version": "2.0",
"versionScheme": "opaque"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "DeepL",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "http",
"authType": "unknown",
"capabilities": [
"text_translate",
"doc_translate",
"language_detect"
]
}
}

View File

@@ -0,0 +1,79 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "dify-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Dify MCP Bridge",
"summary": "Dify 平台 Agent 工具集桥接,支持调用 Dify 内置工具和自定义工具"
},
"en-US": {
"name": "Dify MCP Bridge",
"summary": "Bridge to the Dify platform's agent tools, including built-in and custom tools"
}
},
"tags": [
"Agent",
"工具集",
"Workflow"
]
},
"release": {
"state": "known",
"version": "1.13.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Dify.AI",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"tool_invoke",
"workflow_trigger",
"knowledge_query"
]
}
}

View File

@@ -0,0 +1,79 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "dify-rag"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Dify RAG API",
"summary": "Dify 知识库检索服务,支持语义搜索和多知识库联合检索"
},
"en-US": {
"name": "Dify RAG API",
"summary": "Dify knowledge base retrieval service with semantic search and federated search across multiple knowledge bases"
}
},
"tags": [
"RAG",
"知识库",
"检索"
]
},
"release": {
"state": "known",
"version": "1.13.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Dify.AI",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "http",
"authType": "unknown",
"capabilities": [
"semantic_search",
"multi_dataset",
"relevance_ranking"
]
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "dify"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Dify",
"summary": "开源 AI 应用开发平台,支持 RAG、Agent、工作流编排",
"description": "Dify 是一个开源的 LLM 应用开发平台,提供从 Agent 构建到 AI Workflow 编排、RAG 检索、模型管理等能力,轻松构建和运营生成式 AI 原生应用。支持数百种模型接入。"
},
"en-US": {
"name": "Dify",
"summary": "Open-source AI application development platform with RAG, agents, and workflow orchestration",
"description": "Dify is an open-source LLM application development platform that covers agent building, AI workflow orchestration, RAG retrieval, and model management. It helps teams build and operate generative AI-native applications and supports hundreds of models."
}
},
"tags": [
"LLM",
"RAG",
"Agent",
"Workflow"
]
},
"release": {
"state": "known",
"version": "1.13.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-16",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/langgenius/dify"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Dify.AI",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "ai-platform"
}
}

View File

@@ -0,0 +1,90 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "electron-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Electron MCP",
"summary": "Electron 桌面应用自动化 — 经 CDP 发现窗口、执行 JS、点击填表、截图、读日志"
},
"en-US": {
"name": "Electron MCP",
"summary": "Electron desktop automation via CDP for window discovery, JavaScript execution, clicks, form input, screenshots, and log reading"
}
},
"tags": [
"Electron",
"桌面应用",
"自动化",
"调试",
"CDP"
]
},
"release": {
"state": "known",
"version": "1.5.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-07-29",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://www.npmjs.com/package/electron-mcp-server"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "halilural",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"window_discover",
"js_evaluate",
"ui_interact",
"element_inspect",
"screenshot",
"log_read"
],
"toolCount": 4
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "fetch-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Fetch MCP",
"summary": "网页内容获取与转换,将网页转为 Markdown 供 LLM 使用"
},
"en-US": {
"name": "Fetch MCP",
"summary": "Fetches and converts web content into Markdown for LLM use"
}
},
"tags": [
"网页",
"抓取",
"Markdown"
]
},
"release": {
"state": "known",
"version": "2025.4.7",
"versionScheme": "calver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://pypi.org/project/mcp-server-fetch/"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Anthropic",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"url_fetch",
"html_to_markdown",
"content_extract"
],
"toolCount": 1
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "fs-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Filesystem MCP",
"summary": "本地文件系统操作,支持读写、搜索、监控文件变化"
},
"en-US": {
"name": "Filesystem MCP",
"summary": "Local filesystem operations with file reading, writing, search, and change monitoring"
}
},
"tags": [
"文件系统",
"本地"
]
},
"release": {
"state": "known",
"version": "2026.1.14",
"versionScheme": "calver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Anthropic",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"file_read",
"file_write",
"file_search",
"file_watch"
],
"toolCount": 13
}
}

View File

@@ -0,0 +1,86 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "github-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "GitHub MCP",
"summary": "代码仓库操作,支持 PR、Issue、代码搜索和仓库管理"
},
"en-US": {
"name": "GitHub MCP",
"summary": "Repository operations for pull requests, issues, code search, and repository management"
}
},
"tags": [
"代码",
"Git",
"PR"
]
},
"release": {
"state": "known",
"version": "0.32.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "container",
"url": "https://ghcr.io/github/github-mcp-server"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "GitHub",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "bearer",
"capabilities": [
"repo_read",
"pr_manage",
"issue_manage",
"code_search"
],
"toolCount": 51
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "lobechat"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "LobeChat",
"summary": "开源多 Agent 协作工作空间,支持多模型和插件",
"description": "LobeChat 是一个开源的多 Agent 协作工作空间支持多模型服务提供商OpenAI / Claude / Gemini / Ollama 等),多模态和可扩展的插件系统。一键免费部署私有 ChatGPT/Claude 应用。"
},
"en-US": {
"name": "LobeChat",
"summary": "Open-source multi-agent collaboration workspace with multi-model and plugin support",
"description": "LobeChat is an open-source multi-agent collaboration workspace that supports multiple model providers (OpenAI, Claude, Gemini, Ollama, and more), multimodal interactions, and an extensible plugin system. It enables one-click deployment of private ChatGPT or Claude applications."
}
},
"tags": [
"Chat",
"多模型",
"Agent",
"插件"
]
},
"release": {
"state": "known",
"version": "2.1.38",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-14",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/lobehub/lobe-chat"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "LobeHub",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "chat"
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "memory-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Memory MCP",
"summary": "基于知识图谱的持久化记忆服务,支持实体和关系的存取"
},
"en-US": {
"name": "Memory MCP",
"summary": "Persistent memory service built on a knowledge graph, with entity and relationship storage"
}
},
"tags": [
"记忆",
"知识图谱",
"持久化"
]
},
"release": {
"state": "known",
"version": "2026.1.26",
"versionScheme": "calver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://www.npmjs.com/package/@modelcontextprotocol/server-memory"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Anthropic",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"entity_store",
"relation_store",
"graph_query"
],
"toolCount": 9
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "n8n"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "n8n",
"summary": "可视化工作流自动化平台,支持 400+ 集成",
"description": "n8n 是一个可扩展的工作流自动化工具。使用公平代码许可,拥有原生 AI 能力,可以连接任何东西。支持自托管,提供丰富的第三方服务集成节点。"
},
"en-US": {
"name": "n8n",
"summary": "Visual workflow automation platform with 400+ integrations",
"description": "n8n is an extensible workflow automation tool. It uses a fair-code license, includes native AI capabilities, and can connect to almost anything. It supports self-hosting and offers a broad range of integration nodes for third-party services."
}
},
"tags": [
"Automation",
"Workflow",
"Integration"
]
},
"release": {
"state": "known",
"version": "2.10.4",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-14",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/n8n-io/n8n"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "n8n-io",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "workflow"
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "open-webui"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Open WebUI",
"summary": "自托管的 AI 对话界面,支持 Ollama 和 OpenAI 兼容 API",
"description": "Open WebUI 是一个可扩展的自托管 AI 界面,支持完全离线操作。支持多种 LLM 运行器,包括 Ollama 和 OpenAI 兼容 API内置 RAG 集成、网页浏览、代码执行等功能。"
},
"en-US": {
"name": "Open WebUI",
"summary": "Self-hosted AI chat interface for Ollama and OpenAI-compatible APIs",
"description": "Open WebUI is an extensible, self-hosted AI interface that can operate fully offline. It supports multiple LLM runners, including Ollama and OpenAI-compatible APIs, and includes RAG integration, web browsing, code execution, and more."
}
},
"tags": [
"Chat",
"Ollama",
"WebUI"
]
},
"release": {
"state": "known",
"version": "0.8.10",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-14",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/open-webui/open-webui"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Open WebUI",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "chat"
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "openclaw"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "OpenClaw",
"summary": "开源个人 AI 助手,支持多渠道网关和本地优先架构",
"description": "OpenClaw 是一个开源的个人 AI 助手平台提供多渠道网关WhatsApp、Telegram、Slack、Discord 等)、本地优先架构、会话管理和工具策略控制等能力。支持流式输出、对话压缩和 hooks 扩展。"
},
"en-US": {
"name": "OpenClaw",
"summary": "Open-source personal AI assistant with a multi-channel gateway and local-first architecture",
"description": "OpenClaw is an open-source personal AI assistant platform with a multi-channel gateway (WhatsApp, Telegram, Slack, Discord, and more), a local-first architecture, session management, and tool policy controls. It supports streaming output, conversation compaction, and hook-based extensions."
}
},
"tags": [
"Agent",
"Gateway",
"Multi-Channel",
"Local-First"
]
},
"release": {
"state": "known",
"version": "2026.3.8",
"versionScheme": "calver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-16",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/openclaw/openclaw"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "OpenClaw",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "ai-platform"
}
}

View File

@@ -0,0 +1,88 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "playwright-mcp"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "Playwright MCP",
"summary": "浏览器自动化 — 网页导航、截图、表单填写、DOM 操作、PDF 生成"
},
"en-US": {
"name": "Playwright MCP",
"summary": "Browser automation for navigation, screenshots, form filling, DOM operations, and PDF generation"
}
},
"tags": [
"浏览器",
"自动化",
"Playwright"
]
},
"release": {
"state": "known",
"version": "0.0.68",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "package",
"url": "https://www.npmjs.com/package/@playwright/mcp"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "Microsoft",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "mcp",
"authType": "unknown",
"capabilities": [
"page_navigate",
"screenshot",
"dom_query",
"form_fill",
"pdf_generate",
"content_extract"
],
"toolCount": 34
}
}

View File

@@ -0,0 +1,79 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "ragflow-pipeline"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "RagFlow Pipeline",
"summary": "深度文档理解 RAG 引擎,支持复杂格式文档解析和多级检索"
},
"en-US": {
"name": "RAGFlow Pipeline",
"summary": "Deep document-understanding RAG engine for complex document parsing and multi-stage retrieval"
}
},
"tags": [
"RAG",
"文档理解",
"Pipeline"
]
},
"release": {
"state": "known",
"version": "0.24.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "InfiniFlow",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "http",
"authType": "unknown",
"capabilities": [
"doc_parsing",
"deep_retrieval",
"citation"
]
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "app",
"id": "ragflow"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "RagFlow",
"summary": "基于深度文档理解的开源 RAG 引擎",
"description": "RagFlow 是一款基于深度文档理解构建的开源 RAG 引擎。可以为各种规模的企业及个人提供流畅的 RAG 工作流结合大语言模型LLM针对用户各类不同的复杂格式数据提供可靠的问答以及有理有据的引用。"
},
"en-US": {
"name": "RagFlow",
"summary": "Open-source RAG engine built on deep document understanding",
"description": "RAGFlow is an open-source RAG engine built on deep document understanding. It provides smooth RAG workflows for organizations and individuals at any scale, combining large language models (LLMs) with complex, diverse data formats to deliver reliable answers with grounded citations."
}
},
"tags": [
"RAG",
"文档理解",
"知识库"
]
},
"release": {
"state": "known",
"version": "0.24.0",
"versionScheme": "semver"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-06-14",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {
"content": {
"kind": "git",
"url": "https://github.com/infiniflow/ragflow"
}
},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "InfiniFlow",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "app",
"category": "rag"
}
}

View File

@@ -0,0 +1,83 @@
{
"$schema": "../../schemas/catalog-metadata.v1.schema.json",
"schemaVersion": 1,
"identity": {
"kind": "service",
"id": "wecom-webhook"
},
"presentation": {
"defaultLocale": "zh-CN",
"i18n": {
"zh-CN": {
"name": "企业微信 Webhook",
"summary": "企业微信机器人消息通知支持文本、Markdown、图片、图文、文件、语音和卡片消息"
},
"en-US": {
"name": "WeCom Webhook",
"summary": "WeCom bot notifications supporting text, Markdown, images, news, files, voice messages, and cards"
}
},
"tags": [
"通知",
"企业微信",
"Webhook"
]
},
"release": {
"state": "known",
"version": "1.0",
"versionScheme": "opaque"
},
"timestamps": {
"catalogUpdatedAt": {
"state": "known",
"value": "2026-03-09",
"precision": "day"
},
"releasePublishedAt": {
"state": "unknown"
},
"reviewedAt": {
"state": "unknown"
},
"upstreamObservedAt": {
"state": "unknown"
}
},
"provenance": {},
"governance": {
"availability": "listing-only",
"license": {
"state": "unknown"
},
"redistribution": "verify-package-terms",
"upstreamMaintainer": {
"name": "腾讯",
"verified": false
}
},
"compatibility": {
"platforms": {
"state": "known",
"values": [
"macos",
"windows",
"linux"
]
}
},
"spec": {
"kind": "service",
"protocol": "http",
"authType": "unknown",
"capabilities": [
"text_message",
"markdown_message",
"image_message",
"news_message",
"file_message",
"voice_message",
"card_message"
]
}
}

View File

@@ -6,7 +6,7 @@
"description": "DesireCore 官方注册表 — Docker 应用、MCP 服务、HTTP API 和第三方外部集成的统一目录", "description": "DesireCore 官方注册表 — Docker 应用、MCP 服务、HTTP API 和第三方外部集成的统一目录",
"maintainer": "DesireCore Team", "maintainer": "DesireCore Team",
"repository": "https://github.com/desirecore/registry", "repository": "https://github.com/desirecore/registry",
"lastUpdated": "2026-08-30", "lastUpdated": "2026-08-31",
"stats": { "stats": {
"totalEntries": 22, "totalEntries": 22,
"dockerApps": 8, "dockerApps": 8,
@@ -14,5 +14,12 @@
"httpApis": 5, "httpApis": 5,
"externalIntegrations": 1 "externalIntegrations": 1
}, },
"dataVersion": "4.0.0" "dataVersion": "4.0.0",
"catalogMetadata": {
"version": "1.0.0",
"schema": "schemas/catalog-metadata.v1.schema.json",
"sidecarPath": "entries/<id>/catalog-metadata.v1.json",
"required": true,
"legacyFallback": true
}
} }

View File

@@ -0,0 +1,262 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/desirecore/registry/schemas/catalog-metadata.v1.schema.json",
"title": "DesireCore Registry catalog metadata v1 sidecar",
"description": "CatalogItemV1 的 Registry 来源事实子集。受信 catalogSourceId、catalog provenance 和 official 身份由客户端同步器注入,禁止写入本文档。",
"type": "object",
"required": ["schemaVersion", "identity", "presentation", "release", "timestamps", "provenance", "governance", "compatibility", "spec"],
"properties": {
"$schema": { "const": "../../schemas/catalog-metadata.v1.schema.json" },
"schemaVersion": { "const": 1 },
"identity": {
"type": "object",
"required": ["kind", "id"],
"properties": {
"kind": { "enum": ["app", "service"], "description": "canonical 条目类型Registry mcp/http-api 都归一为 service。" },
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "description": "来源内条目 ID受信来源 ID 不属于 sidecar。" }
},
"additionalProperties": false
},
"presentation": {
"type": "object",
"required": ["defaultLocale", "i18n", "tags"],
"properties": {
"defaultLocale": { "type": "string", "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$" },
"i18n": {
"type": "object",
"minProperties": 1,
"propertyNames": { "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$" },
"additionalProperties": { "$ref": "#/$defs/localeText" }
},
"tags": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 120 }, "uniqueItems": true }
},
"additionalProperties": false
},
"release": { "$ref": "#/$defs/release" },
"timestamps": {
"type": "object",
"required": ["catalogUpdatedAt", "releasePublishedAt", "reviewedAt", "upstreamObservedAt"],
"properties": {
"catalogUpdatedAt": { "$ref": "#/$defs/timestamp" },
"releasePublishedAt": { "$ref": "#/$defs/timestamp" },
"reviewedAt": { "$ref": "#/$defs/timestamp" },
"upstreamObservedAt": { "$ref": "#/$defs/timestamp" }
},
"additionalProperties": false,
"description": "目录修改、内容发布、审核和上游观察时间彼此独立syncedAt 不属于上游元数据。"
},
"provenance": {
"type": "object",
"properties": { "content": { "$ref": "#/$defs/contentSource" } },
"additionalProperties": false,
"description": "只声明内容来源。Catalog 仓库来源由同步器根据受信 repo 上下文注入。"
},
"governance": {
"type": "object",
"required": ["availability", "license", "redistribution"],
"properties": {
"availability": { "enum": ["listing-only", "installable"] },
"stewardship": { "enum": ["partner", "community", "pointer"], "description": "正文不得自报 official受信官方身份只能由同步器注入。" },
"license": { "$ref": "#/$defs/license" },
"redistribution": { "enum": ["allowed", "source-pointer-only", "verify-package-terms"] },
"listingMaintainer": { "$ref": "#/$defs/maintainer" },
"upstreamMaintainer": { "$ref": "#/$defs/maintainer" },
"branding": { "$ref": "#/$defs/branding" },
"compliance": { "$ref": "#/$defs/compliance" }
},
"additionalProperties": false
},
"compatibility": {
"type": "object",
"required": ["platforms"],
"properties": {
"platforms": { "$ref": "#/$defs/platforms" },
"requiredClientVersion": {
"type": "string",
"pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
}
},
"additionalProperties": false
},
"spec": { "oneOf": [{ "$ref": "#/$defs/appSpec" }, { "$ref": "#/$defs/serviceSpec" }] }
},
"additionalProperties": false,
"$defs": {
"localeText": {
"type": "object",
"required": ["name", "summary"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"summary": { "type": "string", "minLength": 1 },
"description": { "type": "string", "minLength": 1 }
},
"additionalProperties": false
},
"release": {
"oneOf": [
{
"type": "object",
"required": ["state"],
"properties": { "state": { "const": "unknown" } },
"additionalProperties": false
},
{
"type": "object",
"required": ["state", "version", "versionScheme"],
"properties": {
"state": { "const": "known" },
"version": { "type": "string", "minLength": 1, "maxLength": 160 },
"versionScheme": { "enum": ["semver", "calver", "opaque"] }
},
"additionalProperties": false
}
]
},
"timestamp": {
"oneOf": [
{
"type": "object",
"required": ["state"],
"properties": { "state": { "const": "unknown" } },
"additionalProperties": false
},
{
"type": "object",
"required": ["state", "value", "precision"],
"properties": {
"state": { "const": "known" },
"value": { "type": "string" },
"precision": { "enum": ["day", "second"] }
},
"additionalProperties": false
}
]
},
"contentSource": {
"type": "object",
"required": ["kind", "url"],
"properties": {
"kind": { "enum": ["git", "web", "zip", "release", "package", "container"] },
"url": { "type": "string", "format": "uri" },
"ref": { "type": "string", "minLength": 1, "maxLength": 500 },
"sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" },
"path": { "type": "string", "minLength": 1 }
},
"additionalProperties": false
},
"license": {
"oneOf": [
{
"type": "object",
"required": ["state"],
"properties": { "state": { "const": "unknown" } },
"additionalProperties": false
},
{
"type": "object",
"required": ["state", "value"],
"properties": {
"state": { "const": "known" },
"value": { "type": "string", "minLength": 1, "maxLength": 240 },
"evidencePath": { "type": "string", "minLength": 1 }
},
"additionalProperties": false
}
]
},
"maintainer": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"url": { "type": "string", "format": "uri" },
"verified": { "type": "boolean", "description": "身份是否已验证;不代表许可证授权或官方身份。" }
},
"additionalProperties": false
},
"branding": {
"type": "object",
"required": ["relationship", "nameUsage", "logoStatus"],
"properties": {
"relationship": { "enum": ["partner", "independent-listing"], "description": "sidecar 不接受 official 自报。" },
"nameUsage": { "enum": ["owned", "licensed", "nominative"] },
"logoStatus": { "enum": ["not-used", "license-covered", "trademark-policy-allowed", "permission-obtained"] }
},
"additionalProperties": false
},
"compliance": {
"type": "object",
"required": ["licenseEvidencePath", "reviewedRef", "reviewedAt", "reviewedBy", "upstreamEndorsed"],
"properties": {
"licenseEvidencePath": { "type": "string", "minLength": 1 },
"noticePath": { "type": "string", "minLength": 1 },
"reviewedRef": { "type": "string", "minLength": 1 },
"reviewedAt": { "type": "string" },
"reviewedBy": { "type": "string", "minLength": 1 },
"upstreamEndorsed": { "type": "boolean" }
},
"additionalProperties": false
},
"platforms": {
"oneOf": [
{
"type": "object",
"required": ["state", "values"],
"properties": {
"state": { "const": "known" },
"values": {
"type": "array",
"items": { "enum": ["macos", "windows", "linux", "web", "ios", "android"] },
"minItems": 1,
"uniqueItems": true
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["state"],
"properties": { "state": { "enum": ["all", "unknown"] } },
"additionalProperties": false
}
]
},
"placementPolicy": {
"type": "object",
"required": ["showInMyApps", "allowNavPin", "allowAgentBinding"],
"properties": {
"showInMyApps": { "type": "boolean" },
"allowNavPin": { "type": "boolean" },
"allowAgentBinding": { "type": "boolean" }
},
"additionalProperties": false
},
"appSpec": {
"type": "object",
"required": ["kind"],
"properties": {
"kind": { "const": "app" },
"category": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"entrypointKinds": {
"type": "array",
"items": { "enum": ["builtin", "embedded-web", "external"] },
"uniqueItems": true
},
"placementPolicy": { "$ref": "#/$defs/placementPolicy" }
},
"additionalProperties": false
},
"serviceSpec": {
"type": "object",
"required": ["kind", "protocol"],
"properties": {
"kind": { "const": "service" },
"protocol": { "enum": ["mcp", "http"] },
"authType": { "enum": ["none", "bearer", "header", "query", "oauth", "unknown"] },
"capabilities": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 }, "uniqueItems": true },
"toolCount": { "type": "integer", "minimum": 0 }
},
"additionalProperties": false
}
}
}

View File

@@ -0,0 +1,162 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/desirecore/registry/schemas/registry-entry.v3.schema.json",
"title": "DesireCore Registry legacy entry v3",
"description": "现有 entries/<id>/manifest.json 的兼容 Schema。Catalog sidecar 是增量契约,不替换本格式。",
"oneOf": [
{ "$ref": "#/$defs/dockerApp" },
{ "$ref": "#/$defs/mcp" },
{ "$ref": "#/$defs/httpApi" }
],
"$defs": {
"nonEmptyString": { "type": "string", "minLength": 1 },
"stringArray": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
"platformArray": {
"type": "array",
"items": { "enum": ["macos", "windows", "linux"] },
"minItems": 1,
"uniqueItems": true
},
"common": {
"type": "object",
"required": ["id", "name", "type", "version", "description"],
"properties": {
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"name": { "$ref": "#/$defs/nonEmptyString" },
"type": { "enum": ["docker-app", "mcp", "http-api"] },
"version": { "$ref": "#/$defs/nonEmptyString" },
"author": { "$ref": "#/$defs/nonEmptyString" },
"description": { "$ref": "#/$defs/nonEmptyString" },
"tags": { "$ref": "#/$defs/stringArray" },
"icon": { "$ref": "#/$defs/nonEmptyString" },
"platformSupport": { "$ref": "#/$defs/platformArray" },
"sourceAppId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"sourceAppName": { "$ref": "#/$defs/nonEmptyString" }
}
},
"dockerRequirements": {
"type": "object",
"required": ["docker", "minMemory", "minDisk", "ports"],
"properties": {
"docker": { "type": "boolean" },
"minMemory": { "$ref": "#/$defs/nonEmptyString" },
"minDisk": { "$ref": "#/$defs/nonEmptyString" },
"ports": { "type": "array", "items": { "type": "integer", "minimum": 1, "maximum": 65535 }, "uniqueItems": true }
},
"additionalProperties": false
},
"dockerInstall": {
"type": "object",
"required": ["method", "requirements"],
"properties": {
"method": { "enum": ["docker", "docker-compose"] },
"requirements": { "$ref": "#/$defs/dockerRequirements" },
"configNeeded": { "$ref": "#/$defs/stringArray" }
},
"additionalProperties": false
},
"expose": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "enum": ["mcp", "http-api"] },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"path": { "type": "string", "minLength": 1 },
"name": { "$ref": "#/$defs/nonEmptyString" }
},
"additionalProperties": false
},
"mcpInstall": {
"type": "object",
"required": ["method"],
"properties": {
"method": { "enum": ["npx", "pip", "uvx", "docker", "binary", "go"] },
"packageName": { "$ref": "#/$defs/nonEmptyString" },
"command": { "$ref": "#/$defs/nonEmptyString" },
"args": { "type": "array", "items": { "type": "string" } },
"postInstall": { "$ref": "#/$defs/stringArray" },
"env": { "type": "object", "additionalProperties": { "type": "string" } }
},
"additionalProperties": false
},
"connection": {
"type": "object",
"required": ["transport"],
"properties": {
"transport": { "enum": ["stdio", "streamable-http", "sse"] },
"command": { "$ref": "#/$defs/nonEmptyString" },
"args": { "type": "array", "items": { "type": "string" } },
"url": { "type": "string", "format": "uri" },
"headers": { "type": "object", "additionalProperties": { "type": "string" } },
"env": { "type": "object", "additionalProperties": { "type": "string" } }
},
"additionalProperties": false
},
"dockerApp": {
"type": "object",
"required": ["id", "name", "type", "version", "description", "category", "shortDesc", "fullDesc", "install"],
"properties": {
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"name": { "$ref": "#/$defs/nonEmptyString" },
"type": { "const": "docker-app" },
"version": { "$ref": "#/$defs/nonEmptyString" },
"author": { "$ref": "#/$defs/nonEmptyString" },
"description": { "$ref": "#/$defs/nonEmptyString" },
"tags": { "$ref": "#/$defs/stringArray" },
"icon": { "$ref": "#/$defs/nonEmptyString" },
"iconLetter": { "$ref": "#/$defs/nonEmptyString" },
"platformSupport": { "$ref": "#/$defs/platformArray" },
"category": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"shortDesc": { "$ref": "#/$defs/nonEmptyString" },
"fullDesc": { "$ref": "#/$defs/nonEmptyString" },
"stars": { "type": "number", "minimum": 0 },
"githubUrl": { "type": "string", "format": "uri" },
"install": { "$ref": "#/$defs/dockerInstall" },
"exposes": { "type": "array", "items": { "$ref": "#/$defs/expose" } }
},
"additionalProperties": false
},
"mcp": {
"type": "object",
"required": ["id", "name", "type", "version", "description", "capabilities", "connection"],
"properties": {
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"name": { "$ref": "#/$defs/nonEmptyString" },
"type": { "const": "mcp" },
"version": { "$ref": "#/$defs/nonEmptyString" },
"author": { "$ref": "#/$defs/nonEmptyString" },
"description": { "$ref": "#/$defs/nonEmptyString" },
"tags": { "$ref": "#/$defs/stringArray" },
"icon": { "$ref": "#/$defs/nonEmptyString" },
"platformSupport": { "$ref": "#/$defs/platformArray" },
"capabilities": { "$ref": "#/$defs/stringArray" },
"toolCount": { "type": "integer", "minimum": 0 },
"install": { "$ref": "#/$defs/mcpInstall" },
"connection": { "$ref": "#/$defs/connection" },
"sourceAppId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"sourceAppName": { "$ref": "#/$defs/nonEmptyString" }
},
"additionalProperties": false
},
"httpApi": {
"type": "object",
"required": ["id", "name", "type", "version", "description", "endpoint", "capabilities"],
"properties": {
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"name": { "$ref": "#/$defs/nonEmptyString" },
"type": { "const": "http-api" },
"version": { "$ref": "#/$defs/nonEmptyString" },
"author": { "$ref": "#/$defs/nonEmptyString" },
"description": { "$ref": "#/$defs/nonEmptyString" },
"tags": { "$ref": "#/$defs/stringArray" },
"icon": { "$ref": "#/$defs/nonEmptyString" },
"platformSupport": { "$ref": "#/$defs/platformArray" },
"endpoint": { "type": "string", "format": "uri" },
"capabilities": { "$ref": "#/$defs/stringArray" },
"sourceAppId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"sourceAppName": { "$ref": "#/$defs/nonEmptyString" }
},
"additionalProperties": false
}
}
}

View File

@@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/desirecore/registry/schemas/registry-manifest.v3.schema.json",
"title": "DesireCore Registry manifest v3",
"description": "Registry 仓库级元数据。catalogMetadata 只广告可选 sidecar 能力,不改变 legacy 条目的读取方式。",
"type": "object",
"required": ["id", "version", "name", "description", "maintainer", "repository", "lastUpdated", "stats", "dataVersion"],
"properties": {
"$schema": { "type": "string", "description": "JSON Schema 标识或路径。" },
"id": { "const": "desirecore-registry-manifest", "description": "Registry 仓库固定标识。" },
"version": { "type": "string", "minLength": 1, "description": "Registry 数据格式版本。" },
"name": { "type": "string", "minLength": 1, "description": "Registry 显示名称。" },
"description": { "type": "string", "minLength": 1, "description": "Registry 用途说明。" },
"maintainer": { "type": "string", "minLength": 1, "description": "Registry 目录维护者。" },
"repository": { "type": "string", "format": "uri", "description": "Registry 仓库地址。" },
"lastUpdated": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "仓库目录元数据最后修改日期,不表示条目内容发布时间。" },
"stats": {
"type": "object",
"description": "必须与 entries/ 实际内容完全一致的统计。",
"required": ["totalEntries", "dockerApps", "mcpServices", "httpApis"],
"properties": {
"totalEntries": { "type": "integer", "minimum": 0 },
"dockerApps": { "type": "integer", "minimum": 0 },
"mcpServices": { "type": "integer", "minimum": 0 },
"httpApis": { "type": "integer", "minimum": 0 },
"externalIntegrations": { "type": "integer", "minimum": 0 }
},
"additionalProperties": false
},
"dataVersion": { "type": "string", "minLength": 1, "description": "仓库数据版本;旧客户端继续按原字段读取。" },
"catalogMetadata": {
"type": "object",
"description": "Catalog metadata v1 的兼容广告。sidecar 在迁移窗口内可选legacy manifest 始终保留。",
"required": ["version", "schema", "sidecarPath", "required", "legacyFallback"],
"properties": {
"version": { "const": "1.0.0" },
"schema": { "const": "schemas/catalog-metadata.v1.schema.json" },
"sidecarPath": { "const": "entries/<id>/catalog-metadata.v1.json" },
"required": { "type": "boolean", "description": "是否要求每个 entry 都具备 sidecar。迁移完成前为 false。" },
"legacyFallback": { "const": true, "description": "旧客户端和缺 sidecar 条目继续读取 manifest.json。" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,173 @@
import { isDeepStrictEqual } from 'node:util'
function joinPath(base, segment) {
if (typeof segment === 'number') return `${base}[${segment}]`
return base === '$' ? `$.${segment}` : `${base}.${segment}`
}
function resolveRef(rootSchema, ref) {
if (!ref.startsWith('#/')) throw new Error(`只支持本地 JSON Pointer $ref收到 ${ref}`)
return ref
.slice(2)
.split('/')
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))
.reduce((current, part) => current?.[part], rootSchema)
}
function typeMatches(value, type) {
if (type === 'null') return value === null
if (type === 'array') return Array.isArray(value)
if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value)
if (type === 'integer') return Number.isInteger(value)
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
return typeof value === type
}
function isUri(value) {
try {
const parsed = new URL(value)
return Boolean(parsed.protocol && parsed.hostname)
} catch {
return false
}
}
function validateNode(value, schema, rootSchema, path, errors) {
if (schema === true) return
if (schema === false) {
errors.push({ path, message: 'Schema 明确拒绝该值' })
return
}
if (!schema || typeof schema !== 'object') return
if (schema.$ref) {
const resolved = resolveRef(rootSchema, schema.$ref)
if (!resolved) {
errors.push({ path, message: `无法解析 $ref ${schema.$ref}` })
return
}
validateNode(value, resolved, rootSchema, path, errors)
return
}
if (Array.isArray(schema.allOf)) {
for (const child of schema.allOf) validateNode(value, child, rootSchema, path, errors)
}
if (Array.isArray(schema.anyOf)) {
const candidates = schema.anyOf.map((child) => {
const childErrors = []
validateNode(value, child, rootSchema, path, childErrors)
return childErrors
})
if (!candidates.some((candidate) => candidate.length === 0)) {
errors.push({ path, message: '值不匹配 anyOf 中的任何分支' })
}
}
if (Array.isArray(schema.oneOf)) {
const candidates = schema.oneOf.map((child) => {
const childErrors = []
validateNode(value, child, rootSchema, path, childErrors)
return childErrors
})
const matches = candidates.filter((candidate) => candidate.length === 0).length
if (matches !== 1) errors.push({ path, message: `值应且仅应匹配 oneOf 的一个分支,实际匹配 ${matches}` })
}
if (schema.not) {
const childErrors = []
validateNode(value, schema.not, rootSchema, path, childErrors)
if (childErrors.length === 0) errors.push({ path, message: '值命中了 not 禁止的结构' })
}
if (schema.if) {
const conditionErrors = []
validateNode(value, schema.if, rootSchema, path, conditionErrors)
const selected = conditionErrors.length === 0 ? schema.then : schema.else
if (selected) validateNode(value, selected, rootSchema, path, errors)
}
if (schema.const !== undefined && !isDeepStrictEqual(value, schema.const)) {
errors.push({ path, message: `必须等于 ${JSON.stringify(schema.const)}` })
}
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => isDeepStrictEqual(value, candidate))) {
errors.push({ path, message: `必须是 ${schema.enum.map((candidate) => JSON.stringify(candidate)).join(' / ')}` })
}
if (schema.type) {
const allowed = Array.isArray(schema.type) ? schema.type : [schema.type]
if (!allowed.some((type) => typeMatches(value, type))) {
errors.push({ path, message: `类型必须是 ${allowed.join(' / ')}` })
return
}
}
if (typeof value === 'string') {
if (schema.minLength !== undefined && value.length < schema.minLength) {
errors.push({ path, message: `长度不能小于 ${schema.minLength}` })
}
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
errors.push({ path, message: `长度不能大于 ${schema.maxLength}` })
}
if (schema.pattern && !new RegExp(schema.pattern, 'u').test(value)) {
errors.push({ path, message: `不匹配 pattern ${schema.pattern}` })
}
if (schema.format === 'uri' && !isUri(value)) errors.push({ path, message: '必须是绝对 URI' })
if (schema.format === 'date-time' && Number.isNaN(Date.parse(value))) {
errors.push({ path, message: '必须是可解析的 date-time' })
}
}
if (typeof value === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) errors.push({ path, message: `不能小于 ${schema.minimum}` })
if (schema.maximum !== undefined && value > schema.maximum) errors.push({ path, message: `不能大于 ${schema.maximum}` })
}
if (Array.isArray(value)) {
if (schema.minItems !== undefined && value.length < schema.minItems) {
errors.push({ path, message: `元素数量不能少于 ${schema.minItems}` })
}
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
errors.push({ path, message: `元素数量不能多于 ${schema.maxItems}` })
}
if (schema.uniqueItems) {
for (let index = 0; index < value.length; index += 1) {
if (value.slice(0, index).some((candidate) => isDeepStrictEqual(candidate, value[index]))) {
errors.push({ path: joinPath(path, index), message: '数组元素必须唯一' })
}
}
}
if (schema.items) {
value.forEach((item, index) => validateNode(item, schema.items, rootSchema, joinPath(path, index), errors))
}
}
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const properties = schema.properties ?? {}
for (const required of schema.required ?? []) {
if (!Object.hasOwn(value, required)) errors.push({ path: joinPath(path, required), message: '缺少必需字段' })
}
if (schema.minProperties !== undefined && Object.keys(value).length < schema.minProperties) {
errors.push({ path, message: `字段数量不能少于 ${schema.minProperties}` })
}
for (const [key, childValue] of Object.entries(value)) {
if (Object.hasOwn(properties, key)) {
validateNode(childValue, properties[key], rootSchema, joinPath(path, key), errors)
} else if (schema.additionalProperties === false) {
errors.push({ path: joinPath(path, key), message: '不允许额外字段' })
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
validateNode(childValue, schema.additionalProperties, rootSchema, joinPath(path, key), errors)
}
if (schema.propertyNames?.pattern && !new RegExp(schema.propertyNames.pattern, 'u').test(key)) {
errors.push({ path: joinPath(path, key), message: `字段名不匹配 pattern ${schema.propertyNames.pattern}` })
}
}
}
}
export function validateJsonSchema(value, schema) {
const errors = []
validateNode(value, schema, schema, '$', errors)
return errors
}

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { validateRegistry } from './validator.mjs'
const scriptDir = dirname(fileURLToPath(import.meta.url))
const defaultRoot = resolve(scriptDir, '..', '..')
const args = process.argv.slice(2)
const rootIndex = args.indexOf('--root')
const root = rootIndex >= 0 ? resolve(args[rootIndex + 1]) : defaultRoot
const json = args.includes('--json')
const requireSidecars = args.includes('--require-sidecars') ? true : undefined
const report = validateRegistry(root, { requireSidecars })
if (json) {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
} else {
const { counts } = report
process.stdout.write(`Registry 校验:${report.ok ? '通过' : '失败'}\n`)
process.stdout.write(`条目 ${counts.totalEntries}App ${counts.dockerApps} / MCP ${counts.mcpServices} / HTTP ${counts.httpApis}\n`)
process.stdout.write(`Catalog sidecar ${counts.sidecars}legacy fallback ${counts.legacyOnly}\n`)
for (const item of report.diagnostics) {
process.stdout.write(`${item.level === 'error' ? 'ERROR' : 'WARN '} ${item.file} ${item.path} [${item.code}] ${item.message}\n`)
}
}
process.exitCode = report.ok ? 0 : 1

View File

@@ -0,0 +1,254 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { basename, join, relative, resolve, sep } from 'node:path'
import { validateJsonSchema } from './json-schema.mjs'
export const CATALOG_SIDECAR_FILENAME = 'catalog-metadata.v1.json'
const UNKNOWN_LICENSES = new Set(['', 'unknown', 'none', 'noassertion', 'unlicensed', 'proprietary-unknown'])
const IMMUTABLE_GIT_REF = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i
const CONTAINER_DIGEST = /^sha256:[a-f0-9]{64}$/i
const SHA256 = /^[a-f0-9]{64}$/i
const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
const CALVER = /^(?:19|20)\d{2}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:[-+][0-9A-Za-z.-]+)?$/
function diagnostic(level, code, file, path, message) {
return { level, code, file, path, message }
}
function readJson(file, diagnostics) {
try {
return JSON.parse(readFileSync(file, 'utf8'))
} catch (error) {
diagnostics.push(diagnostic('error', 'invalid-json', file, '$', error instanceof Error ? error.message : String(error)))
return null
}
}
function schemaDiagnostics(value, schema, file) {
return validateJsonSchema(value, schema).map((error) =>
diagnostic('error', 'schema', file, error.path, error.message),
)
}
function safeRelativePath(value) {
return (
typeof value === 'string' &&
value.length > 0 &&
!value.startsWith('/') &&
!value.startsWith('\\') &&
!value.split(/[\\/]+/u).includes('..') &&
!/^[a-zA-Z]:[\\/]/u.test(value)
)
}
function sameStringSet(left, right) {
if (!Array.isArray(left) || !Array.isArray(right)) return false
return [...left].sort().join('\0') === [...right].sort().join('\0')
}
function isHttpsUrl(value) {
try {
return new URL(value).protocol === 'https:'
} catch {
return false
}
}
function sourceIsImmutable(source) {
if (!source || !isHttpsUrl(source.url) || !safeRelativePath(source.path ?? '.')) return false
if (source.kind === 'git') return IMMUTABLE_GIT_REF.test(source.ref)
if (source.kind === 'container') return CONTAINER_DIGEST.test(source.ref)
if (['web', 'zip', 'release', 'package'].includes(source.kind)) return SHA256.test(source.sha256 ?? '')
return false
}
function timestampShapeIsConsistent(timestamp) {
if (!timestamp) return true
if (timestamp.state === 'unknown') return true
if (timestamp.state !== 'known') return false
if (timestamp.precision === 'day') return /^\d{4}-\d{2}-\d{2}$/u.test(timestamp.value)
if (timestamp.precision === 'second') return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u.test(timestamp.value)
return false
}
function validateSidecarSemantics(manifest, sidecar, file) {
const errors = []
const add = (code, path, message) => errors.push(diagnostic('error', code, file, path, message))
const expectedKind = manifest.type === 'docker-app' ? 'app' : 'service'
if (sidecar.identity?.id !== manifest.id) add('identity-mismatch', '$.identity.id', 'sidecar identity.id 必须与 legacy manifest.id 一致')
if (sidecar.identity?.kind !== expectedKind) add('kind-mismatch', '$.identity.kind', `legacy ${manifest.type} 必须映射为 ${expectedKind}`)
if (sidecar.spec?.kind !== expectedKind) add('spec-kind-mismatch', '$.spec.kind', 'spec.kind 必须与 identity.kind 一致')
if (sidecar.release?.state !== 'known' || sidecar.release.version !== manifest.version) {
add('version-mismatch', '$.release', 'Registry legacy manifest 已声明版本sidecar 必须以 known 状态保留同一版本')
}
if (sidecar.release?.state === 'known' && sidecar.release.versionScheme === 'semver' && !SEMVER.test(sidecar.release.version)) {
add('version-scheme', '$.release.version', '声明 semver 时必须是完整 SemVer')
}
if (sidecar.release?.state === 'known' && sidecar.release.versionScheme === 'calver' && !CALVER.test(sidecar.release.version)) {
add('version-scheme', '$.release.version', '声明 calver 时必须是 YYYY.M.D 形状')
}
const defaultLocale = sidecar.presentation?.defaultLocale
const locales = sidecar.presentation?.i18n ?? {}
const defaultText = locales[defaultLocale]
if (!defaultText) {
add('missing-default-locale', '$.presentation.defaultLocale', 'defaultLocale 必须在 i18n 中存在')
} else {
if (defaultText.name !== manifest.name) add('i18n-legacy-mismatch', `$.presentation.i18n.${defaultLocale}.name`, '默认语言 name 必须与 legacy name 一致')
if (defaultText.summary !== manifest.description) add('i18n-legacy-mismatch', `$.presentation.i18n.${defaultLocale}.summary`, '默认语言 summary 必须与 legacy description 一致')
if (defaultText.description !== undefined && manifest.fullDesc !== undefined && defaultText.description !== manifest.fullDesc) {
add('i18n-legacy-mismatch', `$.presentation.i18n.${defaultLocale}.description`, '默认语言 description 必须与 legacy fullDesc 一致')
}
for (const [locale, text] of Object.entries(locales)) {
if (locale === defaultLocale) continue
if (text.summary === defaultText.summary && (text.description ?? '') === (defaultText.description ?? '')) {
add('duplicate-translation', `$.presentation.i18n.${locale}`, '不同 locale 不能用相同源文案伪装成已翻译内容')
}
}
}
if (!sameStringSet(sidecar.presentation?.tags, manifest.tags ?? [])) {
add('presentation-mismatch', '$.presentation.tags', 'sidecar tags 必须与 legacy tags 集合一致')
}
for (const [name, timestamp] of Object.entries(sidecar.timestamps ?? {})) {
if (!timestampShapeIsConsistent(timestamp)) add('timestamp-precision', `$.timestamps.${name}`, '时间值必须与 day/second precision 一致且使用 UTC')
}
const platforms = sidecar.compatibility?.platforms
if (platforms?.state === 'known' && !sameStringSet(platforms.values, manifest.platformSupport ?? [])) {
add('platform-mismatch', '$.compatibility.platforms.values', 'known 平台必须与 legacy platformSupport 集合一致')
}
if (platforms?.state === 'all' && !sameStringSet(manifest.platformSupport ?? [], ['macos', 'windows', 'linux'])) {
add('platform-mismatch', '$.compatibility.platforms', 'all 只能用于 legacy 已显式声明三平台的条目')
}
if (expectedKind === 'app') {
if (sidecar.spec?.category !== manifest.category) add('spec-mismatch', '$.spec.category', 'App category 必须与 legacy category 一致')
} else {
const expectedProtocol = manifest.type === 'mcp' ? 'mcp' : 'http'
if (sidecar.spec?.protocol !== expectedProtocol) add('spec-mismatch', '$.spec.protocol', `Service protocol 必须是 ${expectedProtocol}`)
if (!sameStringSet(sidecar.spec?.capabilities, manifest.capabilities)) add('spec-mismatch', '$.spec.capabilities', 'Service capabilities 必须与 legacy 集合一致')
if (manifest.toolCount !== undefined && sidecar.spec?.toolCount !== manifest.toolCount) add('spec-mismatch', '$.spec.toolCount', 'toolCount 必须与 legacy 一致')
}
const source = sidecar.provenance?.content
if (source?.path !== undefined && !safeRelativePath(source.path)) add('unsafe-evidence-path', '$.provenance.content.path', '内容 path 必须是安全相对路径')
const compliance = sidecar.governance?.compliance
for (const key of ['licenseEvidencePath', 'noticePath']) {
if (compliance?.[key] !== undefined && !safeRelativePath(compliance[key])) {
add('unsafe-evidence-path', `$.governance.compliance.${key}`, '证据路径必须是安全相对路径')
}
}
if (sidecar.governance?.license?.state === 'known' && sidecar.governance.license.evidencePath !== undefined && !safeRelativePath(sidecar.governance.license.evidencePath)) {
add('unsafe-evidence-path', '$.governance.license.evidencePath', '许可证证据路径必须是安全相对路径')
}
if (compliance && source && compliance.reviewedRef !== source.ref) {
add('review-ref-mismatch', '$.governance.compliance.reviewedRef', '审核 ref 必须与内容来源 ref 完全一致')
}
if (compliance && sidecar.timestamps?.reviewedAt?.state !== 'known') {
add('review-time-mismatch', '$.timestamps.reviewedAt', '存在 compliance 时 reviewedAt 必须是 known')
} else if (compliance && compliance.reviewedAt !== sidecar.timestamps.reviewedAt.value) {
add('review-time-mismatch', '$.governance.compliance.reviewedAt', '治理审核时间必须与 timestamps.reviewedAt 一致')
}
if (sidecar.governance?.availability === 'installable') {
const missing = []
if (!sourceIsImmutable(source)) missing.push('不可变 HTTPS 内容来源')
if (!sidecar.governance.stewardship) missing.push('stewardship')
if (
sidecar.governance.license?.state !== 'known' ||
UNKNOWN_LICENSES.has((sidecar.governance.license.value ?? '').trim().toLowerCase())
) missing.push('已知 license')
if (!['allowed', 'source-pointer-only'].includes(sidecar.governance.redistribution)) missing.push('已复核 redistribution')
if (!sidecar.governance.listingMaintainer?.name || !sidecar.governance.upstreamMaintainer?.name) missing.push('双维护者身份')
if (!sidecar.governance.branding) missing.push('branding 证据')
if (!compliance) missing.push('compliance 证据')
if (missing.length > 0) add('installable-without-evidence', '$.governance.availability', `installable 缺少:${missing.join('、')}`)
}
return errors
}
function findEntryDirs(entriesDir) {
if (!existsSync(entriesDir)) return []
return readdirSync(entriesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
.map((entry) => join(entriesDir, entry.name))
.sort()
}
function relativeFile(root, file) {
return relative(root, file).split(sep).join('/')
}
export function validateRegistry(repoRoot, options = {}) {
const root = resolve(repoRoot)
const diagnostics = []
const manifestFile = join(root, 'manifest.json')
const schemaVersionFile = join(root, 'SCHEMA_VERSION')
const rootSchema = readJson(join(root, 'schemas', 'registry-manifest.v3.schema.json'), diagnostics)
const entrySchema = readJson(join(root, 'schemas', 'registry-entry.v3.schema.json'), diagnostics)
const externalEntrySchema = readJson(join(root, 'schemas', 'registry-entry.schema.json'), diagnostics)
const sidecarSchema = readJson(join(root, 'schemas', 'catalog-metadata.v1.schema.json'), diagnostics)
const rootManifest = readJson(manifestFile, diagnostics)
const entryDirs = findEntryDirs(join(root, 'entries'))
const counts = { totalEntries: 0, dockerApps: 0, mcpServices: 0, httpApis: 0, externalIntegrations: 0, sidecars: 0, legacyOnly: 0 }
if (rootManifest && rootSchema) diagnostics.push(...schemaDiagnostics(rootManifest, rootSchema, relativeFile(root, manifestFile)))
const schemaVersion = existsSync(schemaVersionFile) ? readFileSync(schemaVersionFile, 'utf8').trim() : ''
if (!schemaVersion) diagnostics.push(diagnostic('error', 'missing-schema-version', 'SCHEMA_VERSION', '$', 'SCHEMA_VERSION 不能为空'))
if (rootManifest?.version !== schemaVersion) diagnostics.push(diagnostic('error', 'schema-version-mismatch', 'manifest.json', '$.version', 'manifest.version 必须与 SCHEMA_VERSION 一致'))
const seenIds = new Set()
for (const entryDir of entryDirs) {
const manifestPath = join(entryDir, 'manifest.json')
const manifestRelative = relativeFile(root, manifestPath)
if (!existsSync(manifestPath)) {
diagnostics.push(diagnostic('error', 'missing-entry-manifest', relativeFile(root, entryDir), '$', 'entry 目录缺少 manifest.json'))
continue
}
const entry = readJson(manifestPath, diagnostics)
if (!entry) continue
counts.totalEntries += 1
if (entry.type === 'docker-app') counts.dockerApps += 1
if (entry.type === 'mcp') counts.mcpServices += 1
if (entry.type === 'http-api') counts.httpApis += 1
if (entry.type === 'external-integration') counts.externalIntegrations += 1
const effectiveEntrySchema = entry.type === 'external-integration' ? externalEntrySchema : entrySchema
if (effectiveEntrySchema) diagnostics.push(...schemaDiagnostics(entry, effectiveEntrySchema, manifestRelative))
if (entry.id !== basename(entryDir)) diagnostics.push(diagnostic('error', 'directory-id-mismatch', manifestRelative, '$.id', 'manifest.id 必须与 entries/<id> 目录名一致'))
if (seenIds.has(entry.id)) diagnostics.push(diagnostic('error', 'duplicate-id', manifestRelative, '$.id', 'Registry 条目 ID 重复'))
seenIds.add(entry.id)
const sidecarPath = join(entryDir, CATALOG_SIDECAR_FILENAME)
if (entry.type === 'external-integration') continue
if (!existsSync(sidecarPath)) {
counts.legacyOnly += 1
const requireSidecars = options.requireSidecars ?? rootManifest?.catalogMetadata?.required ?? false
diagnostics.push(diagnostic(requireSidecars ? 'error' : 'warning', 'missing-sidecar', manifestRelative, '$', `缺少可选 ${CATALOG_SIDECAR_FILENAME},继续使用 legacy manifest`))
continue
}
counts.sidecars += 1
const sidecar = readJson(sidecarPath, diagnostics)
if (!sidecar) continue
const sidecarRelative = relativeFile(root, sidecarPath)
if (sidecarSchema) diagnostics.push(...schemaDiagnostics(sidecar, sidecarSchema, sidecarRelative))
diagnostics.push(...validateSidecarSemantics(entry, sidecar, sidecarRelative))
}
if (rootManifest?.stats) {
for (const key of ['totalEntries', 'dockerApps', 'mcpServices', 'httpApis', 'externalIntegrations']) {
if (rootManifest.stats[key] === undefined && key === 'externalIntegrations' && counts[key] === 0) continue
if (rootManifest.stats[key] !== counts[key]) diagnostics.push(diagnostic('error', 'stats-mismatch', 'manifest.json', `$.stats.${key}`, `声明 ${rootManifest.stats[key]},实际 ${counts[key]}`))
}
}
diagnostics.sort((left, right) =>
left.file.localeCompare(right.file) || left.path.localeCompare(right.path) || left.code.localeCompare(right.code),
)
const errors = diagnostics.filter((item) => item.level === 'error')
const warnings = diagnostics.filter((item) => item.level === 'warning')
return { ok: errors.length === 0, root, counts, errors, warnings, diagnostics }
}

View File

@@ -0,0 +1,415 @@
import assert from 'node:assert/strict'
import { afterEach, test } from 'node:test'
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { validateRegistry } from './validator.mjs'
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
const temporaryRoots = []
afterEach(() => {
while (temporaryRoots.length > 0) rmSync(temporaryRoots.pop(), { recursive: true, force: true })
})
function writeJson(file, value) {
mkdirSync(dirname(file), { recursive: true })
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
}
function legacyApp(overrides = {}) {
return {
id: 'demo-app',
name: 'Demo App',
type: 'docker-app',
version: '1.2.3',
author: 'Demo Org',
description: '一个用于测试的应用',
tags: ['demo'],
icon: 'box',
iconLetter: 'D',
platformSupport: ['macos', 'windows', 'linux'],
category: 'tools',
shortDesc: '一个用于测试的应用',
fullDesc: '这是用于验证 Registry 契约的测试应用。',
install: {
method: 'docker',
requirements: { docker: true, minMemory: '1GB', minDisk: '1GB', ports: [8080] },
configNeeded: ['Docker'],
},
...overrides,
}
}
function legacyMcp(overrides = {}) {
return {
id: 'demo-mcp',
name: 'Demo MCP',
type: 'mcp',
version: '2026.8.31',
author: 'Demo Org',
description: '一个用于测试的 MCP 服务',
tags: ['demo'],
icon: 'terminal',
platformSupport: ['macos', 'windows', 'linux'],
capabilities: ['read'],
toolCount: 1,
install: { method: 'npx', packageName: '@demo/mcp', command: 'npx', args: ['-y', '@demo/mcp@1.0.0'] },
connection: { transport: 'stdio', command: 'npx', args: ['-y', '@demo/mcp@1.0.0'] },
...overrides,
}
}
function legacyHttp(overrides = {}) {
return {
id: 'demo-http',
name: 'Demo HTTP',
type: 'http-api',
version: '3.0',
author: 'Demo Org',
description: '一个用于测试的 HTTP 服务',
tags: ['demo'],
icon: 'globe',
platformSupport: ['macos', 'windows', 'linux'],
endpoint: 'https://api.example.com/v1',
capabilities: ['query'],
...overrides,
}
}
function unknownTimestamps(overrides = {}) {
return {
catalogUpdatedAt: { state: 'unknown' },
releasePublishedAt: { state: 'unknown' },
reviewedAt: { state: 'unknown' },
upstreamObservedAt: { state: 'unknown' },
...overrides,
}
}
function appSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'app', id: 'demo-app' },
presentation: {
defaultLocale: 'zh-CN',
i18n: {
'zh-CN': {
name: 'Demo App',
summary: '一个用于测试的应用',
description: '这是用于验证 Registry 契约的测试应用。',
},
},
tags: ['demo'],
},
release: { state: 'known', version: '1.2.3', versionScheme: 'semver' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'all' } },
spec: { kind: 'app', category: 'tools' },
...overrides,
}
}
function mcpSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'service', id: 'demo-mcp' },
presentation: {
defaultLocale: 'zh-CN',
i18n: { 'zh-CN': { name: 'Demo MCP', summary: '一个用于测试的 MCP 服务' } },
tags: ['demo'],
},
release: { state: 'known', version: '2026.8.31', versionScheme: 'calver' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'known', values: ['macos', 'windows', 'linux'] } },
spec: { kind: 'service', protocol: 'mcp', capabilities: ['read'], toolCount: 1 },
...overrides,
}
}
function httpSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'service', id: 'demo-http' },
presentation: {
defaultLocale: 'zh-CN',
i18n: { 'zh-CN': { name: 'Demo HTTP', summary: '一个用于测试的 HTTP 服务' } },
tags: ['demo'],
},
release: { state: 'known', version: '3.0', versionScheme: 'opaque' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'all' } },
spec: { kind: 'service', protocol: 'http', authType: 'unknown', capabilities: ['query'] },
...overrides,
}
}
function makeRegistry(entries, sidecars = new Map(), rootOverrides = {}) {
const root = mkdtempSync(join(tmpdir(), 'registry-validator-'))
temporaryRoots.push(root)
cpSync(join(repoRoot, 'schemas'), join(root, 'schemas'), { recursive: true })
const counts = {
totalEntries: entries.length,
dockerApps: entries.filter((entry) => entry.type === 'docker-app').length,
mcpServices: entries.filter((entry) => entry.type === 'mcp').length,
httpApis: entries.filter((entry) => entry.type === 'http-api').length,
}
writeFileSync(join(root, 'SCHEMA_VERSION'), '3.1.0\n')
writeJson(join(root, 'manifest.json'), {
$schema: 'http://json-schema.org/draft-07/schema#',
id: 'desirecore-registry-manifest',
version: '3.1.0',
name: 'Test Registry',
description: 'Test Registry',
maintainer: 'Test',
repository: 'https://example.com/registry',
lastUpdated: '2026-08-31',
stats: counts,
dataVersion: '3.1.0',
catalogMetadata: {
version: '1.0.0',
schema: 'schemas/catalog-metadata.v1.schema.json',
sidecarPath: 'entries/<id>/catalog-metadata.v1.json',
required: false,
legacyFallback: true,
},
...rootOverrides,
})
for (const entry of entries) {
const entryDir = join(root, 'entries', entry.id)
writeJson(join(entryDir, 'manifest.json'), entry)
if (sidecars.has(entry.id)) writeJson(join(entryDir, 'catalog-metadata.v1.json'), sidecars.get(entry.id))
}
return root
}
function codes(report) {
return report.errors.map((item) => item.code)
}
test('当前 v4 条目保持可读21 个 App/Service 具备 sidecar', () => {
const report = validateRegistry(repoRoot)
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
assert.deepEqual({
totalEntries: report.counts.totalEntries,
dockerApps: report.counts.dockerApps,
mcpServices: report.counts.mcpServices,
httpApis: report.counts.httpApis,
externalIntegrations: report.counts.externalIntegrations,
}, {
totalEntries: 22,
dockerApps: 8,
mcpServices: 8,
httpApis: 5,
externalIntegrations: 1,
})
assert.equal(report.counts.sidecars + report.counts.legacyOnly, report.counts.totalEntries - report.counts.externalIntegrations)
assert.equal(report.warnings.filter((item) => item.code === 'missing-sidecar').length, report.counts.legacyOnly)
})
test('合法 listing-only App sidecar 通过', () => {
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', appSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
assert.equal(report.counts.sidecars, 1)
})
test('合法 MCP sidecar 保留 CalVer、能力和 toolCount', () => {
const report = validateRegistry(makeRegistry([legacyMcp()], new Map([['demo-mcp', mcpSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('合法 HTTP sidecar 保留 opaque 版本、协议、鉴权 unknown 和能力', () => {
const report = validateRegistry(makeRegistry([legacyHttp()], new Map([['demo-http', httpSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('stats 与实际目录不一致时失败', () => {
const root = makeRegistry([legacyApp()], new Map(), { stats: { totalEntries: 99, dockerApps: 8, mcpServices: 0, httpApis: 0 } })
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('stats-mismatch'))
})
test('目录名与 manifest.id 不一致时失败', () => {
const root = makeRegistry([legacyApp()])
const source = join(root, 'entries', 'demo-app')
const target = join(root, 'entries', 'wrong-directory')
cpSync(source, target, { recursive: true })
rmSync(source, { recursive: true, force: true })
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('directory-id-mismatch'))
})
test('legacy type-specific 必需字段缺失时失败', () => {
const entry = legacyMcp()
delete entry.connection
const report = validateRegistry(makeRegistry([entry]))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('schema'))
})
test('sidecar 严格拒绝额外字段和正文 sourceId', () => {
const sidecar = appSidecar({ sourceId: 'registry:official', unexpected: true })
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(report.errors.filter((item) => item.code === 'schema').length >= 2)
})
test('sidecar 拒绝正文 official 治理声明', () => {
const sidecar = appSidecar({
governance: {
availability: 'listing-only',
stewardship: 'official',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('schema'))
})
test('i18n 必须包含默认语言且不能伪造重复翻译', () => {
const missingDefault = appSidecar({
presentation: { defaultLocale: 'en-US', i18n: { 'zh-CN': appSidecar().presentation.i18n['zh-CN'] } },
})
let report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', missingDefault]])))
assert.ok(codes(report).includes('missing-default-locale'))
const duplicated = appSidecar()
duplicated.presentation.i18n['en-US'] = { ...duplicated.presentation.i18n['zh-CN'] }
report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', duplicated]])))
assert.ok(codes(report).includes('duplicate-translation'))
})
test('证据路径拒绝绝对路径和目录穿越', () => {
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref: 'a'.repeat(40), path: '../secret' } },
governance: {
availability: 'listing-only',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
compliance: {
licenseEvidencePath: '/tmp/LICENSE',
reviewedRef: 'a'.repeat(40),
reviewedAt: '2026-08-31',
reviewedBy: 'test',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.equal(codes(report).filter((code) => code === 'unsafe-evidence-path').length, 2)
})
test('mutable source 可 listing-only但不能 installable', () => {
const mutableSource = { kind: 'git', url: 'https://example.com/repo.git', ref: 'main' }
let sidecar = appSidecar({ provenance: { content: mutableSource } })
let report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
sidecar = appSidecar({
provenance: { content: mutableSource },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'installable',
stewardship: 'pointer',
license: { state: 'known', value: 'MIT', evidencePath: 'LICENSE' },
redistribution: 'source-pointer-only',
listingMaintainer: { name: 'DesireCore Team', verified: true },
upstreamMaintainer: { name: 'Demo Org', verified: false },
branding: { relationship: 'independent-listing', nameUsage: 'nominative', logoStatus: 'not-used' },
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: 'main',
reviewedAt: '2026-08-31',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('installable-without-evidence'))
})
test('完整不可变来源与审核证据允许 installable', () => {
const ref = 'a'.repeat(40)
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref } },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'installable',
stewardship: 'pointer',
license: { state: 'known', value: 'MIT', evidencePath: 'LICENSE' },
redistribution: 'source-pointer-only',
listingMaintainer: { name: 'DesireCore Team', verified: true },
upstreamMaintainer: { name: 'Demo Org', verified: false },
branding: { relationship: 'independent-listing', nameUsage: 'nominative', logoStatus: 'not-used' },
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: ref,
reviewedAt: '2026-08-31',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('审核 ref 和审核时间必须绑定同一来源与 timestamps', () => {
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref: 'a'.repeat(40) } },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'listing-only',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: 'b'.repeat(40),
reviewedAt: '2026-08-30',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.ok(codes(report).includes('review-ref-mismatch'))
assert.ok(codes(report).includes('review-time-mismatch'))
})
test('--require-sidecars 等价策略会让 legacy-only 失败', () => {
const report = validateRegistry(makeRegistry([legacyApp()]), { requireSidecars: true })
assert.equal(report.ok, false)
assert.ok(codes(report).includes('missing-sidecar'))
})
test('根 manifest 声明 required 时默认拒绝缺失 sidecar', () => {
const root = makeRegistry([legacyApp()], new Map(), {
catalogMetadata: {
version: '1.0.0',
schema: 'schemas/catalog-metadata.v1.schema.json',
sidecarPath: 'entries/<id>/catalog-metadata.v1.json',
required: true,
legacyFallback: true,
},
})
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('missing-sidecar'))
})