fix: 绑定目录获取与生命周期收据 (#10)

* fix(apps): 绑定目录快照与最终执行

* fix(apps): 结算目录执行前失败

* fix(services): 拒绝不完整目录响应

* fix(services): 按生命周期收据精确卸载

* fix(apps): 采用服务端生命周期收据

* fix(apps): 绑定目录操作防重放

* fix(apps): 原子切换重装收据
This commit is contained in:
2026-08-31 04:00:50 -04:00
committed by GitHub
parent 3890514896
commit ee0bc7b044
5 changed files with 1558 additions and 36 deletions

View File

@@ -0,0 +1,638 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
import {
buildInstalledCatalogReceiptPatch,
buildServiceCatalogDeleteTarget,
completeServiceCatalogReceipt,
evaluateRegistryCatalogResolverResult,
parseRegistryCatalogAcquisitionMessage,
parseRegistryCatalogAcquisitionLocatorMessage,
planRegistryCatalogPreExecutionSettlement,
resolveServiceUninstallOwnership,
registryCatalogRuntimeServerId,
} from '../scripts/registry-catalog-acquisition.mjs'
const here = dirname(fileURLToPath(import.meta.url))
const skillPath = join(here, '..', 'SKILL.md')
const commitA = 'a'.repeat(40)
const commitB = 'b'.repeat(40)
const operationA = '11111111-1111-4111-8111-111111111111'
const operationB = '22222222-2222-4222-8222-222222222222'
function envelope(overrides = {}) {
const sourceId = overrides.sourceId ?? 'registry:source-a'
return {
kind: 'app',
sourceId,
entryId: 'same-id',
snapshot: {
schemaVersion: 1,
catalogSourceId: sourceId,
catalogCommit: overrides.catalogCommit ?? commitA,
catalogPath: 'entries/same-id',
releaseVersion: overrides.releaseVersion ?? '1.2.3',
contentRef: 'v1.2.3',
contentSha256: 'c'.repeat(64),
},
operation: {
action: overrides.action ?? 'install',
deviceId: overrides.deviceId ?? 'device-a',
operationId: overrides.operationId ?? operationA,
},
}
}
function request(overrides = {}) {
return envelope(overrides)
}
function pendingEntry(overrides = {}) {
return {
sourceId: overrides.sourceId ?? 'registry:source-a',
entryId: overrides.entryId ?? 'same-id',
deviceId: overrides.deviceId ?? 'device-a',
deviceName: overrides.deviceName ?? 'Device A',
status: overrides.status ?? 'installing',
operationId: overrides.operationId ?? operationA,
}
}
function manifestFor(expected) {
return {
id: expected.entryId,
type: expected.kind === 'service' ? 'mcp' : 'docker-app',
name: 'Same ID',
version: expected.snapshot.releaseVersion,
description: 'Server-authorized lifecycle',
}
}
function receiptFor(expected, overrides = {}) {
const manifest = manifestFor(expected)
return {
...expected.snapshot,
kind: expected.kind,
entryId: expected.entryId,
lifecycle: {
manifest,
...(expected.kind === 'app' ? { installGuide: '# Server-authorized guide\n' } : {}),
},
...overrides,
}
}
function success(expected, overrides = {}) {
return {
success: true,
data: {
kind: expected.kind,
sourceId: expected.sourceId,
entryId: expected.entryId,
snapshot: expected.snapshot,
manifest: manifestFor(expected),
installGuide: '# Server-authorized guide\n',
catalogReceipt: receiptFor(expected),
...overrides,
},
}
}
test('missing, duplicate, and malformed machine lines fail closed', () => {
assert.throws(() => parseRegistryCatalogAcquisitionMessage('请安装 Same ID'), /machine_line_missing/)
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify(request())}\nRegistryCatalogAcquisition=${JSON.stringify(request())}`
),
/machine_line_ambiguous/
)
assert.throws(
() => parseRegistryCatalogAcquisitionMessage('RegistryCatalogAcquisition={"kind":"app"'),
/machine_line_malformed/
)
})
test('App request rejects caller-provided install/connection and source/snapshot mismatch', () => {
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify({ ...request(), install: { command: 'bash' } })}`
),
/app_client_config_forbidden/
)
const mismatched = envelope()
mismatched.snapshot.catalogSourceId = 'registry:source-b'
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(`RegistryCatalogAcquisition=${JSON.stringify(mismatched)}`),
/source_identity_mismatch/
)
})
test('same entryId from source A and source B remains two exact acquisition identities', () => {
const sourceA = parseRegistryCatalogAcquisitionMessage(
`安装来源 A\nRegistryCatalogAcquisition=${JSON.stringify(envelope())}`
)
const sourceB = parseRegistryCatalogAcquisitionMessage(
`安装来源 B\nRegistryCatalogAcquisition=${JSON.stringify(envelope({
sourceId: 'registry:source-b',
catalogCommit: commitB,
}))}`
)
assert.equal(sourceA.request.entryId, sourceB.request.entryId)
assert.notEqual(sourceA.request.sourceId, sourceB.request.sourceId)
assert.notDeepEqual(sourceA.request.snapshot, sourceB.request.snapshot)
assert.deepEqual(sourceA.request.operation, {
action: 'install', deviceId: 'device-a', operationId: operationA,
})
})
test('400/404 and every real resolver 409 preserve exact error codes and block execution', () => {
const expected = request()
for (const [status, errorCode] of [
[400, 'registry_acquisition_invalid_request'],
[404, 'registry_acquisition_not_found'],
[409, 'registry_acquisition_snapshot_stale'],
[409, 'registry_acquisition_blocked'],
[409, 'registry_acquisition_client_upgrade_required'],
[409, 'registry_acquisition_config_mismatch'],
[409, 'registry_acquisition_install_guide_unavailable'],
[409, 'registry_acquisition_receipt_missing'],
[409, 'registry_acquisition_ownership_mismatch'],
]) {
assert.deepEqual(
evaluateRegistryCatalogResolverResult(expected, status, {
success: false,
errorCode,
reasons: ['missing-canonical-or-kind-mismatch'],
}),
{
allowed: false,
errorCode,
status,
mayExecuteCommands: false,
reasons: ['missing-canonical-or-kind-mismatch'],
}
)
}
assert.deepEqual(
evaluateRegistryCatalogResolverResult(expected, 409, { success: false, errorCode: 'blocked' }),
{ allowed: false, errorCode: 'unexpected_resolver_error', status: 409, mayExecuteCommands: false }
)
})
test('parsed locator settles resolver/Human Gate/pre-execution stops without cross-source writes', () => {
const acquisition = parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify(envelope({
sourceId: 'registry:source-b',
catalogCommit: commitB,
action: 'reinstall',
}))}`
)
const entries = [
pendingEntry({ sourceId: 'registry:source-a', status: 'reinstalling' }),
pendingEntry({ sourceId: 'registry:source-b', status: 'reinstalling' }),
]
for (const stage of ['resolver', 'human_gate', 'pre_execution']) {
assert.deepEqual(planRegistryCatalogPreExecutionSettlement({ stage, locator: acquisition.locator, entries }), {
allowed: false,
mayExecuteCommands: false,
settlement: {
sourceId: 'registry:source-b',
entryId: 'same-id',
deviceId: 'device-a',
operationId: operationA,
status: 'installed',
},
reason: `registry_catalog_${stage}_stopped`,
})
}
assert.equal(planRegistryCatalogPreExecutionSettlement({
stage: 'resolver',
locator: acquisition.locator,
entries: [pendingEntry({
sourceId: 'registry:source-b',
status: 'reinstalling',
operationId: operationB,
})],
}).settlement, null)
})
test('first install stops as failed, uninstall/Human Gate cancellation restores installed', () => {
const install = parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify(envelope())}`
)
assert.equal(planRegistryCatalogPreExecutionSettlement({
stage: 'parse',
locator: install.locator,
entries: [pendingEntry()],
}).settlement.status, 'failed')
const uninstall = parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify(envelope({ action: 'uninstall' }))}`
)
assert.equal(planRegistryCatalogPreExecutionSettlement({
stage: 'human_gate',
locator: uninstall.locator,
entries: [pendingEntry({ status: 'uninstalling' })],
}).settlement.status, 'installed')
assert.deepEqual(planRegistryCatalogPreExecutionSettlement({
stage: 'human_gate',
locator: uninstall.locator,
entries: [pendingEntry({ status: 'uninstalling', deviceId: 'device-b' })],
}), {
allowed: false,
mayExecuteCommands: false,
settlement: null,
reason: 'registry_catalog_pending_intent_not_found',
})
})
test('missing or malformed machine input has no locator and cannot guess a ledger write', () => {
assert.throws(() => parseRegistryCatalogAcquisitionMessage('请安装 Same ID'), /machine_line_missing/)
assert.throws(
() => parseRegistryCatalogAcquisitionMessage('RegistryCatalogAcquisition={"kind":"app"'),
/machine_line_malformed/
)
assert.throws(
() => planRegistryCatalogPreExecutionSettlement({
stage: 'parse',
locator: null,
entries: [pendingEntry()],
}),
/settlement_locator_invalid/
)
})
test('a parsed App locator can settle an invalid snapshot without authorizing acquisition', () => {
const invalid = envelope()
invalid.snapshot.catalogCommit = 'mutable-main'
const message = `RegistryCatalogAcquisition=${JSON.stringify(invalid)}`
const locator = parseRegistryCatalogAcquisitionLocatorMessage(message)
assert.deepEqual(locator, {
sourceId: 'registry:source-a',
entryId: 'same-id',
operation: { action: 'install', deviceId: 'device-a', operationId: operationA },
})
assert.throws(() => parseRegistryCatalogAcquisitionMessage(message), /catalog_commit_invalid/)
assert.equal(planRegistryCatalogPreExecutionSettlement({
stage: 'parse',
locator,
entries: [pendingEntry()],
}).settlement.status, 'failed')
})
test('valid 200 response returns only server-authorized App manifest and install guide', () => {
const expected = request()
const result = evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
snapshot: {
contentSha256: expected.snapshot.contentSha256,
releaseVersion: expected.snapshot.releaseVersion,
catalogPath: expected.snapshot.catalogPath,
catalogCommit: expected.snapshot.catalogCommit,
catalogSourceId: expected.snapshot.catalogSourceId,
schemaVersion: expected.snapshot.schemaVersion,
contentRef: expected.snapshot.contentRef,
},
install: { command: 'must-not-be-consumed-by-app' },
connection: { url: 'http://must-not-be-consumed.invalid' },
}))
assert.deepEqual(result, {
allowed: true,
kind: 'app',
sourceId: expected.sourceId,
entryId: expected.entryId,
snapshot: expected.snapshot,
manifest: manifestFor(expected),
installGuide: '# Server-authorized guide\n',
catalogReceipt: receiptFor(expected),
})
})
test('response source, snapshot, manifest identity, or install guide mismatch fails closed', () => {
const expected = request()
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, { sourceId: 'registry:source-b' })),
/resolver_identity_mismatch/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
snapshot: { ...expected.snapshot, catalogCommit: commitB },
})),
/resolver_identity_mismatch/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
manifest: { id: 'other-id', type: 'docker-app' },
})),
/manifest_identity_mismatch/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, { installGuide: '' })),
/install_guide_invalid/
)
})
test('server receipt candidate is mandatory, immutable, and capped at 64 KiB', () => {
const expected = request()
const missing = success(expected)
delete missing.data.catalogReceipt
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, missing),
/resolver_response_invalid/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
catalogReceipt: {
...receiptFor(expected),
catalogCommit: commitB,
},
})),
/receipt_identity_mismatch/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
catalogReceipt: {
...receiptFor(expected),
lifecycle: {
...receiptFor(expected).lifecycle,
manifest: { ...manifestFor(expected), version: '9.9.9' },
},
},
})),
/lifecycle_identity_mismatch/
)
const oversizedGuide = 'x'.repeat(64 * 1024 + 1)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
installGuide: oversizedGuide,
catalogReceipt: {
...receiptFor(expected),
lifecycle: { manifest: manifestFor(expected), installGuide: oversizedGuide },
},
})),
/receipt_install_guide_invalid/
)
assert.throws(
() => evaluateRegistryCatalogResolverResult(expected, 200, success(expected, {
installGuide: '# changed guide\n',
})),
/receipt_install_guide_mismatch/
)
const candidate = receiptFor(expected)
assert.deepEqual(buildInstalledCatalogReceiptPatch({
sourceId: expected.sourceId,
entryId: expected.entryId,
operationId: expected.operation.operationId,
catalogReceipt: candidate,
}), {
sourceId: expected.sourceId,
operationId: expected.operation.operationId,
status: 'installed',
version: expected.snapshot.releaseVersion,
catalogReceipt: candidate,
})
})
test('reinstall atomically switches v1 to v2 while failure keeps v1 facts', () => {
const oldRequest = request({ releaseVersion: '1.0.0', catalogCommit: commitA })
const target = request({
action: 'reinstall',
releaseVersion: '2.0.0',
catalogCommit: commitB,
operationId: operationB,
})
const oldReceipt = receiptFor(oldRequest)
const pendingCatalogReceipt = {
...target.snapshot,
kind: 'app',
entryId: target.entryId,
}
const candidate = receiptFor(target)
const resolved = evaluateRegistryCatalogResolverResult(target, 200, success(target))
assert.deepEqual(resolved.catalogReceipt, candidate)
assert.deepEqual(buildInstalledCatalogReceiptPatch({
sourceId: target.sourceId,
entryId: target.entryId,
operationId: target.operation.operationId,
catalogReceipt: resolved.catalogReceipt,
}), {
sourceId: target.sourceId,
operationId: target.operation.operationId,
status: 'installed',
version: '2.0.0',
catalogReceipt: candidate,
})
const failure = planRegistryCatalogPreExecutionSettlement({
stage: 'human_gate',
locator: { sourceId: target.sourceId, entryId: target.entryId, operation: target.operation },
entries: [{
...pendingEntry({ status: 'reinstalling', operationId: operationB }),
version: '1.0.0',
catalogReceipt: oldReceipt,
pendingCatalogReceipt,
}],
})
assert.deepEqual(failure.settlement, {
sourceId: target.sourceId,
entryId: target.entryId,
deviceId: target.operation.deviceId,
operationId: target.operation.operationId,
status: 'installed',
})
assert.equal(Object.hasOwn(failure.settlement, 'version'), false)
assert.equal(Object.hasOwn(failure.settlement, 'catalogReceipt'), false)
assert.equal(Object.hasOwn(failure.settlement, 'pendingCatalogReceipt'), false)
assert.throws(() => evaluateRegistryCatalogResolverResult(target, 200, success(target, {
catalogReceipt: { ...candidate, catalogCommit: commitA },
})), /receipt_identity_mismatch/)
})
test('Service keeps only resolver-returned install and connection after exact identity validation', () => {
const expected = {
...request(),
kind: 'service',
install: { method: 'npx', packageName: '@example/server' },
connection: { transport: 'stdio', command: 'npx', args: ['@example/server'] },
}
const result = evaluateRegistryCatalogResolverResult(expected, 200, {
success: true,
data: {
kind: 'service',
sourceId: expected.sourceId,
entryId: expected.entryId,
snapshot: expected.snapshot,
manifest: manifestFor(expected),
install: { method: 'npx', packageName: '@example/server@1.2.3' },
connection: { transport: 'stdio', command: 'npx', args: ['@example/server@1.2.3'] },
catalogReceipt: receiptFor(expected),
},
})
assert.deepEqual(result, {
allowed: true,
kind: 'service',
sourceId: expected.sourceId,
entryId: expected.entryId,
snapshot: expected.snapshot,
manifest: manifestFor(expected),
install: { method: 'npx', packageName: '@example/server@1.2.3' },
connection: { transport: 'stdio', command: 'npx', args: ['@example/server@1.2.3'] },
catalogReceipt: receiptFor(expected),
})
})
test('App and Service both require a valid operation UUID', () => {
const { operation: _operation, ...serviceRequest } = request()
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify({ ...serviceRequest, kind: 'service' })}`
),
/(?:request|operation)_invalid/
)
const missingId = request()
delete missingId.operation.operationId
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify(missingId)}`
),
/operation_invalid/
)
for (const kind of ['app', 'service']) {
assert.throws(
() => parseRegistryCatalogAcquisitionMessage(
`RegistryCatalogAcquisition=${JSON.stringify({
...request(),
kind,
operation: { ...request().operation, operationId: 'not-a-uuid' },
})}`
),
/operation_id_invalid/
)
}
})
test('Service 200 response without install or connection fails closed before execution', () => {
const expected = { ...request(), kind: 'service' }
const baseData = {
kind: 'service',
sourceId: expected.sourceId,
entryId: expected.entryId,
snapshot: expected.snapshot,
manifest: manifestFor(expected),
catalogReceipt: receiptFor(expected),
}
assert.throws(() => evaluateRegistryCatalogResolverResult(expected, 200, {
success: true,
data: { ...baseData, connection: { transport: 'stdio', command: 'npx' } },
}), /resolver_response_invalid/)
assert.throws(() => evaluateRegistryCatalogResolverResult(expected, 200, {
success: true,
data: { ...baseData, install: { method: 'npx', packageName: '@example/server' } },
}), /resolver_response_invalid/)
})
test('MCP only completes the server receipt candidate and builds a CAS-safe PATCH', () => {
const expected = { ...request(), kind: 'service' }
const runtimeServerId = registryCatalogRuntimeServerId(expected.sourceId, expected.entryId)
const candidate = receiptFor(expected)
assert.deepEqual(completeServiceCatalogReceipt(candidate, runtimeServerId), {
...candidate,
runtimeServerId,
})
assert.deepEqual(buildInstalledCatalogReceiptPatch({
sourceId: expected.sourceId,
entryId: expected.entryId,
operationId: expected.operation.operationId,
catalogReceipt: candidate,
runtimeServerId,
}), {
sourceId: expected.sourceId,
operationId: expected.operation.operationId,
status: 'installed',
version: expected.snapshot.releaseVersion,
catalogReceipt: { ...candidate, runtimeServerId },
})
assert.throws(
() => completeServiceCatalogReceipt(candidate, 'well_formed_but_wrong'),
/runtime_server_id_mismatch/
)
assert.throws(
() => buildInstalledCatalogReceiptPatch({
sourceId: 'registry:source-b',
entryId: expected.entryId,
operationId: expected.operation.operationId,
catalogReceipt: candidate,
runtimeServerId,
}),
/receipt_identity_mismatch/
)
})
test('Service uninstall authorizes only exact active service receipt and runtime key', () => {
const expected = request({ action: 'uninstall' })
const locator = {
sourceId: expected.sourceId,
entryId: expected.entryId,
operation: expected.operation,
}
const receipt = completeServiceCatalogReceipt(
receiptFor({ ...expected, kind: 'service' }),
registryCatalogRuntimeServerId(expected.sourceId, expected.entryId)
)
const entry = { ...pendingEntry({ status: 'uninstalling' }), catalogReceipt: receipt }
assert.deepEqual(resolveServiceUninstallOwnership({ locator, snapshot: expected.snapshot, entries: [entry] }), {
allowed: true,
deleteAllowed: true,
sourceId: expected.sourceId,
entryId: expected.entryId,
deviceId: expected.operation.deviceId,
operationId: expected.operation.operationId,
runtimeServerId: registryCatalogRuntimeServerId(expected.sourceId, expected.entryId),
})
assert.deepEqual(buildServiceCatalogDeleteTarget(locator), {
entryId: expected.entryId,
sourceId: expected.sourceId,
deviceId: expected.operation.deviceId,
operationId: expected.operation.operationId,
})
for (const invalidReceipt of [
{ ...receipt, kind: undefined },
{ ...receipt, kind: 'app' },
{ ...receipt, runtimeServerId: undefined },
]) {
assert.equal(resolveServiceUninstallOwnership({
locator,
snapshot: expected.snapshot,
entries: [{ ...entry, catalogReceipt: invalidReceipt }],
}).deleteAllowed, false)
}
assert.equal(resolveServiceUninstallOwnership({
locator,
snapshot: expected.snapshot,
entries: [{ ...entry, sourceId: 'registry:source-b' }],
}).deleteAllowed, false)
assert.equal(resolveServiceUninstallOwnership({
locator,
snapshot: expected.snapshot,
entries: [{ ...entry, operationId: operationB }],
}).deleteAllowed, false)
})
test('Skill contract has no fixed official/local Registry fallback and resolves before execution', async () => {
const skill = await readFile(skillPath, 'utf8')
assert.doesNotMatch(skill, /registry\/official\/entries/)
assert.match(skill, /POST \/api\/registry\/acquisitions\/resolve/)
assert.match(skill, /禁止 fallback/)
assert.match(skill, /plan-settlement/)
assert.match(skill, /lifecycle receipt/)
assert.match(skill, /registry_acquisition_receipt_missing/)
assert.match(skill, /build-service-delete/)
assert.match(skill, /operationId=<operationId>/)
assert.ok(skill.indexOf('解析机器消息并调用 resolver') < skill.indexOf('环境校验'))
assert.ok(skill.indexOf('### 执行前协议') < skill.indexOf('`docker version`'))
})