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

@@ -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'))
})