qwen-code/scripts/check-serve-fast-path-bundle.js
jinye ea536d361f
fix(cli): Handle ACP read_file for managed local paths (#6021)
* fix(cli): handle ACP read_file local roots

Allow ACP read_file calls to fall back to local reads for explicitly permitted local roots when the serve workspace boundary rejects them, and preserve useful messages for plain object read errors.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Add the missing getUserAutoMemoryRoot export to the acpAgent test core mock so the updated acpAgent import resolves under Vitest.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp): Narrow local read fallback temp roots

Remove the broad OS temp directory from default read_file allow roots and ACP local read fallback roots. Keep qwen-managed temp roots readable and reuse the shared isSubpath helper after realpath resolution for ACP fallback containment.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix CI failure on PR #6021

Add the serve fast-path bundle check script and root npm script that the current CI workflow invokes. This mirrors the already-merged mainline check without pulling unrelated workflow or test config changes into this PR.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address ACP read error review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden ACP error normalization

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix Windows CI path expectation (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6021)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-30 11:15:49 +00:00

315 lines
8.4 KiB
JavaScript

#!/usr/bin/env node
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const DEFAULT_METAFILE_PATH = resolve('dist/esbuild.json');
const METAFILE_BUILD_COMMAND =
'npm run build -- --cli-only && cross-env DEV=true npm run bundle';
const SERVE_PRE_LISTEN_ROOTS = [
{
label: 'serve fast path entry',
suffixes: [
'packages/cli/src/serve/fast-path.ts',
'packages/cli/dist/src/serve/fast-path.js',
],
},
{
label: 'serve fast path settings',
suffixes: [
'packages/cli/src/serve/fast-path-settings.ts',
'packages/cli/dist/src/serve/fast-path-settings.js',
],
},
{
label: 'run qwen serve entry',
suffixes: [
'packages/cli/src/serve/run-qwen-serve.ts',
'packages/cli/dist/src/serve/run-qwen-serve.js',
],
},
];
const FORBIDDEN_SOURCE_INPUTS = [
{
label: 'Serve ACP compatibility shim',
suffixes: [
'packages/cli/src/serve/acp-session-bridge.ts',
'packages/cli/dist/src/serve/acp-session-bridge.js',
],
},
{
label: 'ACP bridge runtime',
suffixes: [
'packages/acp-bridge/src/bridge.ts',
'packages/acp-bridge/dist/bridge.js',
],
},
{
label: 'ACP bridge client runtime',
suffixes: [
'packages/acp-bridge/src/bridgeClient.ts',
'packages/acp-bridge/dist/bridgeClient.js',
],
},
{
label: 'ACP spawnChannel runtime',
suffixes: [
'packages/acp-bridge/src/spawnChannel.ts',
'packages/acp-bridge/dist/spawnChannel.js',
],
},
{
label: 'ACP permission mediator runtime',
suffixes: [
'packages/acp-bridge/src/permissionMediator.ts',
'packages/acp-bridge/dist/permissionMediator.js',
],
},
{
label: 'ACP compaction engine runtime',
suffixes: [
'packages/acp-bridge/src/compactionEngine.ts',
'packages/acp-bridge/dist/compactionEngine.js',
],
},
{
label: 'Core shell tool runtime',
suffixes: [
'packages/core/src/tools/shell.ts',
'packages/core/dist/src/tools/shell.js',
],
},
];
const FORBIDDEN_VENDOR_PACKAGES = [
{ label: 'glob vendor package', packageName: 'glob' },
{ label: 'chokidar vendor package', packageName: 'chokidar' },
{ label: '@iarna/toml vendor package', packageName: '@iarna/toml' },
{ label: 'fzf vendor package', packageName: 'fzf' },
];
export function normalizeMetafilePath(filePath) {
return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
}
function inputMatchesSuffix(input, suffix) {
const normalizedInput = normalizeMetafilePath(input);
return normalizedInput === suffix || normalizedInput.endsWith(`/${suffix}`);
}
function inputMatchesAnySuffix(input, suffixes) {
return suffixes.some((suffix) => inputMatchesSuffix(input, suffix));
}
function inputMatchesPackage(input, packageName) {
const normalizedInput = normalizeMetafilePath(input);
const marker = `node_modules/${packageName}/`;
return (
normalizedInput === `node_modules/${packageName}` ||
normalizedInput.includes(marker)
);
}
function normalizeOutputs(metafile) {
return new Map(
Object.entries(metafile.outputs ?? {}).map(([outputPath, output]) => [
normalizeMetafilePath(outputPath),
output,
]),
);
}
function findServePreListenRootOutputs(outputs) {
const rootOutputs = [];
const missingRoots = [];
for (const root of SERVE_PRE_LISTEN_ROOTS) {
let matchedOutput;
for (const [outputPath, output] of outputs) {
for (const input of Object.keys(output.inputs ?? {})) {
if (inputMatchesAnySuffix(input, root.suffixes)) {
matchedOutput = outputPath;
break;
}
}
if (matchedOutput) break;
}
if (matchedOutput) {
rootOutputs.push(matchedOutput);
} else {
missingRoots.push(`${root.label} (${root.suffixes.join(' or ')})`);
}
}
if (missingRoots.length > 0) {
throw new Error(
'Could not find bundled outputs for serve pre-listen roots:\n' +
missingRoots.map((root) => `- ${root}`).join('\n') +
`\nRun \`${METAFILE_BUILD_COMMAND}\` to produce the metafile.`,
);
}
return [...new Set(rootOutputs)];
}
function collectStaticClosure(outputs, entryOutputs) {
const queue = [...entryOutputs];
const closure = new Set(queue);
const parent = new Map();
for (let i = 0; i < queue.length; i++) {
const outputPath = queue[i];
const output = outputs.get(outputPath);
for (const bundledImport of output?.imports ?? []) {
if (bundledImport.external) continue;
if (bundledImport.kind === 'dynamic-import') continue;
const importedOutput = normalizeMetafilePath(bundledImport.path);
if (!outputs.has(importedOutput) || closure.has(importedOutput)) {
continue;
}
closure.add(importedOutput);
parent.set(importedOutput, outputPath);
queue.push(importedOutput);
}
}
return { closure, parent };
}
function buildImportPath(entryOutputs, outputPath, parent) {
const roots = new Set(entryOutputs);
const reversed = [outputPath];
let current = outputPath;
while (!roots.has(current)) {
current = parent.get(current);
if (!current) break;
reversed.push(current);
}
return reversed.reverse();
}
export function findServeFastPathBundleOffenders(metafile) {
const outputs = normalizeOutputs(metafile);
const entryOutputs = findServePreListenRootOutputs(outputs);
const { closure, parent } = collectStaticClosure(outputs, entryOutputs);
const offenders = [];
const seen = new Set();
for (const outputPath of closure) {
const output = outputs.get(outputPath);
const inputs = Object.keys(output?.inputs ?? {});
for (const input of inputs) {
const sourceMatch = FORBIDDEN_SOURCE_INPUTS.find(({ suffixes }) =>
inputMatchesAnySuffix(input, suffixes),
);
if (sourceMatch) {
addOffender(
sourceMatch.label,
normalizeMetafilePath(input),
outputPath,
);
}
const vendorMatch = FORBIDDEN_VENDOR_PACKAGES.find(({ packageName }) =>
inputMatchesPackage(input, packageName),
);
if (vendorMatch) {
addOffender(
vendorMatch.label,
normalizeMetafilePath(input),
outputPath,
);
}
}
}
return offenders;
function addOffender(label, matchedInput, outputPath) {
const key = `${label}\0${matchedInput}\0${outputPath}`;
if (seen.has(key)) return;
seen.add(key);
offenders.push({
label,
matchedInput,
outputPath,
bytes: outputs.get(outputPath)?.bytes ?? 0,
importPath: buildImportPath(entryOutputs, outputPath, parent),
});
}
}
export function formatServeFastPathBundleOffenders(offenders) {
return offenders
.map((offender) => {
const importPath = offender.importPath.join(' -> ');
return [
`- ${offender.label}`,
` input: ${offender.matchedInput}`,
` output: ${offender.outputPath} (${offender.bytes} bytes)`,
` static path: ${importPath}`,
].join('\n');
})
.join('\n');
}
export function checkServeFastPathBundle({
metafilePath = DEFAULT_METAFILE_PATH,
} = {}) {
if (!existsSync(metafilePath)) {
throw new Error(
`Missing esbuild metafile at ${metafilePath}. ` +
`Run \`${METAFILE_BUILD_COMMAND}\` to produce it.`,
);
}
let metafile;
try {
metafile = JSON.parse(readFileSync(metafilePath, 'utf8'));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Invalid esbuild metafile at ${metafilePath}: ${reason}. ` +
`Run \`${METAFILE_BUILD_COMMAND}\` to regenerate it.`,
);
}
const offenders = findServeFastPathBundleOffenders(metafile);
return { ok: offenders.length === 0, offenders };
}
function main() {
try {
const result = checkServeFastPathBundle();
if (result.ok) {
console.log('Serve fast-path bundle closure check passed.');
return;
}
console.error(
'Serve fast-path bundle closure includes pre-listen runtime modules:\n' +
formatServeFastPathBundleOffenders(result.offenders),
);
process.exitCode = 1;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
main();
}