mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* Squashed 'packages/mobile-mcp/' content from commit c5d7d27fd git-subtree-dir: packages/mobile-mcp git-subtree-split: c5d7d27fd61e4762e15ae4b1c68b6c011be88bb7 * feat(mobile-mcp): vendor mobile-mcp with opt-in 0-1000 relative coordinates Fork mobile-next/mobile-mcp (v0.0.61) into packages/mobile-mcp/ via git subtree, renamed to @qwen-code/mobile-mcp with the following additions: Relative coordinate shim (src/coord-norm.ts): - MOBILE_MCP_COORDINATE_SPACE=1 enables 0-1000 normalized coordinates - MOBILE_MCP_COORDINATE_SCALE configurable (default 1000, 999 for mobile_use) - Input denormalization for click/double_tap/long_press/swipe - Output normalization for list_elements and get_screen_size - Tool description rewriting when enabled - Default off = zero behavior change Android enhancements: - mobile_install_app: -r/-g/-d/-t install flags (Android only) - mobile_ui_dump: full UIAutomator XML hierarchy dump - mobile_adb_pull / mobile_adb_push: file transfer via ADB Infrastructure: - cd-mobile-mcp.yml: npm publish workflow (tag mobile-mcp-v*) - scripts/sync-from-upstream.sh: git subtree pull for upstream sync - .vendored-from / .vendored-patches.md: vendoring metadata - Upstream telemetry disabled by default - eslint.config.js: exclude packages/mobile-mcp from root lint * chore(mobile-mcp): update package-lock.json for workspace dependencies * fix(mobile-mcp): quote all YAML strings to pass yamllint * fix(mobile-mcp): fix cd workflow yaml to pass both yamllint and actionlint * fix(mobile-mcp): address review findings on our additions - ensureScreenSize: log warning instead of silent failure (#4) - invalidateScreenSize on orientation change (#5) - adb_push: path.posix.resolve to prevent /sdcard/ traversal (#6) - adb_pull: readOnlyHint → destructiveHint (writes local file) (#9) - adb_push: remove validateOutputPath on read-source local_path (#11) - normalizeElementResult: log error instead of bare catch (#16) - rewriteDescription: remove dead duplicate regex (#17) - cd workflow: add test step between build and publish (#19) * fix(mobile-mcp): update server.json identity and fix package.json main entrypoint --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
88 lines
2.3 KiB
TypeScript
88 lines
2.3 KiB
TypeScript
import path from "node:path";
|
|
import os from "node:os";
|
|
import fs from "node:fs";
|
|
import { ActionableError } from "./robot";
|
|
|
|
export function validatePackageName(packageName: string): void {
|
|
if (!/^[a-zA-Z0-9._]+$/.test(packageName)) {
|
|
throw new ActionableError(`Invalid package name: "${packageName}"`);
|
|
}
|
|
}
|
|
|
|
export function validateLocale(locale: string): void {
|
|
if (!/^[a-zA-Z0-9,\- ]+$/.test(locale)) {
|
|
throw new ActionableError(`Invalid locale: "${locale}"`);
|
|
}
|
|
}
|
|
|
|
function getAllowedRoots(): string[] {
|
|
const roots = [
|
|
os.tmpdir(),
|
|
process.cwd(),
|
|
];
|
|
|
|
// macOS /tmp is a symlink to /private/tmp, add both to be safe
|
|
if (process.platform === "darwin") {
|
|
roots.push("/tmp");
|
|
roots.push("/private/tmp");
|
|
}
|
|
|
|
return roots.map(r => path.resolve(r));
|
|
}
|
|
|
|
function isPathUnderRoot(filePath: string, root: string): boolean {
|
|
const relative = path.relative(root, filePath);
|
|
if (relative === "") {
|
|
return false;
|
|
}
|
|
|
|
if (path.isAbsolute(relative)) {
|
|
return false;
|
|
}
|
|
|
|
if (relative.startsWith("..")) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function validateFileExtension(filePath: string, allowedExtensions: string[], toolName: string): void {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
if (!allowedExtensions.includes(ext)) {
|
|
throw new ActionableError(`${toolName} requires a ${allowedExtensions.join(", ")} file extension, got: "${ext || "(none)"}"`);
|
|
}
|
|
}
|
|
|
|
function resolveWithSymlinks(filePath: string): string {
|
|
const resolved = path.resolve(filePath);
|
|
const dir = path.dirname(resolved);
|
|
const filename = path.basename(resolved);
|
|
|
|
try {
|
|
return path.join(fs.realpathSync(dir), filename);
|
|
} catch {
|
|
return resolved;
|
|
}
|
|
}
|
|
|
|
export function validateOutputPath(filePath: string): void {
|
|
const resolved = resolveWithSymlinks(filePath);
|
|
const allowedRoots = getAllowedRoots();
|
|
const isWindows = process.platform === "win32";
|
|
|
|
const isAllowed = allowedRoots.some(root => {
|
|
if (isWindows) {
|
|
return isPathUnderRoot(resolved.toLowerCase(), root.toLowerCase());
|
|
}
|
|
|
|
return isPathUnderRoot(resolved, root);
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
const dir = path.dirname(resolved);
|
|
throw new ActionableError(
|
|
`"${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.`
|
|
);
|
|
}
|
|
}
|