diff --git a/README.md b/README.md index dac8ee9..ed410e8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DesireCore 客户端启动时会克隆此仓库,并定期同步更新。用户 ```text . ├── README.md # 本文件 -├── SCHEMA_VERSION # 数据格式版本号(当前 4.2.0) +├── SCHEMA_VERSION # 数据格式版本号(4.3.0兼容候选,尚未开放新工作台条目) ├── manifest.json # 仓库元数据(版本、统计、维护者) ├── package.json # Registry 校验入口 ├── schemas/ # legacy entry、仓库 manifest 与 catalog sidecar Schema @@ -68,7 +68,19 @@ Catalog sidecar 的 App spec 同步 `productKind`、可选 `extension` 和入口 English: Plugins are App products with host contributions, sharing the canonical catalog and installation identity. Deployment type does not decide product kind. Applications may provide contributions or consume direct dependencies; a plugin must provide a real contribution. Artifact packages contain declarative materials without ports or processes. Dependency product references do not grant installation, activation or permission. Existing listings retain their source and compliance evidence; schema migration does not make listing-only content installable or publish a client release. -## 条目格式 +## 固定工作台应用(4.3兼容候选) + +4.3保留全部4.2旧分支,新增 `web-bundle` 与带 `appRelease` 的 `native-app`/`docker-app` 严格分支。网页包不冒充无执行声明artifact;宿主/容器服务应用使用 `install.method: release`,运行知识与依赖在固定清单中,不限定Node技术栈,也不自动执行说明文字。Docker显式声明固定imageRefs,每项是repository@sha256,复用原容器/执行/服务owner;没有真实身份adapter时保留Agent管理,不因为Schema合法声称直开。新分支声明来源无关releaseDigest、包内manifestPath/definitionPath与实际能力要求,source.sha256仍为下载归档原始字节摘要;descriptor在安装时按来源物化并单独核验。 + +正式目录未新增工作台条目。`fixtures/authoring/`只有合成的listing-only契约样本,example.test地址不是可获取的应用发行物,不表示上传、审核、运行或第二用户安装已完成。主仓内置seed保持旧受审提交;此候选先经外仓与客户端reader分别review,只有客户端真实app-authoring-v1/web-bundle等能力发行后才能开放新entries。最低版本字符串不能代替握手,旧客户端继续读取旧条目并忽略未知类型,不允许改交付类型绕过。 + +作者从同一固定导出生成manifest.json、catalog-metadata.v1.json、install.md三文件,产品ID/版本/入口保持一致。证据不足保持listing-only;installable需真实来源、许可和审核记录。用户实例/数据/平台secret/开发机路径不写目录。目录PR、来源上传、准入和安装各自核验;同版本异内容创建新修订,目录更新不热替换已安装快照,撤回不自动卸载或删除数据。 + +GitHub/Gitea私有来源依真实ACL和连接;客户端private来源不走公开镜像回退,认证失败拒绝新获取。团队移除不自动改来源ACL,需实际源管理员处理;已下载字节不能收回。离线已安装应用按保存事实维护,不把目录缓存当新安装的完整运行环境。 + +English: Version 4.3 is a compatibility candidate, preserving legacy branches and adding fixed web-bundle and authored native release contracts. Synthetic listing-only fixtures are not published artifacts. Official entries wait for reviewed Schema/client support and actual capability release. Archive bytes, portable release identity and source-materialized descriptor identity are distinct. Private-source access depends on actual Git ACL; team membership does not automatically revoke source access, and catalog withdrawal does not delete installed data. + +## 条目格式(兼容字段) 每个条目是 `entries//` 下的一个目录,目录名即条目 ID。 @@ -82,7 +94,7 @@ English: Plugins are App products with host contributions, sharing the canonical |------|------|------|------| | `id` | string | ✅ | 唯一标识,与目录名一致 | | `name` | string | ✅ | 显示名称 | -| `type` | string | ✅ | 条目类型:`native-app` / `docker-app` / `artifact` / `mcp` / `http-api` / `external-integration` | +| `type` | string | ✅ | 条目类型:`native-app` / `docker-app` / `artifact` / `web-bundle` / `mcp` / `http-api` / `external-integration` | | `version` | string | ✅ | 上游原始版本字符串;可为 SemVer、CalVer 或不透明版本 | | `description` | string | ✅ | 一行功能摘要 | | `author` | string | | 作者或组织 | diff --git a/SCHEMA_VERSION b/SCHEMA_VERSION index 6aba2b2..8089590 100644 --- a/SCHEMA_VERSION +++ b/SCHEMA_VERSION @@ -1 +1 @@ -4.2.0 +4.3.0 diff --git a/fixtures/authoring/entries/docker-workbench-example/catalog-metadata.v1.json b/fixtures/authoring/entries/docker-workbench-example/catalog-metadata.v1.json new file mode 100644 index 0000000..e56d030 --- /dev/null +++ b/fixtures/authoring/entries/docker-workbench-example/catalog-metadata.v1.json @@ -0,0 +1,82 @@ +{ + "$schema": "../../schemas/catalog-metadata.v1.schema.json", + "schemaVersion": 1, + "identity": { + "kind": "app", + "id": "docker-workbench-example" + }, + "presentation": { + "defaultLocale": "zh-CN", + "i18n": { + "zh-CN": { + "name": "容器工作台示例", + "summary": "仅用于契约核验的合成工作台材料。", + "description": "仅用于契约核验的合成工作台材料。" + } + }, + "tags": [ + "workbench", + "example" + ] + }, + "release": { + "state": "known", + "version": "1.0.0", + "versionScheme": "semver" + }, + "timestamps": { + "catalogUpdatedAt": { + "state": "unknown" + }, + "releasePublishedAt": { + "state": "unknown" + }, + "reviewedAt": { + "state": "unknown" + }, + "upstreamObservedAt": { + "state": "unknown" + } + }, + "provenance": { + "content": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + } + }, + "governance": { + "stewardship": "community", + "availability": "listing-only", + "license": { + "state": "known", + "value": "MIT" + }, + "redistribution": "verify-package-terms" + }, + "compatibility": { + "platforms": { + "state": "known", + "values": [ + "macos", + "windows", + "linux" + ] + }, + "requiredClientVersion": "10.0.180" + }, + "spec": { + "kind": "app", + "category": "tools", + "productKind": "application", + "entrypointKinds": [ + "embedded-web" + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } + } +} diff --git a/fixtures/authoring/entries/docker-workbench-example/install.md b/fixtures/authoring/entries/docker-workbench-example/install.md new file mode 100644 index 0000000..fdaf1af --- /dev/null +++ b/fixtures/authoring/entries/docker-workbench-example/install.md @@ -0,0 +1,26 @@ +# 容器工作台示例 1.0.0 + +目录材料对应固定应用release,上传、目录审阅和实例安装分别核验。 + +- 来源:https://git.example.test/examples/releases/download/1.0.0/workbench.zip +- 固定来源ref:1111111111111111111111111111111111111111 +- 归档响应体SHA-256:ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284 +- releaseDigest:ebe36976b3bf3f3af1eb833162fe61a1509bdbdb373000da1e2c067dada17540 +- 作者定义:application.json +- 所需能力:app-authoring-v1, release, runtime-binding + +先读取应用安装管理技能 app-install-manager,解析当前请求、精确目标与占用。实际能力握手必须满足以上要求,版本号不能代替握手。 + +使用用户已授权的Git/HTTP工具取得此固定材料,核对归档原始字节摘要;安全解包后核读app-release.json和全部文件摘要、作者定义与版本。releaseDigest按app-release-c14n-v1计算,不能换成归档摘要。 + +通过已有release prepare接口取得owned固定候选与来源物化descriptor,分别核验descriptor原始字节;沿唯一安装writer提交CAS,登记成功后再检查启动和功能,不能把材料存在或端口响应说成已完成安装。 + +运行和依赖说明位于runtime/说明.md;Agent选择实际执行方式并走已有执行owner。网页直接打开须取得真实运行身份与代际绑定,不能用旧PID/端口/核验记录代替。 + +固定镜像依赖为registry.example.test/example/app@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd;镜像许可、真实Docker环境和来源鉴权分别核验。使用原容器/执行/服务owner,缺可信身份adapter时保持Agent管理,不能因为Schema通过声称容器已运行。 + +此release未声明平台业务文件合同;安装说明不授予宿主文件或共享数据库权限。持久数据和备份要求须在实际应用中核读。 + +同版本异内容必须创建新修订。目录更新只产生候选,不能改写已安装快照。目录撤回阻止新获取,不自动卸载或删除本机数据;离线只能继续使用已经核验且本地依赖完整的版本。 + +私有来源由真实Git/Gitea ACL和连接鉴权;凭据不写入本目录、归档或URL。团队成员移除不会自动撤销来源端访问,需源管理员处理;已下载字节不能收回。 diff --git a/fixtures/authoring/entries/docker-workbench-example/manifest.json b/fixtures/authoring/entries/docker-workbench-example/manifest.json new file mode 100644 index 0000000..6ce7692 --- /dev/null +++ b/fixtures/authoring/entries/docker-workbench-example/manifest.json @@ -0,0 +1,68 @@ +{ + "id": "docker-workbench-example", + "name": "容器工作台示例", + "type": "docker-app", + "productKind": "application", + "version": "1.0.0", + "author": "Example Author", + "description": "仅用于契约核验的合成工作台材料。", + "tags": [ + "workbench", + "example" + ], + "icon": "AppWindow", + "iconLetter": "容", + "platformSupport": [ + "macos", + "windows", + "linux" + ], + "category": "tools", + "shortDesc": "仅用于契约核验的合成工作台材料。", + "fullDesc": "仅用于契约核验的合成工作台材料。", + "install": { + "method": "release", + "requirements": { + "docker": true, + "ports": [] + } + }, + "appRelease": { + "schemaVersion": 1, + "releaseDigest": "ebe36976b3bf3f3af1eb833162fe61a1509bdbdb373000da1e2c067dada17540", + "manifestPath": "app-release.json", + "definitionPath": "application.json", + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "runtime-binding" + ], + "imageRefs": [ + "registry.example.test/example/app@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + ] + }, + "source": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + }, + "requiredClientVersion": "10.0.180", + "entrypoints": [ + { + "id": "main", + "name": "工作台", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + }, + "stewardship": "community", + "availability": "listing-only", + "license": "MIT", + "redistribution": "verify-package-terms" +} diff --git a/fixtures/authoring/entries/service-workbench-example/catalog-metadata.v1.json b/fixtures/authoring/entries/service-workbench-example/catalog-metadata.v1.json new file mode 100644 index 0000000..5fb3a74 --- /dev/null +++ b/fixtures/authoring/entries/service-workbench-example/catalog-metadata.v1.json @@ -0,0 +1,82 @@ +{ + "$schema": "../../schemas/catalog-metadata.v1.schema.json", + "schemaVersion": 1, + "identity": { + "kind": "app", + "id": "service-workbench-example" + }, + "presentation": { + "defaultLocale": "zh-CN", + "i18n": { + "zh-CN": { + "name": "服务工作台示例", + "summary": "仅用于契约核验的合成工作台材料。", + "description": "仅用于契约核验的合成工作台材料。" + } + }, + "tags": [ + "workbench", + "example" + ] + }, + "release": { + "state": "known", + "version": "1.0.0", + "versionScheme": "semver" + }, + "timestamps": { + "catalogUpdatedAt": { + "state": "unknown" + }, + "releasePublishedAt": { + "state": "unknown" + }, + "reviewedAt": { + "state": "unknown" + }, + "upstreamObservedAt": { + "state": "unknown" + } + }, + "provenance": { + "content": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + } + }, + "governance": { + "stewardship": "community", + "availability": "listing-only", + "license": { + "state": "known", + "value": "MIT" + }, + "redistribution": "verify-package-terms" + }, + "compatibility": { + "platforms": { + "state": "known", + "values": [ + "macos", + "windows", + "linux" + ] + }, + "requiredClientVersion": "10.0.180" + }, + "spec": { + "kind": "app", + "category": "tools", + "productKind": "application", + "entrypointKinds": [ + "embedded-web" + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } + } +} diff --git a/fixtures/authoring/entries/service-workbench-example/install.md b/fixtures/authoring/entries/service-workbench-example/install.md new file mode 100644 index 0000000..993ab8d --- /dev/null +++ b/fixtures/authoring/entries/service-workbench-example/install.md @@ -0,0 +1,24 @@ +# 服务工作台示例 1.0.0 + +目录材料对应固定应用release,上传、目录审阅和实例安装分别核验。 + +- 来源:https://git.example.test/examples/releases/download/1.0.0/workbench.zip +- 固定来源ref:1111111111111111111111111111111111111111 +- 归档响应体SHA-256:ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284 +- releaseDigest:73393f784bede68657be54fdfcbb9dcb083a5a15f8e272d01dce453152cc966f +- 作者定义:application.json +- 所需能力:app-authoring-v1, release, runtime-binding + +先读取应用安装管理技能 app-install-manager,解析当前请求、精确目标与占用。实际能力握手必须满足以上要求,版本号不能代替握手。 + +使用用户已授权的Git/HTTP工具取得此固定材料,核对归档原始字节摘要;安全解包后核读app-release.json和全部文件摘要、作者定义与版本。releaseDigest按app-release-c14n-v1计算,不能换成归档摘要。 + +通过已有release prepare接口取得owned固定候选与来源物化descriptor,分别核验descriptor原始字节;沿唯一安装writer提交CAS,登记成功后再检查启动和功能,不能把材料存在或端口响应说成已完成安装。 + +运行和依赖说明位于runtime/说明.md;Agent选择实际执行方式并走已有执行owner。网页直接打开须取得真实运行身份与代际绑定,不能用旧PID/端口/核验记录代替。 + +此release未声明平台业务文件合同;安装说明不授予宿主文件或共享数据库权限。持久数据和备份要求须在实际应用中核读。 + +同版本异内容必须创建新修订。目录更新只产生候选,不能改写已安装快照。目录撤回阻止新获取,不自动卸载或删除本机数据;离线只能继续使用已经核验且本地依赖完整的版本。 + +私有来源由真实Git/Gitea ACL和连接鉴权;凭据不写入本目录、归档或URL。团队成员移除不会自动撤销来源端访问,需源管理员处理;已下载字节不能收回。 diff --git a/fixtures/authoring/entries/service-workbench-example/manifest.json b/fixtures/authoring/entries/service-workbench-example/manifest.json new file mode 100644 index 0000000..ae71fe2 --- /dev/null +++ b/fixtures/authoring/entries/service-workbench-example/manifest.json @@ -0,0 +1,65 @@ +{ + "id": "service-workbench-example", + "name": "服务工作台示例", + "type": "native-app", + "productKind": "application", + "version": "1.0.0", + "author": "Example Author", + "description": "仅用于契约核验的合成工作台材料。", + "tags": [ + "workbench", + "example" + ], + "icon": "AppWindow", + "iconLetter": "服", + "platformSupport": [ + "macos", + "windows", + "linux" + ], + "category": "tools", + "shortDesc": "仅用于契约核验的合成工作台材料。", + "fullDesc": "仅用于契约核验的合成工作台材料。", + "install": { + "method": "release", + "requirements": { + "docker": false, + "ports": [] + } + }, + "appRelease": { + "schemaVersion": 1, + "releaseDigest": "73393f784bede68657be54fdfcbb9dcb083a5a15f8e272d01dce453152cc966f", + "manifestPath": "app-release.json", + "definitionPath": "application.json", + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "runtime-binding" + ] + }, + "source": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + }, + "requiredClientVersion": "10.0.180", + "entrypoints": [ + { + "id": "main", + "name": "工作台", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + }, + "stewardship": "community", + "availability": "listing-only", + "license": "MIT", + "redistribution": "verify-package-terms" +} diff --git a/fixtures/authoring/entries/static-workbench-example/catalog-metadata.v1.json b/fixtures/authoring/entries/static-workbench-example/catalog-metadata.v1.json new file mode 100644 index 0000000..9e60e16 --- /dev/null +++ b/fixtures/authoring/entries/static-workbench-example/catalog-metadata.v1.json @@ -0,0 +1,83 @@ +{ + "$schema": "../../schemas/catalog-metadata.v1.schema.json", + "schemaVersion": 1, + "identity": { + "kind": "app", + "id": "static-workbench-example" + }, + "presentation": { + "defaultLocale": "zh-CN", + "i18n": { + "zh-CN": { + "name": "静态工作台示例", + "summary": "仅用于契约核验的合成工作台材料。", + "description": "仅用于契约核验的合成工作台材料。" + } + }, + "tags": [ + "workbench", + "example" + ] + }, + "release": { + "state": "known", + "version": "1.0.0", + "versionScheme": "semver" + }, + "timestamps": { + "catalogUpdatedAt": { + "state": "unknown" + }, + "releasePublishedAt": { + "state": "unknown" + }, + "reviewedAt": { + "state": "unknown" + }, + "upstreamObservedAt": { + "state": "unknown" + } + }, + "provenance": { + "content": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + } + }, + "governance": { + "stewardship": "community", + "availability": "listing-only", + "license": { + "state": "known", + "value": "MIT" + }, + "redistribution": "verify-package-terms" + }, + "compatibility": { + "platforms": { + "state": "known", + "values": [ + "macos", + "windows", + "linux", + "web" + ] + }, + "requiredClientVersion": "10.0.180" + }, + "spec": { + "kind": "app", + "category": "tools", + "productKind": "application", + "entrypointKinds": [ + "embedded-web" + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } + } +} diff --git a/fixtures/authoring/entries/static-workbench-example/install.md b/fixtures/authoring/entries/static-workbench-example/install.md new file mode 100644 index 0000000..9da1827 --- /dev/null +++ b/fixtures/authoring/entries/static-workbench-example/install.md @@ -0,0 +1,24 @@ +# 静态工作台示例 1.0.0 + +目录材料对应固定应用release,上传、目录审阅和实例安装分别核验。 + +- 来源:https://git.example.test/examples/releases/download/1.0.0/workbench.zip +- 固定来源ref:1111111111111111111111111111111111111111 +- 归档响应体SHA-256:ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284 +- releaseDigest:fe8290e42731a0132753b0c99bc0c08c82e039845ac7a489e3ee8c1e36515044 +- 作者定义:application.json +- 所需能力:app-authoring-v1, release, web-bundle + +先读取应用安装管理技能 app-install-manager,解析当前请求、精确目标与占用。实际能力握手必须满足以上要求,版本号不能代替握手。 + +使用用户已授权的Git/HTTP工具取得此固定材料,核对归档原始字节摘要;安全解包后核读app-release.json和全部文件摘要、作者定义与版本。releaseDigest按app-release-c14n-v1计算,不能换成归档摘要。 + +通过已有release prepare接口取得owned固定候选与来源物化descriptor,分别核验descriptor原始字节;沿唯一安装writer提交CAS,登记成功后再检查启动和功能,不能把材料存在或端口响应说成已完成安装。 + +静态文件只能沿release.delivery.entrypoints映射承载,不运行构建/宿主程序,不注册虚假的全局API服务。 + +此release未声明平台业务文件合同;安装说明不授予宿主文件或共享数据库权限。持久数据和备份要求须在实际应用中核读。 + +同版本异内容必须创建新修订。目录更新只产生候选,不能改写已安装快照。目录撤回阻止新获取,不自动卸载或删除本机数据;离线只能继续使用已经核验且本地依赖完整的版本。 + +私有来源由真实Git/Gitea ACL和连接鉴权;凭据不写入本目录、归档或URL。团队成员移除不会自动撤销来源端访问,需源管理员处理;已下载字节不能收回。 diff --git a/fixtures/authoring/entries/static-workbench-example/manifest.json b/fixtures/authoring/entries/static-workbench-example/manifest.json new file mode 100644 index 0000000..c436c23 --- /dev/null +++ b/fixtures/authoring/entries/static-workbench-example/manifest.json @@ -0,0 +1,66 @@ +{ + "id": "static-workbench-example", + "name": "静态工作台示例", + "type": "web-bundle", + "productKind": "application", + "version": "1.0.0", + "author": "Example Author", + "description": "仅用于契约核验的合成工作台材料。", + "tags": [ + "workbench", + "example" + ], + "icon": "AppWindow", + "iconLetter": "静", + "platformSupport": [ + "macos", + "windows", + "linux", + "web" + ], + "category": "tools", + "shortDesc": "仅用于契约核验的合成工作台材料。", + "fullDesc": "仅用于契约核验的合成工作台材料。", + "install": { + "method": "web-bundle", + "requirements": { + "docker": false, + "ports": [] + } + }, + "appRelease": { + "schemaVersion": 1, + "releaseDigest": "fe8290e42731a0132753b0c99bc0c08c82e039845ac7a489e3ee8c1e36515044", + "manifestPath": "app-release.json", + "definitionPath": "application.json", + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "web-bundle" + ] + }, + "source": { + "kind": "release", + "url": "https://git.example.test/examples/releases/download/1.0.0/workbench.zip", + "ref": "1111111111111111111111111111111111111111", + "sha256": "ed9b876d3162d09d5d7264cf552079d44e8a17d14665a00fb97bffb94c121284" + }, + "requiredClientVersion": "10.0.180", + "entrypoints": [ + { + "id": "main", + "name": "工作台", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + }, + "stewardship": "community", + "availability": "listing-only", + "license": "MIT", + "redistribution": "verify-package-terms" +} diff --git a/manifest.json b/manifest.json index 35e538e..b5ca813 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,12 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "id": "desirecore-registry-manifest", - "version": "4.2.0", + "version": "4.3.0", "name": "DesireCore Registry", "description": "DesireCore 官方注册表 — 应用、插件、MCP 服务、HTTP API 和第三方外部集成的统一目录", "maintainer": "DesireCore Team", "repository": "https://github.com/desirecore/registry", - "lastUpdated": "2026-10-11", + "lastUpdated": "2026-10-13", "stats": { "totalEntries": 24, "dockerApps": 8, @@ -15,7 +15,7 @@ "httpApis": 5, "externalIntegrations": 1 }, - "dataVersion": "4.2.0", + "dataVersion": "4.3.0", "catalogMetadata": { "version": "1.0.0", "schema": "schemas/catalog-metadata.v1.schema.json", diff --git a/package-lock.json b/package-lock.json index fa9d5db..18f24a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@desirecore/registry", - "version": "4.2.0", + "version": "4.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@desirecore/registry", - "version": "4.2.0", + "version": "4.3.0", "devDependencies": { "ajv": "8.20.0" }, diff --git a/package.json b/package.json index 82b4685..d7e79cf 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "@desirecore/registry", - "version": "4.2.0", + "version": "4.3.0", "private": true, "description": "Validated catalog data for the DesireCore Registry", "type": "module", "scripts": { "validate": "node scripts/validate-registry.mjs", - "test": "node --test scripts/validate-registry.test.mjs scripts/native-app.test.mjs scripts/plugin-contract.test.mjs && npm run validate" + "test": "node --test scripts/validate-registry.test.mjs scripts/native-app.test.mjs scripts/plugin-contract.test.mjs scripts/application-authoring.test.mjs && npm run validate" }, "devDependencies": { "ajv": "8.20.0" diff --git a/schemas/registry-entry.schema.json b/schemas/registry-entry.schema.json index e0b365a..3c68f10 100644 --- a/schemas/registry-entry.schema.json +++ b/schemas/registry-entry.schema.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://github.com/desirecore/registry/schemas/registry-entry.schema.json", - "title": "DesireCore Registry Entry 4.2", - "description": "严格区分应用/插件与服务/外部集成的 Registry 4.2 authored 契约。产品归属、部署方式、宿主贡献分别声明;声明不产生授权。此候选须在 Registry 外仓 review 后发布。", + "title": "DesireCore Registry Entry 4.3", + "description": "兼容保留4.2条目并新增固定应用release/web-bundle。候选Schema先审阅,客户端具备真实app-authoring-v1能力后才能开放条目。", "oneOf": [ { "$ref": "#/definitions/dockerApp" @@ -21,6 +21,15 @@ }, { "$ref": "#/definitions/externalIntegration" + }, + { + "$ref": "#/definitions/webBundle" + }, + { + "$ref": "#/definitions/authoredNativeApp" + }, + { + "$ref": "#/definitions/authoredDockerApp" } ], "definitions": { @@ -3460,6 +3469,1747 @@ } } ] + }, + "appRelease": { + "type": "object", + "description": "Registry 指向固定应用 release 的兼容视图;来源归档摘要在 source.sha256,不能与 releaseDigest 交换。", + "required": [ + "schemaVersion", + "releaseDigest", + "manifestPath", + "definitionPath", + "requiredCapabilities" + ], + "properties": { + "schemaVersion": { + "const": 1, + "description": "固定 release 目录引用格式版本。" + }, + "releaseDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "app-release-c14n-v1 规范材料身份;不是归档文件摘要。" + }, + "manifestPath": { + "type": "string", + "const": "app-release.json", + "description": "获取并核验的包内固定 release 清单。" + }, + "definitionPath": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))(?!.*[\\\\\\x00-\\x1f])[^?#]+$", + "description": "同一 release 文件清单中的作者定义路径;相对 POSIX 路径,禁止宿主路径、穿越和 URL。" + }, + "requiredCapabilities": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "contains": { + "const": "app-authoring-v1" + }, + "description": "获取后需要重新握手的真实能力;最低客户端版本不能代替能力支持。" + }, + "imageRefs": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "^(?:localhost|[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+)(?::[1-9][0-9]{0,4})?/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*@sha256:[a-f0-9]{64}$", + "description": "显式DNS/IPv4或localhost注册表、可选端口和repository@sha256:小写64位摘要;不能用浮动tag、shell、凭据或宿主路径。" + }, + "description": "仅容器release从同一delivery映射的固定镜像依赖;不包含登录凭据,不假定本机Docker可用。" + } + }, + "additionalProperties": false + }, + "webBundle": { + "type": "object", + "description": "固定网页包:页面到包内文件的映射在同一release清单,不执行宿主程序、不派生内部服务。", + "required": [ + "id", + "name", + "type", + "version", + "author", + "description", + "tags", + "icon", + "iconLetter", + "platformSupport", + "category", + "fullDesc", + "shortDesc", + "install", + "source", + "requiredClientVersion", + "placementPolicy", + "stewardship", + "availability", + "license", + "redistribution", + "entrypoints", + "appRelease" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "name": { + "$ref": "#/definitions/nonEmptyString" + }, + "type": { + "const": "web-bundle", + "description": "与 release.delivery 一致的交付类别。" + }, + "version": { + "$ref": "#/definitions/version" + }, + "author": { + "$ref": "#/definitions/nonEmptyString" + }, + "description": { + "$ref": "#/definitions/nonEmptyString" + }, + "tags": { + "$ref": "#/definitions/tags" + }, + "icon": { + "$ref": "#/definitions/nonEmptyString" + }, + "iconLetter": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "platformSupport": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "macos", + "windows", + "linux", + "web" + ] + }, + "description": "来源release支持的平台;运行时仍需真实宿主能力。" + }, + "category": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9-]+$", + "description": "应用分类(slug 格式),可扩展任意分类" + }, + "fullDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "shortDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "githubUrl": { + "$ref": "#/definitions/httpsUrl" + }, + "requiredClientVersion": { + "$ref": "#/definitions/version" + }, + "install": { + "type": "object", + "required": [ + "method", + "requirements" + ], + "additionalProperties": false, + "description": "先获取固定 release 再由 Agent 沿既有发布/执行owner处理;不是自动安装DSL。", + "properties": { + "method": { + "const": "web-bundle", + "description": "固定应用材料的安装方式。" + }, + "requirements": { + "type": "object", + "required": [ + "docker", + "ports" + ], + "additionalProperties": false, + "description": "发布者声明的环境条件;未知资源数字省略,不是当前运行健康。", + "properties": { + "docker": { + "const": false, + "description": "此静态或宿主应用分支不安装Docker。" + }, + "ports": { + "type": "array", + "uniqueItems": true, + "maxItems": 0, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "description": "声明服务端口要求;运行时地址必须由可信执行owner提供。" + }, + "minMemory": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的内存条件;未知省略。" + }, + "minDisk": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的磁盘条件;未知省略。" + } + } + }, + "configNeeded": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "发布者说明的配置准备项,不包含凭据。" + } + } + }, + "placementPolicy": { + "type": "object", + "description": "目录维护者声明的可放置范围;只限制能力,不能替用户自动固定或绑定", + "required": [ + "showInMyApps", + "allowNavPin", + "allowAgentBinding" + ], + "properties": { + "showInMyApps": { + "type": "boolean", + "description": "安装后是否允许出现在“我的应用”" + }, + "allowNavPin": { + "type": "boolean", + "description": "用户是否可将应用固定到主导航" + }, + "allowAgentBinding": { + "type": "boolean", + "description": "用户是否可将应用关联到 Agent 会话侧栏" + } + }, + "additionalProperties": false + }, + "source": { + "type": "object", + "description": "经过审核的上游来源指针;安装时必须进一步证明 ref 或制品摘要不可变。", + "required": [ + "kind", + "url", + "ref" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "git", + "release", + "package", + "container" + ], + "description": "上游来源类型:Git 仓库、Release 制品、包管理器制品或容器镜像。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "权威上游地址;不得由客户端在安装请求中替换。" + }, + "ref": { + "type": "string", + "minLength": 1, + "description": "审核版本引用;安装候选必须是完整 commit、制品版本或容器 digest。" + }, + "sha256": { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}$", + "description": "Release、package 或其他下载响应体的 SHA-256 摘要。" + }, + "path": { + "type": "string", + "minLength": 1, + "description": "单仓库目录中的相对路径;不得作为运行时任意本地路径使用。" + }, + "upstreamRepo": { + "type": "string", + "format": "uri", + "description": "制品与源码不在同一地址时对应的上游源码仓库。" + } + }, + "additionalProperties": false + }, + "stewardship": { + "type": "string", + "enum": [ + "official", + "partner", + "community", + "pointer" + ], + "description": "条目治理身份:official 仅能由受信 official catalog provider 注入;partner/community/pointer 分别表示合作方、社区维护或纯上游指针。" + }, + "availability": { + "type": "string", + "enum": [ + "listing-only", + "installable" + ], + "description": "目录可用性:listing-only 仅允许浏览上游;installable 才允许进入既有受治理获取流程,不表示已经安装或正在运行。" + }, + "license": { + "type": "string", + "minLength": 1, + "description": "覆盖实际制品的许可证标识。" + }, + "redistribution": { + "type": "string", + "enum": [ + "allowed", + "source-pointer-only", + "verify-package-terms" + ], + "description": "内容转交策略:allowed 可按许可证分发;source-pointer-only 只能从权威上游取得;verify-package-terms 表示条款尚未完成复核。" + }, + "listingMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "upstreamMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "branding": { + "type": "object", + "description": "名称、Logo 和官方关系的展示依据;代码许可证不能自动授予商标或宣传素材权利。", + "required": [ + "relationship", + "nameUsage", + "logoStatus" + ], + "properties": { + "relationship": { + "type": "string", + "enum": [ + "official", + "partner", + "independent-listing" + ], + "description": "与上游的公开关系:官方、已确认合作,或独立事实索引。" + }, + "nameUsage": { + "type": "string", + "enum": [ + "owned", + "licensed", + "nominative" + ], + "description": "项目名称的使用依据:自有、已获许可,或识别上游所需的指称性使用。" + }, + "logoStatus": { + "type": "string", + "enum": [ + "not-used", + "license-covered", + "trademark-policy-allowed", + "permission-obtained" + ], + "description": "Logo 使用依据;未确认权利时必须为 not-used,并由客户端使用中性图标。" + } + }, + "additionalProperties": false + }, + "compliance": { + "type": "object", + "description": "绑定到本次不可变来源的合规审核记录;新版本必须重新审核,不能继承旧版本结论。", + "required": [ + "licenseEvidencePath", + "reviewedRef", + "reviewedAt", + "reviewedBy", + "upstreamEndorsed" + ], + "properties": { + "licenseEvidencePath": { + "type": "string", + "minLength": 1, + "description": "在已审核源码或制品中定位许可证文本的相对路径。" + }, + "noticePath": { + "type": "string", + "minLength": 1, + "description": "需要履行 NOTICE 或署名义务时,对应材料的相对路径。" + }, + "reviewedRef": { + "type": "string", + "minLength": 1, + "description": "实际完成许可证、品牌和供应链复核的不可变 ref,必须与获取来源一致。" + }, + "reviewedAt": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:T.*)?$", + "description": "审核日期或 ISO 8601 时间戳。" + }, + "reviewedBy": { + "type": "string", + "minLength": 1, + "description": "可追溯的审核者或审核流程标识。" + }, + "upstreamEndorsed": { + "type": "boolean", + "description": "上游是否明确认可该条目;false 时必须显示非官方、未经背书声明。" + } + }, + "additionalProperties": false + }, + "productKind": { + "const": "application", + "description": "用户自建应用;不是平台声明插件。" + }, + "entrypoints": { + "type": "array", + "maxItems": 32, + "items": { + "oneOf": [ + { + "type": "object", + "description": "由隔离 App Host 承载的 Web 应用入口,运行时 URL 只可由服务端从 serviceRef 解析", + "required": [ + "id", + "name", + "kind", + "serviceRef" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "embedded-web", + "description": "隔离 Web 入口判别字段" + }, + "serviceRef": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "Registry 服务 ID 或服务端生成的本机端口引用;不是 URL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "id", + "name", + "kind" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID。" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "交给 Agent 打开的入口显示名。" + }, + "kind": { + "const": "agent", + "description": "读取实例维护资料后委派 Agent;无需服务端口或标准启动接口。" + } + } + } + ], + "description": "应用的一个用户可见启动入口,按 kind 严格判别" + }, + "description": "明确用户入口;plugin 允许空数组,普通应用至少一个入口。" + }, + "appRelease": { + "$ref": "#/definitions/appRelease" + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "installable" + } + }, + "required": [ + "availability" + ] + }, + "then": { + "required": [ + "branding", + "compliance", + "listingMaintainer", + "upstreamMaintainer" + ] + } + } + ] + }, + "authoredNativeApp": { + "type": "object", + "description": "带运行说明的固定宿主应用:可信执行owner核验身份,目录不保存启动命令或当前端口。", + "required": [ + "id", + "name", + "type", + "version", + "author", + "description", + "tags", + "icon", + "iconLetter", + "platformSupport", + "category", + "fullDesc", + "shortDesc", + "install", + "source", + "requiredClientVersion", + "placementPolicy", + "stewardship", + "availability", + "license", + "redistribution", + "entrypoints", + "appRelease" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "name": { + "$ref": "#/definitions/nonEmptyString" + }, + "type": { + "const": "native-app", + "description": "与 release.delivery 一致的交付类别。" + }, + "version": { + "$ref": "#/definitions/version" + }, + "author": { + "$ref": "#/definitions/nonEmptyString" + }, + "description": { + "$ref": "#/definitions/nonEmptyString" + }, + "tags": { + "$ref": "#/definitions/tags" + }, + "icon": { + "$ref": "#/definitions/nonEmptyString" + }, + "iconLetter": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "platformSupport": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "macos", + "windows", + "linux", + "web" + ] + }, + "description": "来源release支持的平台;运行时仍需真实宿主能力。" + }, + "category": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9-]+$", + "description": "应用分类(slug 格式),可扩展任意分类" + }, + "fullDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "shortDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "githubUrl": { + "$ref": "#/definitions/httpsUrl" + }, + "requiredClientVersion": { + "$ref": "#/definitions/version" + }, + "install": { + "type": "object", + "required": [ + "method", + "requirements" + ], + "additionalProperties": false, + "description": "先获取固定 release 再由 Agent 沿既有发布/执行owner处理;不是自动安装DSL。", + "properties": { + "method": { + "const": "release", + "description": "固定应用材料的安装方式。" + }, + "requirements": { + "type": "object", + "required": [ + "docker", + "ports" + ], + "additionalProperties": false, + "description": "发布者声明的环境条件;未知资源数字省略,不是当前运行健康。", + "properties": { + "docker": { + "const": false, + "description": "此静态或宿主应用分支不安装Docker。" + }, + "ports": { + "type": "array", + "uniqueItems": true, + "maxItems": 32, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "description": "声明服务端口要求;运行时地址必须由可信执行owner提供。" + }, + "minMemory": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的内存条件;未知省略。" + }, + "minDisk": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的磁盘条件;未知省略。" + }, + "node": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "明确依赖Node时的版本要求,不由交付类别猜测。" + }, + "envVars": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "运行环境变量名称,不包含秘密值。" + } + } + }, + "configNeeded": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "发布者说明的配置准备项,不包含凭据。" + } + } + }, + "placementPolicy": { + "type": "object", + "description": "目录维护者声明的可放置范围;只限制能力,不能替用户自动固定或绑定", + "required": [ + "showInMyApps", + "allowNavPin", + "allowAgentBinding" + ], + "properties": { + "showInMyApps": { + "type": "boolean", + "description": "安装后是否允许出现在“我的应用”" + }, + "allowNavPin": { + "type": "boolean", + "description": "用户是否可将应用固定到主导航" + }, + "allowAgentBinding": { + "type": "boolean", + "description": "用户是否可将应用关联到 Agent 会话侧栏" + } + }, + "additionalProperties": false + }, + "source": { + "type": "object", + "description": "经过审核的上游来源指针;安装时必须进一步证明 ref 或制品摘要不可变。", + "required": [ + "kind", + "url", + "ref" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "git", + "release", + "package", + "container" + ], + "description": "上游来源类型:Git 仓库、Release 制品、包管理器制品或容器镜像。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "权威上游地址;不得由客户端在安装请求中替换。" + }, + "ref": { + "type": "string", + "minLength": 1, + "description": "审核版本引用;安装候选必须是完整 commit、制品版本或容器 digest。" + }, + "sha256": { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}$", + "description": "Release、package 或其他下载响应体的 SHA-256 摘要。" + }, + "path": { + "type": "string", + "minLength": 1, + "description": "单仓库目录中的相对路径;不得作为运行时任意本地路径使用。" + }, + "upstreamRepo": { + "type": "string", + "format": "uri", + "description": "制品与源码不在同一地址时对应的上游源码仓库。" + } + }, + "additionalProperties": false + }, + "stewardship": { + "type": "string", + "enum": [ + "official", + "partner", + "community", + "pointer" + ], + "description": "条目治理身份:official 仅能由受信 official catalog provider 注入;partner/community/pointer 分别表示合作方、社区维护或纯上游指针。" + }, + "availability": { + "type": "string", + "enum": [ + "listing-only", + "installable" + ], + "description": "目录可用性:listing-only 仅允许浏览上游;installable 才允许进入既有受治理获取流程,不表示已经安装或正在运行。" + }, + "license": { + "type": "string", + "minLength": 1, + "description": "覆盖实际制品的许可证标识。" + }, + "redistribution": { + "type": "string", + "enum": [ + "allowed", + "source-pointer-only", + "verify-package-terms" + ], + "description": "内容转交策略:allowed 可按许可证分发;source-pointer-only 只能从权威上游取得;verify-package-terms 表示条款尚未完成复核。" + }, + "listingMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "upstreamMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "branding": { + "type": "object", + "description": "名称、Logo 和官方关系的展示依据;代码许可证不能自动授予商标或宣传素材权利。", + "required": [ + "relationship", + "nameUsage", + "logoStatus" + ], + "properties": { + "relationship": { + "type": "string", + "enum": [ + "official", + "partner", + "independent-listing" + ], + "description": "与上游的公开关系:官方、已确认合作,或独立事实索引。" + }, + "nameUsage": { + "type": "string", + "enum": [ + "owned", + "licensed", + "nominative" + ], + "description": "项目名称的使用依据:自有、已获许可,或识别上游所需的指称性使用。" + }, + "logoStatus": { + "type": "string", + "enum": [ + "not-used", + "license-covered", + "trademark-policy-allowed", + "permission-obtained" + ], + "description": "Logo 使用依据;未确认权利时必须为 not-used,并由客户端使用中性图标。" + } + }, + "additionalProperties": false + }, + "compliance": { + "type": "object", + "description": "绑定到本次不可变来源的合规审核记录;新版本必须重新审核,不能继承旧版本结论。", + "required": [ + "licenseEvidencePath", + "reviewedRef", + "reviewedAt", + "reviewedBy", + "upstreamEndorsed" + ], + "properties": { + "licenseEvidencePath": { + "type": "string", + "minLength": 1, + "description": "在已审核源码或制品中定位许可证文本的相对路径。" + }, + "noticePath": { + "type": "string", + "minLength": 1, + "description": "需要履行 NOTICE 或署名义务时,对应材料的相对路径。" + }, + "reviewedRef": { + "type": "string", + "minLength": 1, + "description": "实际完成许可证、品牌和供应链复核的不可变 ref,必须与获取来源一致。" + }, + "reviewedAt": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:T.*)?$", + "description": "审核日期或 ISO 8601 时间戳。" + }, + "reviewedBy": { + "type": "string", + "minLength": 1, + "description": "可追溯的审核者或审核流程标识。" + }, + "upstreamEndorsed": { + "type": "boolean", + "description": "上游是否明确认可该条目;false 时必须显示非官方、未经背书声明。" + } + }, + "additionalProperties": false + }, + "productKind": { + "const": "application", + "description": "用户自建应用;不是平台声明插件。" + }, + "entrypoints": { + "type": "array", + "maxItems": 32, + "items": { + "oneOf": [ + { + "type": "object", + "description": "DesireCore 自带并随客户端发布的页面入口;第三方目录不得伪造此入口类型", + "required": [ + "id", + "name", + "kind", + "route" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "builtin", + "description": "内置页面入口判别字段" + }, + "route": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^/(?!/)", + "description": "客户端内置路由;必须是单斜杠开头的相对应用路由,不能是 URL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "description": "由隔离 App Host 承载的 Web 应用入口,运行时 URL 只可由服务端从 serviceRef 解析", + "required": [ + "id", + "name", + "kind", + "serviceRef" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "embedded-web", + "description": "隔离 Web 入口判别字段" + }, + "serviceRef": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "Registry 服务 ID 或服务端生成的本机端口引用;不是 URL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "description": "由系统浏览器打开的目录声明入口;运行时 URL 只可由服务端从 serviceRef 解析", + "required": [ + "id", + "name", + "kind", + "serviceRef" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "external", + "description": "外部浏览器入口判别字段" + }, + "serviceRef": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "Registry 服务 ID;不是 URL,解析结果还需 HTTPS 或本机 loopback 校验" + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^/(?!/)", + "description": "可选服务内路径;解析层禁止凭据、反斜杠和目录穿越" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "id", + "name", + "kind" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID。" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "交给 Agent 打开的入口显示名。" + }, + "kind": { + "const": "agent", + "description": "读取实例维护资料后委派 Agent;无需服务端口或标准启动接口。" + } + } + } + ], + "description": "应用的一个用户可见启动入口,按 kind 严格判别" + }, + "description": "明确用户入口;plugin 允许空数组,普通应用至少一个入口。" + }, + "appRelease": { + "$ref": "#/definitions/appRelease" + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "installable" + } + }, + "required": [ + "availability" + ] + }, + "then": { + "required": [ + "branding", + "compliance", + "listingMaintainer", + "upstreamMaintainer" + ] + } + } + ] + }, + "authoredDockerApp": { + "type": "object", + "description": "固定容器镜像依赖的release应用:使用原Docker执行/服务owner,不新建容器管理器;缺真实身份adapter时不声称直开。", + "required": [ + "id", + "name", + "type", + "version", + "author", + "description", + "tags", + "icon", + "iconLetter", + "platformSupport", + "category", + "fullDesc", + "shortDesc", + "install", + "source", + "requiredClientVersion", + "placementPolicy", + "stewardship", + "availability", + "license", + "redistribution", + "entrypoints", + "appRelease" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "name": { + "$ref": "#/definitions/nonEmptyString" + }, + "type": { + "const": "docker-app", + "description": "与 release.delivery 一致的交付类别。" + }, + "version": { + "$ref": "#/definitions/version" + }, + "author": { + "$ref": "#/definitions/nonEmptyString" + }, + "description": { + "$ref": "#/definitions/nonEmptyString" + }, + "tags": { + "$ref": "#/definitions/tags" + }, + "icon": { + "$ref": "#/definitions/nonEmptyString" + }, + "iconLetter": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "platformSupport": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "macos", + "windows", + "linux", + "web" + ] + }, + "description": "来源release支持的平台;运行时仍需真实宿主能力。" + }, + "category": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9-]+$", + "description": "应用分类(slug 格式),可扩展任意分类" + }, + "fullDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "shortDesc": { + "$ref": "#/definitions/nonEmptyString" + }, + "githubUrl": { + "$ref": "#/definitions/httpsUrl" + }, + "requiredClientVersion": { + "$ref": "#/definitions/version" + }, + "install": { + "type": "object", + "required": [ + "method", + "requirements" + ], + "additionalProperties": false, + "description": "先获取固定 release 再由 Agent 沿既有发布/执行owner处理;不是自动安装DSL。", + "properties": { + "method": { + "const": "release", + "description": "固定应用材料的安装方式。" + }, + "requirements": { + "type": "object", + "required": [ + "docker", + "ports" + ], + "additionalProperties": false, + "description": "发布者声明的环境条件;未知资源数字省略,不是当前运行健康。", + "properties": { + "docker": { + "const": true, + "description": "此静态或宿主应用分支不安装Docker。" + }, + "ports": { + "type": "array", + "uniqueItems": true, + "maxItems": 32, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "description": "声明服务端口要求;运行时地址必须由可信执行owner提供。" + }, + "minMemory": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的内存条件;未知省略。" + }, + "minDisk": { + "type": "string", + "minLength": 1, + "description": "实际发布材料声明的磁盘条件;未知省略。" + }, + "node": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "明确依赖Node时的版本要求,不由交付类别猜测。" + }, + "envVars": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "运行环境变量名称,不包含秘密值。" + } + } + }, + "configNeeded": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "发布者说明的配置准备项,不包含凭据。" + } + } + }, + "placementPolicy": { + "type": "object", + "description": "目录维护者声明的可放置范围;只限制能力,不能替用户自动固定或绑定", + "required": [ + "showInMyApps", + "allowNavPin", + "allowAgentBinding" + ], + "properties": { + "showInMyApps": { + "type": "boolean", + "description": "安装后是否允许出现在“我的应用”" + }, + "allowNavPin": { + "type": "boolean", + "description": "用户是否可将应用固定到主导航" + }, + "allowAgentBinding": { + "type": "boolean", + "description": "用户是否可将应用关联到 Agent 会话侧栏" + } + }, + "additionalProperties": false + }, + "source": { + "type": "object", + "description": "经过审核的上游来源指针;安装时必须进一步证明 ref 或制品摘要不可变。", + "required": [ + "kind", + "url", + "ref" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "git", + "release", + "package", + "container" + ], + "description": "上游来源类型:Git 仓库、Release 制品、包管理器制品或容器镜像。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "权威上游地址;不得由客户端在安装请求中替换。" + }, + "ref": { + "type": "string", + "minLength": 1, + "description": "审核版本引用;安装候选必须是完整 commit、制品版本或容器 digest。" + }, + "sha256": { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}$", + "description": "Release、package 或其他下载响应体的 SHA-256 摘要。" + }, + "path": { + "type": "string", + "minLength": 1, + "description": "单仓库目录中的相对路径;不得作为运行时任意本地路径使用。" + }, + "upstreamRepo": { + "type": "string", + "format": "uri", + "description": "制品与源码不在同一地址时对应的上游源码仓库。" + } + }, + "additionalProperties": false + }, + "stewardship": { + "type": "string", + "enum": [ + "official", + "partner", + "community", + "pointer" + ], + "description": "条目治理身份:official 仅能由受信 official catalog provider 注入;partner/community/pointer 分别表示合作方、社区维护或纯上游指针。" + }, + "availability": { + "type": "string", + "enum": [ + "listing-only", + "installable" + ], + "description": "目录可用性:listing-only 仅允许浏览上游;installable 才允许进入既有受治理获取流程,不表示已经安装或正在运行。" + }, + "license": { + "type": "string", + "minLength": 1, + "description": "覆盖实际制品的许可证标识。" + }, + "redistribution": { + "type": "string", + "enum": [ + "allowed", + "source-pointer-only", + "verify-package-terms" + ], + "description": "内容转交策略:allowed 可按许可证分发;source-pointer-only 只能从权威上游取得;verify-package-terms 表示条款尚未完成复核。" + }, + "listingMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "upstreamMaintainer": { + "type": "object", + "description": "目录策展者或上游维护者身份;两种角色必须分别存放,不能把 author 当成已验证维护者。", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "维护者的公开名称或组织名称。" + }, + "url": { + "type": "string", + "format": "uri", + "description": "维护者的公开主页、项目主页或可验证身份页面。" + }, + "verified": { + "type": "boolean", + "description": "平台是否已验证该身份;不等同于许可证授权、内容审核或上游背书。" + } + }, + "additionalProperties": false + }, + "branding": { + "type": "object", + "description": "名称、Logo 和官方关系的展示依据;代码许可证不能自动授予商标或宣传素材权利。", + "required": [ + "relationship", + "nameUsage", + "logoStatus" + ], + "properties": { + "relationship": { + "type": "string", + "enum": [ + "official", + "partner", + "independent-listing" + ], + "description": "与上游的公开关系:官方、已确认合作,或独立事实索引。" + }, + "nameUsage": { + "type": "string", + "enum": [ + "owned", + "licensed", + "nominative" + ], + "description": "项目名称的使用依据:自有、已获许可,或识别上游所需的指称性使用。" + }, + "logoStatus": { + "type": "string", + "enum": [ + "not-used", + "license-covered", + "trademark-policy-allowed", + "permission-obtained" + ], + "description": "Logo 使用依据;未确认权利时必须为 not-used,并由客户端使用中性图标。" + } + }, + "additionalProperties": false + }, + "compliance": { + "type": "object", + "description": "绑定到本次不可变来源的合规审核记录;新版本必须重新审核,不能继承旧版本结论。", + "required": [ + "licenseEvidencePath", + "reviewedRef", + "reviewedAt", + "reviewedBy", + "upstreamEndorsed" + ], + "properties": { + "licenseEvidencePath": { + "type": "string", + "minLength": 1, + "description": "在已审核源码或制品中定位许可证文本的相对路径。" + }, + "noticePath": { + "type": "string", + "minLength": 1, + "description": "需要履行 NOTICE 或署名义务时,对应材料的相对路径。" + }, + "reviewedRef": { + "type": "string", + "minLength": 1, + "description": "实际完成许可证、品牌和供应链复核的不可变 ref,必须与获取来源一致。" + }, + "reviewedAt": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:T.*)?$", + "description": "审核日期或 ISO 8601 时间戳。" + }, + "reviewedBy": { + "type": "string", + "minLength": 1, + "description": "可追溯的审核者或审核流程标识。" + }, + "upstreamEndorsed": { + "type": "boolean", + "description": "上游是否明确认可该条目;false 时必须显示非官方、未经背书声明。" + } + }, + "additionalProperties": false + }, + "productKind": { + "const": "application", + "description": "用户自建应用;不是平台声明插件。" + }, + "entrypoints": { + "type": "array", + "maxItems": 32, + "items": { + "oneOf": [ + { + "type": "object", + "description": "DesireCore 自带并随客户端发布的页面入口;第三方目录不得伪造此入口类型", + "required": [ + "id", + "name", + "kind", + "route" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "builtin", + "description": "内置页面入口判别字段" + }, + "route": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^/(?!/)", + "description": "客户端内置路由;必须是单斜杠开头的相对应用路由,不能是 URL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "description": "由隔离 App Host 承载的 Web 应用入口,运行时 URL 只可由服务端从 serviceRef 解析", + "required": [ + "id", + "name", + "kind", + "serviceRef" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "embedded-web", + "description": "隔离 Web 入口判别字段" + }, + "serviceRef": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "Registry 服务 ID 或服务端生成的本机端口引用;不是 URL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "description": "由系统浏览器打开的目录声明入口;运行时 URL 只可由服务端从 serviceRef 解析", + "required": [ + "id", + "name", + "kind", + "serviceRef" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "入口显示名称" + }, + "kind": { + "const": "external", + "description": "外部浏览器入口判别字段" + }, + "serviceRef": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "Registry 服务 ID;不是 URL,解析结果还需 HTTPS 或本机 loopback 校验" + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^/(?!/)", + "description": "可选服务内路径;解析层禁止凭据、反斜杠和目录穿越" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "id", + "name", + "kind" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]*$", + "description": "应用内稳定入口 ID。" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "description": "交给 Agent 打开的入口显示名。" + }, + "kind": { + "const": "agent", + "description": "读取实例维护资料后委派 Agent;无需服务端口或标准启动接口。" + } + } + } + ], + "description": "应用的一个用户可见启动入口,按 kind 严格判别" + }, + "description": "明确用户入口;plugin 允许空数组,普通应用至少一个入口。" + }, + "appRelease": { + "$ref": "#/definitions/appRelease" + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "installable" + } + }, + "required": [ + "availability" + ] + }, + "then": { + "required": [ + "branding", + "compliance", + "listingMaintainer", + "upstreamMaintainer" + ] + } + }, + { + "properties": { + "appRelease": { + "required": [ + "imageRefs" + ] + } + } + } + ] } } } diff --git a/schemas/registry-manifest.v3.schema.json b/schemas/registry-manifest.v3.schema.json index c3f652e..5e82a43 100644 --- a/schemas/registry-manifest.v3.schema.json +++ b/schemas/registry-manifest.v3.schema.json @@ -23,6 +23,7 @@ "dockerApps": { "type": "integer", "minimum": 0 }, "nativeApps": { "type": "integer", "minimum": 0 }, "artifactApps": { "type": "integer", "minimum": 0, "description": "纯声明制品的 App 条目数,包括插件;不是安装数。" }, + "webBundles": { "type": "integer", "minimum": 0, "description": "固定网页包App条目数;不是安装数或验证成功数量。" }, "mcpServices": { "type": "integer", "minimum": 0 }, "httpApis": { "type": "integer", "minimum": 0 }, "externalIntegrations": { "type": "integer", "minimum": 0 } diff --git a/scripts/application-authoring.test.mjs b/scripts/application-authoring.test.mjs new file mode 100644 index 0000000..f94dca4 --- /dev/null +++ b/scripts/application-authoring.test.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict' +import { readFileSync, cpSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { tmpdir } from 'node:os' +import { test, afterEach } from 'node:test' +import Ajv from 'ajv' +import { validateRegistry } from './catalog/validator.mjs' +import { validateApplicationReleaseContract } from './catalog/application-release-contract.mjs' + +const source = join(dirname(fileURLToPath(import.meta.url)),'..') +const temporary = [] +afterEach(() => { for (const root of temporary.splice(0)) rmSync(root,{recursive:true,force:true}) }) +const schema = JSON.parse(readFileSync(join(source,'schemas/registry-entry.schema.json'),'utf8')) +const validate = new Ajv({allErrors:true,strict:true,strictTypes:false,strictRequired:false,validateFormats:false}).compile(schema) +const load = id => JSON.parse(readFileSync(join(source,'fixtures/authoring/entries',id,'manifest.json'),'utf8')) + +test('4.3只新增兼容与合成fixtures,正式目录仍无web-bundle或appRelease条目',() => { + const root = JSON.parse(readFileSync(join(source,'manifest.json'),'utf8')) + assert.equal(root.version,'4.3.0') + assert.equal(root.stats.totalEntries,24) + assert.equal(root.stats.webBundles,undefined) +}) +test('静态与带服务合成目录三文件保持App语义,两个validator同样通过',() => { + const root = mkdtempSync(join(tmpdir(),'registry-authoring-'));temporary.push(root) + cpSync(join(source,'schemas'),join(root,'schemas'),{recursive:true}) + cpSync(join(source,'fixtures/authoring/entries'),join(root,'entries'),{recursive:true}) + const manifest = JSON.parse(readFileSync(join(source,'manifest.json'),'utf8')) + manifest.stats={totalEntries:3,dockerApps:1,nativeApps:1,webBundles:1,mcpServices:0,httpApis:0,externalIntegrations:0} + writeFileSync(join(root,'manifest.json'),JSON.stringify(manifest));writeFileSync(join(root,'SCHEMA_VERSION'),'4.3.0\n') + const report = validateRegistry(root) + assert.equal(report.ok,true,JSON.stringify(report.errors)) + assert.equal(report.counts.webBundles,1) + for (const id of ['static-workbench-example','service-workbench-example','docker-workbench-example']) { + const item=load(id);assert.equal(validate(item),true,JSON.stringify(validate.errors)) + assert.deepEqual(validateApplicationReleaseContract(item),[]) + assert.equal(item.sourceId,undefined) + assert.notEqual(item.appRelease.releaseDigest,item.source.sha256) + assert.equal(item.availability,'listing-only') + } +}) +test('不能把完整HTML伪装成artifact、native-node或内部MCP服务',() => { + const item=load('static-workbench-example') + for (const change of [copy=>{copy.type='artifact'},copy=>{copy.install.method='native-node'},copy=>{copy.connection={transport:'stdio',command:'node'}},copy=>{copy.appRelease.materialRoot='/private'}]) { + const copy=structuredClone(item);change(copy);assert.equal(validate(copy),false) + } +}) +test('未知能力、可变ref和私有凭据不成为正式分发事实',() => { + const item=load('static-workbench-example') + for (const change of [copy=>{copy.source.ref='latest'},copy=>{copy.source.url='https://credential@git.example.test/private.zip'},copy=>{copy.appRelease.requiredCapabilities=['app-authoring-v1']}]) { + const copy=structuredClone(item);change(copy);assert.ok(validateApplicationReleaseContract(copy).length>0) + } +}) +test('保留既有native-node条目,新native release字段不污染旧分支',() => { + const old=JSON.parse(readFileSync(join(source,'entries/desirecore-control/manifest.json'),'utf8')) + assert.equal(validate(old),true,JSON.stringify(validate.errors)) + old.appRelease=load('service-workbench-example').appRelease + assert.equal(validate(old),false) +}) +test('Docker发布固定真实镜像内容引用,不把浮动tag或错误端口当成固定材料',() => { + const item=load('docker-workbench-example') + assert.equal(validate(item),true,JSON.stringify(validate.errors)) + assert.equal(item.install.method,'release');assert.equal(item.install.requirements.docker,true) + for (const image of ['node:latest','registry.example.test/example/app:1.0.0','localhost:70000/app@sha256:'+'d'.repeat(64)]) { + const copy=structuredClone(item);copy.appRelease.imageRefs=[image] + assert.ok(!validate(copy) || validateApplicationReleaseContract(copy).length>0) + } +}) diff --git a/scripts/catalog/application-contract.mjs b/scripts/catalog/application-contract.mjs index 95a1e8e..4dba0ed 100644 --- a/scripts/catalog/application-contract.mjs +++ b/scripts/catalog/application-contract.mjs @@ -1,6 +1,6 @@ /** 发布者关系声明的跨字段检查;不推导安装、权限或运行时健康。 */ export function validateApplicationContract(manifest) { - if (!['docker-app', 'native-app', 'artifact'].includes(manifest.type)) return [] + if (!['docker-app', 'native-app', 'artifact', 'web-bundle'].includes(manifest.type)) return [] const errors = [] const ids = new Set() for (const entrypoint of Array.isArray(manifest.entrypoints) ? manifest.entrypoints : []) { diff --git a/scripts/catalog/application-release-contract.mjs b/scripts/catalog/application-release-contract.mjs new file mode 100644 index 0000000..3de4d59 --- /dev/null +++ b/scripts/catalog/application-release-contract.mjs @@ -0,0 +1,32 @@ +/** 作者材料的跨字段检查;身份与运行事实由客户端owner核验,不执行安装说明。 */ +export function validateApplicationReleaseContract(manifest) { + if (manifest.type !== 'web-bundle' && manifest.appRelease === undefined) return [] + const errors = [] + if (!['web-bundle', 'native-app', 'docker-app'].includes(manifest.type)) errors.push('appRelease: unsupported delivery type') + if (!manifest.appRelease?.requiredCapabilities?.includes('app-authoring-v1')) errors.push('appRelease: app-authoring-v1 capability handshake required') + if (!manifest.appRelease?.requiredCapabilities?.includes('release') || + (manifest.type === 'web-bundle' && !manifest.appRelease.requiredCapabilities.includes('web-bundle'))) errors.push('appRelease: delivery capability handshake required') + const images = manifest.appRelease?.imageRefs + if (manifest.type === 'docker-app' && (!Array.isArray(images) || !images.length || images.some(image => + typeof image !== 'string' || !/^[a-z0-9][a-z0-9._:/-]*@sha256:[a-f0-9]{64}$/.test(image) || + Number(image.split('/')[0].split(':')[1] ?? 0) > 65535))) errors.push('appRelease: Docker images must be pinned by digest') + if (manifest.type !== 'docker-app' && images !== undefined) errors.push('appRelease: imageRefs belongs only to Docker release') + const source = manifest.source + if (!source || !['git', 'release'].includes(source.kind) || + !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.ref ?? '') || !/^[a-f0-9]{64}$/.test(source.sha256 ?? '')) { + errors.push('source: authored release requires immutable commit and archive sha256') + } + try { + const url = new URL(source?.url) + if (url.protocol !== 'https:' || url.username || url.password || url.hash || url.search) errors.push('source: credential-free fixed HTTPS URL required') + } catch { errors.push('source: invalid URL') } + if (source?.kind === 'git' && !isPortablePath(source.path)) errors.push('source: git archive path must be canonical and relative') + if (!isPortablePath(manifest.appRelease?.definitionPath)) errors.push('appRelease: definitionPath must be canonical and relative') + return errors +} + +function isPortablePath(value) { + return typeof value === 'string' && value.length > 0 && value.length <= 512 && + !/[\\\x00-\x1f?#:]/.test(value) && !value.startsWith('/') && + value.split('/').every(part => part.length > 0 && part !== '.' && part !== '..') +} diff --git a/scripts/catalog/validator.mjs b/scripts/catalog/validator.mjs index b8b75bf..979d927 100644 --- a/scripts/catalog/validator.mjs +++ b/scripts/catalog/validator.mjs @@ -4,6 +4,7 @@ import { basename, join, relative, resolve, sep } from 'node:path' import { validateJsonSchema } from './json-schema.mjs' import { findStaticMetrics } from './static-metadata.mjs' import { validateApplicationContract } from './application-contract.mjs' +import { validateApplicationReleaseContract } from './application-release-contract.mjs' export const CATALOG_SIDECAR_FILENAME = 'catalog-metadata.v1.json' @@ -77,7 +78,7 @@ function timestampShapeIsConsistent(timestamp) { function validateSidecarSemantics(manifest, sidecar, file) { const errors = [] const add = (code, path, message) => errors.push(diagnostic('error', code, file, path, message)) - const expectedKind = ['docker-app', 'native-app', 'artifact'].includes(manifest.type) ? 'app' : 'service' + const expectedKind = ['docker-app', 'native-app', 'artifact', 'web-bundle'].includes(manifest.type) ? '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}`) @@ -140,7 +141,7 @@ function validateSidecarSemantics(manifest, sidecar, file) { if (manifest.toolCount !== undefined && sidecar.spec?.toolCount !== manifest.toolCount) add('spec-mismatch', '$.spec.toolCount', 'toolCount 必须与 legacy 一致') } - if (['native-app', 'artifact'].includes(manifest.type)) { + if (['native-app', 'artifact', 'web-bundle'].includes(manifest.type)) { for (const field of ['stewardship', 'availability', 'redistribution', 'listingMaintainer', 'upstreamMaintainer', 'branding', 'compliance']) { if (!isDeepStrictEqual(manifest[field], sidecar.governance?.[field])) add('native-governance-mismatch', '$.governance.' + field, '原生应用 manifest 与 sidecar 的治理事实必须一致') } @@ -237,12 +238,14 @@ export function validateRegistry(repoRoot, options = {}) { if (entry.type === 'docker-app') counts.dockerApps += 1 if (entry.type === 'native-app') counts.nativeApps = (counts.nativeApps ?? 0) + 1 if (entry.type === 'artifact') counts.artifactApps = (counts.artifactApps ?? 0) + 1 + if (entry.type === 'web-bundle') counts.webBundles = (counts.webBundles ?? 0) + 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 = ['external-integration', 'native-app', 'artifact'].includes(entry.type) ? externalEntrySchema : currentEntrySchema + const effectiveEntrySchema = ['external-integration', 'native-app', 'artifact', 'web-bundle'].includes(entry.type) ? externalEntrySchema : currentEntrySchema if (effectiveEntrySchema) diagnostics.push(...schemaDiagnostics(entry, effectiveEntrySchema, manifestRelative)) for (const message of validateApplicationContract(entry)) diagnostics.push(diagnostic('error', 'application-contract', manifestRelative, '$', message)) + for (const message of validateApplicationReleaseContract(entry)) diagnostics.push(diagnostic('error', 'application-release-contract', manifestRelative, '$', message)) if (entry.id !== basename(entryDir)) diagnostics.push(diagnostic('error', 'directory-id-mismatch', manifestRelative, '$.id', 'manifest.id 必须与 entries/ 目录名一致')) if (seenIds.has(entry.id)) diagnostics.push(diagnostic('error', 'duplicate-id', manifestRelative, '$.id', 'Registry 条目 ID 重复')) seenIds.add(entry.id) @@ -264,8 +267,8 @@ export function validateRegistry(repoRoot, options = {}) { } if (rootManifest?.stats) { - for (const key of ['totalEntries', 'dockerApps', 'nativeApps', 'artifactApps', 'mcpServices', 'httpApis', 'externalIntegrations']) { - if (['nativeApps', 'artifactApps'].includes(key) && rootManifest.stats[key] === undefined && !counts[key]) continue + for (const key of ['totalEntries', 'dockerApps', 'nativeApps', 'artifactApps', 'webBundles', 'mcpServices', 'httpApis', 'externalIntegrations']) { + if (['nativeApps', 'artifactApps', 'webBundles'].includes(key) && rootManifest.stats[key] === undefined && !counts[key]) continue if (rootManifest.stats[key] === undefined && key === 'externalIntegrations' && counts[key] === 0) continue if (rootManifest.stats[key] !== (counts[key] ?? 0)) diagnostics.push(diagnostic('error', 'stats-mismatch', 'manifest.json', `$.stats.${key}`, `声明 ${rootManifest.stats[key]},实际 ${counts[key] ?? 0}`)) } diff --git a/scripts/validate-registry.mjs b/scripts/validate-registry.mjs index f9b2378..7ae6332 100644 --- a/scripts/validate-registry.mjs +++ b/scripts/validate-registry.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url' import Ajv from 'ajv' import { findStaticMetrics } from './catalog/static-metadata.mjs' import { validateApplicationContract } from './catalog/application-contract.mjs' +import { validateApplicationReleaseContract } from './catalog/application-release-contract.mjs' const scriptDirectory = dirname(fileURLToPath(import.meta.url)) const rootArgumentIndex = process.argv.indexOf('--root') @@ -348,6 +349,7 @@ for (const directoryEntry of await readdir(entriesRoot, { withFileTypes: true }) addError(`entries/${directoryEntry.name}/manifest.json: ${formatAjvErrors(validateEntry.errors)}`) } for (const message of validateApplicationContract(manifest)) addError(`entries/${directoryEntry.name}/manifest.json: ${message}`) + for (const message of validateApplicationReleaseContract(manifest)) addError(`entries/${directoryEntry.name}/manifest.json: ${message}`) if (manifest.type === 'external-integration') { await validateExternalIntegration(entryDirectory, manifest) } @@ -359,6 +361,7 @@ if (rootManifest) { dockerApps: manifests.filter((entry) => entry.type === 'docker-app').length, ...(manifests.some((entry) => entry.type === 'native-app') || rootManifest.stats?.nativeApps !== undefined ? { nativeApps: manifests.filter((entry) => entry.type === 'native-app').length } : {}), ...(manifests.some((entry) => entry.type === 'artifact') || rootManifest.stats?.artifactApps !== undefined ? { artifactApps: manifests.filter((entry) => entry.type === 'artifact').length } : {}), + ...(manifests.some((entry) => entry.type === 'web-bundle') || rootManifest.stats?.webBundles !== undefined ? { webBundles: manifests.filter((entry) => entry.type === 'web-bundle').length } : {}), mcpServices: manifests.filter((entry) => entry.type === 'mcp').length, httpApis: manifests.filter((entry) => entry.type === 'http-api').length, externalIntegrations: manifests.filter((entry) => entry.type === 'external-integration').length,