feat: 以原生应用上架 DesireCore Control 1.4.0 (#6)

This commit is contained in:
2026-09-18 17:35:34 +08:00
committed by GitHub
parent 223be1f04b
commit 07442e03ca
18 changed files with 446 additions and 20 deletions

View File

@@ -18,7 +18,7 @@ if (json) {
} 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(`条目 ${counts.totalEntries}(App ${counts.dockerApps + (counts.nativeApps ?? 0)}(原生 ${counts.nativeApps ?? 0}) / 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`)

View File

@@ -1,4 +1,5 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { isDeepStrictEqual } from 'node:util'
import { basename, join, relative, resolve, sep } from 'node:path'
import { validateJsonSchema } from './json-schema.mjs'
@@ -74,7 +75,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 = manifest.type === 'docker-app' ? 'app' : 'service'
const expectedKind = ['docker-app', 'native-app'].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}`)
@@ -133,6 +134,16 @@ function validateSidecarSemantics(manifest, sidecar, file) {
if (manifest.toolCount !== undefined && sidecar.spec?.toolCount !== manifest.toolCount) add('spec-mismatch', '$.spec.toolCount', 'toolCount 必须与 legacy 一致')
}
if (manifest.type === 'native-app') {
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 的治理事实必须一致')
}
if (manifest.license !== sidecar.governance?.license?.value) add('native-license-mismatch', '$.governance.license', '原生应用许可证必须一致')
if (!isDeepStrictEqual(manifest.source, sidecar.provenance?.content)) add('native-source-mismatch', '$.provenance.content', '制品 URL/ref/SHA-256 必须一致')
if (manifest.requiredClientVersion !== sidecar.compatibility?.requiredClientVersion) add('native-client-mismatch', '$.compatibility.requiredClientVersion', '最低客户端版本必须一致')
if (!isDeepStrictEqual(manifest.placementPolicy, sidecar.spec?.placementPolicy)) add('native-placement-mismatch', '$.spec.placementPolicy', '应用放置策略必须一致')
}
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
@@ -213,10 +224,11 @@ export function validateRegistry(repoRoot, options = {}) {
if (!entry) continue
counts.totalEntries += 1
if (entry.type === 'docker-app') counts.dockerApps += 1
if (entry.type === 'native-app') counts.nativeApps = (counts.nativeApps ?? 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 = entry.type === 'external-integration' ? externalEntrySchema : entrySchema
const effectiveEntrySchema = ['external-integration', 'native-app'].includes(entry.type) ? 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 重复'))
@@ -239,9 +251,10 @@ export function validateRegistry(repoRoot, options = {}) {
}
if (rootManifest?.stats) {
for (const key of ['totalEntries', 'dockerApps', 'mcpServices', 'httpApis', 'externalIntegrations']) {
for (const key of ['totalEntries', 'dockerApps', 'nativeApps', 'mcpServices', 'httpApis', 'externalIntegrations']) {
if (key === 'nativeApps' && rootManifest.stats[key] === undefined && !counts.nativeApps) continue
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]}`))
if (rootManifest.stats[key] !== (key === 'nativeApps' ? (counts.nativeApps ?? 0) : counts[key])) diagnostics.push(diagnostic('error', 'stats-mismatch', 'manifest.json', `$.stats.${key}`, `声明 ${rootManifest.stats[key]},实际 ${counts[key]}`))
}
}

View File

@@ -197,18 +197,20 @@ function codes(report) {
return report.errors.map((item) => item.code)
}
test('当前 v4 条目保持可读,21 个 App/Service 具备 sidecar', () => {
test('当前 v4.1 条目保持可读,原生应用与既有 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,
nativeApps: report.counts.nativeApps,
mcpServices: report.counts.mcpServices,
httpApis: report.counts.httpApis,
externalIntegrations: report.counts.externalIntegrations,
}, {
totalEntries: 22,
totalEntries: 23,
dockerApps: 8,
nativeApps: 1,
mcpServices: 8,
httpApis: 5,
externalIntegrations: 1,

View File

@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import { test, afterEach } from 'node:test'
import { cpSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { validateRegistry } from './catalog/validator.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 }) })
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'registry-native-'))
temporary.push(root)
cpSync(join(source, 'schemas'), join(root, 'schemas'), { recursive: true })
cpSync(join(source, 'entries/desirecore-control'), join(root, 'entries/desirecore-control'), { recursive: true })
const manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8'))
manifest.stats = { totalEntries: 1, dockerApps: 0, nativeApps: 1, mcpServices: 0, httpApis: 0, externalIntegrations: 0 }
writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest))
writeFileSync(join(root, 'SCHEMA_VERSION'), '4.1.0\n')
return root
}
function mutate(root, file, change) {
const path = join(root, file)
const data = JSON.parse(readFileSync(path, 'utf8'))
change(data)
writeFileSync(path, JSON.stringify(data))
}
const entry = 'entries/desirecore-control/'
test('真实原生应用条目是 App,固定 1.4.0 制品,不包含内部 MCP 配置', () => {
const root = fixture()
const report = validateRegistry(root)
assert.equal(report.ok, true, JSON.stringify(report.errors))
assert.equal(report.counts.nativeApps, 1)
assert.equal(report.counts.mcpServices, 0)
const manifest = JSON.parse(readFileSync(join(root, entry, 'manifest.json'), 'utf8'))
assert.equal(manifest.type, 'native-app')
assert.equal(manifest.install.method, 'native-node')
assert.equal(manifest.install.requirements.docker, false)
assert.equal(manifest.version, '1.4.0')
assert.equal(manifest.connection, undefined)
assert.equal(manifest.exposes, undefined)
})
for (const field of ['connection', 'exposes', 'toolCount', 'capabilities']) test('拒绝原生应用内部服务字段 ' + field, () => {
const root = fixture()
mutate(root, entry + 'manifest.json', data => { data[field] = field === 'toolCount' ? 5 : field === 'connection' ? { transport: 'stdio', command: 'node' } : [] })
assert.equal(validateRegistry(root).ok, false)
})
test('摘要、最低客户端版本和统计必须与目录事实一致', () => {
for (const change of [data => { data.provenance.content.sha256 = '0'.repeat(64) }, data => { data.compatibility.requiredClientVersion = '0.0.0' }]) {
const root = fixture()
mutate(root, entry + 'catalog-metadata.v1.json', change)
assert.equal(validateRegistry(root).ok, false)
}
const root = fixture()
mutate(root, 'manifest.json', data => { data.stats.nativeApps = 0 })
assert.equal(validateRegistry(root).ok, false)
})
test('字段顺序不是治理身份,值相同时允许序列化重排', () => {
const root = fixture()
mutate(root, entry + 'manifest.json', data => { data.source = Object.fromEntries(Object.entries(data.source).reverse()) })
assert.equal(validateRegistry(root).ok, true)
})

View File

@@ -348,6 +348,7 @@ if (rootManifest) {
const expectedStats = {
totalEntries: manifests.length,
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 } : {}),
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,

View File

@@ -47,7 +47,8 @@ test('accepts the checked-in Registry fixture', async () => {
try {
const result = runValidator(root)
assert.equal(result.status, 0, result.stderr)
assert.match(result.stdout, /Registry validation passed: 22 entries/)
const manifest = await readJson(join(root, 'manifest.json'))
assert.ok(result.stdout.includes(`Registry validation passed: ${manifest.stats.totalEntries} entries`))
} finally {
await rm(root, { recursive: true, force: true })
}