fix(skill): validate workspace dependency entry types

This commit is contained in:
fz committed 2026-09-18 22:19:37 +08:00
1 parent bffd23d3bc
commit 87cd0d8eb9
6 files changed
+57 -12

No files matched your search

@@ -432,12 +432,16 @@ async function officeFixture(root: string, pythonOnly: boolean) {
return { source, paths }
}
it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'missing-assets'] as const)('composes Office resources through the SDK profile (%s)', async (mode) => {
it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'missing-assets', 'wrong-type'] as const)('composes Office resources through the SDK profile (%s)', async (mode) => {
const root = await mkdtemp(join(tmpdir(), 'sdk-office-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const enabled = mode !== 'unset' && mode !== 'empty'
const { source, paths } = await officeFixture(root, mode === 'python-only')
if (mode === 'missing-assets') await rm(join(root, 'resources', 'office-skills'), { recursive: true })
if (mode === 'wrong-type') {
await rm(paths.python)
await mkdir(paths.python)
}
const requests: Record<string, unknown>[] = []
const server = createServer((request, response) => {
let body = ''
@@ -490,7 +494,24 @@ it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'missing-assets'] a
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(enabled && mode !== 'missing-assets')
}
if (enabled) {
if (mode === 'wrong-type') {
expect(requests).toHaveLength(2)
const messages = requests[1]!.messages as { content: { type: string; tool_use_id?: string; content?: unknown }[] }[]
const result = messages.flatMap(message => message.content).find(block => block.tool_use_id === 'workspace-dependencies')
expect(JSON.parse(JSON.stringify(result).replaceAll(JSON.stringify(paths.python).slice(1, -1), '<python>'))).toMatchInlineSnapshot(`
{
"content": [
{
"text": "Error: primary runtime: expected file at <python>",
"type": "text",
},
],
"is_error": true,
"tool_use_id": "workspace-dependencies",
"type": "tool_result",
}
`)
} else if (enabled) {
expect(requests).toHaveLength(2)
expect(requests[1]!.messages).toEqual(expect.arrayContaining([
expect.objectContaining({ role: 'user', content: expect.arrayContaining([
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/tool-workspace-dependencies/README.md
README.md: 36d1f0080712d6ed71917ead5097beb6b52c3c66
README.zh.md: e491f92d329142498993ce7976045a95e7ea16f7
README.md: 1f7147a1febeb0210befe64dc462e2527013dab3
README.zh.md: f2a1934256776ef97d414be4709a6b918989ae35
@@ -60,7 +60,7 @@ A container can copy both directories into an immutable image layer and set `DSH
<details>
<summary>Implementation internals — click to expand</summary>
`readPrimaryRuntime` and build smoke checks share `parsePrimaryRuntime`. It validates the flat manifest and rejects duplicate normalized distribution names. Legacy `components` metadata is normalized in memory, retaining its consistency checks; a missing legacy distribution map becomes empty. Mixed flat and legacy version fields are rejected. Reads do not rewrite metadata, and equivalent normalized manifests can reuse an installed payload. `workspaceDependencyPaths` derives the platform-specific entries. `installPrimaryRuntime` copies into a staging directory, verifies every declared entry, and swaps it into place while retaining the previous tree on failure; `resolvePrimaryRuntime` verifies the same entries without copying. The tool memoizes the first successful preparation for the plugin's lifetime.
`readPrimaryRuntime` and build smoke checks share `parsePrimaryRuntime`. It validates the flat manifest and rejects duplicate normalized distribution names. Legacy `components` metadata is normalized in memory, retaining its consistency checks; a missing legacy distribution map becomes empty. Mixed flat and legacy version fields are rejected. Reads do not rewrite metadata, and equivalent normalized manifests can reuse an installed payload. `workspaceDependencyPaths` derives the platform-specific entries. `installPrimaryRuntime` copies into a staging directory, requires declared interpreters and scripts to be files and package roots to be directories, and swaps it into place while retaining the previous tree on failure; `resolvePrimaryRuntime` verifies the same entries without copying. The tool memoizes the first successful preparation for the plugin's lifetime.
| File | Responsibility |
|---|---|
@@ -60,7 +60,7 @@ kind: "package-reference"
<details>
<summary>实现细节——点击展开</summary>
`readPrimaryRuntime` 与构建冒烟检查共用 `parsePrimaryRuntime`。它校验扁平清单,拒绝归一化后重名的分发包。旧 `components` 元数据在内存中归一化,并保留旧格式的一致性校验;旧分发包版本表缺失时转换为空表。混用顶层与旧组件版本字段会被拒绝。读取不改写元数据,归一化后等价的清单可复用已安装产物。`workspaceDependencyPaths` 推导各平台的条目路径。`installPrimaryRuntime` 复制到暂存目录、校验每个声明的条目、再换入正式位置,失败时保留旧树;`resolvePrimaryRuntime` 做同样的校验但不复制。工具在插件生命周期内记住首次成功的结果。
`readPrimaryRuntime` 与构建冒烟检查共用 `parsePrimaryRuntime`。它校验扁平清单,拒绝归一化后重名的分发包。旧 `components` 元数据在内存中归一化,并保留旧格式的一致性校验;旧分发包版本表缺失时转换为空表。混用顶层与旧组件版本字段会被拒绝。读取不改写元数据,归一化后等价的清单可复用已安装产物。`workspaceDependencyPaths` 推导各平台的条目路径。`installPrimaryRuntime` 复制到暂存目录、要求声明的解释器和脚本为文件、库路径为目录、再换入正式位置,失败时保留旧树;`resolvePrimaryRuntime` 做同样的校验但不复制。工具在插件生命周期内记住首次成功的结果。
| 文件 | 职责 |
|---|---|
@@ -155,9 +155,17 @@ export function workspaceDependencyPaths(root: string, manifest: PrimaryRuntimeM
}
}
function payloadEntries(paths: WorkspaceDependencies): string[] {
return [paths.python, paths.node, paths.pnpm, paths.pythonPackages, paths.nodePackages]
.filter((path): path is string => path !== undefined)
async function validatePayloadEntries(paths: WorkspaceDependencies): Promise<void> {
for (const [path, kind] of [
[paths.python, 'file'], [paths.node, 'file'], [paths.pnpm, 'file'],
[paths.pythonPackages, 'directory'], [paths.nodePackages, 'directory'],
] as const) {
if (path === undefined) continue
const entry = await stat(path)
if (!(kind === 'file' ? entry.isFile() : entry.isDirectory())) {
throw new Error(`primary runtime: expected ${kind} at ${path}`)
}
}
}
async function exists(path: string): Promise<boolean> {
@@ -183,7 +191,7 @@ async function compatibleManifest(source: string): Promise<PrimaryRuntimeManifes
*/
export async function resolvePrimaryRuntime(source: string): Promise<WorkspaceDependencies> {
const paths = workspaceDependencyPaths(source, await compatibleManifest(source))
for (const path of payloadEntries(paths)) await stat(path)
await validatePayloadEntries(paths)
return paths
}
@@ -202,14 +210,14 @@ export async function installPrimaryRuntime(source: string, root: string): Promi
if (!rootExists && previousExists) await rename(previous, root)
if (await exists(join(root, 'runtime.json')) && JSON.stringify(await readPrimaryRuntime(root)) === JSON.stringify(manifest)) {
const paths = workspaceDependencyPaths(root, manifest)
for (const path of payloadEntries(paths)) await stat(path)
await validatePayloadEntries(paths)
return paths
}
const staging = await mkdtemp(join(dirname(root), '.primary-runtime-'))
try {
await cp(source, staging, { recursive: true, dereference: true })
const paths = workspaceDependencyPaths(staging, manifest)
for (const path of payloadEntries(paths)) await stat(path)
await validatePayloadEntries(paths)
await rm(previous, { recursive: true, force: true })
const replacing = await exists(root)
if (replacing) await rename(root, previous)
@@ -132,6 +132,22 @@ it('refuses linked installation directories without modifying their targets', as
expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
})
it.each(['in-place', 'copy', 'reuse'] as const)('rejects wrong payload entry types during %s preparation', async (mode) => {
for (const field of ['python', 'node', 'pnpm', 'pythonPackages', 'nodePackages'] as const) {
const { source, root, manifest } = await fixture()
if (mode === 'reuse') await installPrimaryRuntime(source, root)
const paths = workspaceDependencyPaths(mode === 'reuse' ? root : source, manifest)
const path = paths[field]!
const directory = field.endsWith('Packages')
await rm(path, { recursive: true })
if (directory) await writeFile(path, 'not a library directory')
else await mkdir(path)
const operation = mode === 'in-place' ? resolvePrimaryRuntime(source) : installPrimaryRuntime(source, root)
await expect(operation).rejects.toThrow(`expected ${directory ? 'directory' : 'file'} at`)
if (mode === 'copy') await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
}
})
it('rejects malformed metadata and incompatible targets', async () => {
const { source, root, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, arch: process.arch === 'x64' ? 'arm64' : 'x64' }))