fix: 完全重写tools.ts—safePath全部到位+命令净化+启动验证通过
This commit is contained in:
@@ -1,29 +1,18 @@
|
|||||||
/**
|
|
||||||
* P0 Core Tool Implementations
|
|
||||||
*
|
|
||||||
* file_list, file_read, file_write, file_move, exec, process_list, sysinfo.
|
|
||||||
* These map to C-002 through C-011 in the backlog.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
|
|
||||||
/** Sandbox: restrict file ops to this base directory or env GCA_WORKSPACE */
|
|
||||||
const BASE_DIR = path.resolve(process.env.GCA_WORKSPACE ?? process.cwd());
|
const BASE_DIR = path.resolve(process.env.GCA_WORKSPACE ?? process.cwd());
|
||||||
|
|
||||||
function safePath(p: string): string {
|
function safePath(p: string): string {
|
||||||
const resolved = path.resolve(BASE_DIR, p);
|
const r = path.resolve(BASE_DIR, p);
|
||||||
if (!resolved.startsWith(BASE_DIR)) throw new Error(`Path traversal blocked: ${p}`);
|
if (!r.startsWith(BASE_DIR)) throw new Error(`Path traversal blocked: ${p}`);
|
||||||
return resolved;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-002: file_list 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-002: file_list ────────────────────────────────────────
|
||||||
export async function file_list(args: {
|
export async function file_list(args: { path: string; recursive?: boolean }): Promise<any> {
|
||||||
path: string;
|
|
||||||
recursive?: boolean;
|
|
||||||
}): Promise<any> {
|
|
||||||
const dir = safePath(args.path || ".");
|
const dir = safePath(args.path || ".");
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
const result = entries.map(e => ({
|
const result = entries.map(e => ({
|
||||||
@@ -34,78 +23,55 @@ export async function file_list(args: {
|
|||||||
return { path: dir, entries: result, count: result.length };
|
return { path: dir, entries: result, count: result.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-003: file_read 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-003: file_read ────────────────────────────────────────
|
||||||
export async function file_read(args: {
|
export async function file_read(args: { path: string; encoding?: string; offset?: number; limit?: number }): Promise<any> {
|
||||||
path: string;
|
const filePath = safePath(args.path);
|
||||||
encoding?: string;
|
const enc = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding;
|
||||||
offset?: number;
|
let content = fs.readFileSync(filePath, enc);
|
||||||
limit?: number;
|
if (enc === "utf8") {
|
||||||
}): Promise<any> {
|
|
||||||
const encoding = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding;
|
|
||||||
let content = fs.readFileSync(safePath(args.path, encoding);
|
|
||||||
if (encoding === "utf8") {
|
|
||||||
const lines = (content as string).split("\n");
|
const lines = (content as string).split("\n");
|
||||||
const start = args.offset ?? 0;
|
content = lines.slice(args.offset ?? 0, args.limit ? (args.offset ?? 0) + args.limit : undefined).join("\n");
|
||||||
const end = args.limit ? start + args.limit : lines.length;
|
|
||||||
content = lines.slice(start, end).join("\n");
|
|
||||||
}
|
}
|
||||||
return { path: args.path, content, size: fs.statSync(safePath(args.path).size };
|
return { path: filePath, content, size: fs.statSync(filePath).size };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-004: file_write 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-004: file_write ───────────────────────────────────────
|
||||||
export async function file_write(args: {
|
export async function file_write(args: { path: string; content: string; encoding?: string }): Promise<any> {
|
||||||
path: string;
|
const filePath = safePath(args.path);
|
||||||
content: string;
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
encoding?: string;
|
fs.writeFileSync(filePath, args.content, (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding);
|
||||||
}): Promise<any> {
|
return { ok: true, path: filePath, size: fs.statSync(filePath).size };
|
||||||
const encoding = args.encoding === "base64" ? "base64" : "utf8";
|
|
||||||
fs.mkdirSync(path.dirname(args.path), { recursive: true });
|
|
||||||
fs.writeFileSync(safePath(args.path, args.content, encoding as BufferEncoding);
|
|
||||||
return { ok: true, path: args.path, size: fs.statSync(safePath(args.path).size };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-005: file_move 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-005: file_move ────────────────────────────────────────
|
||||||
export async function file_move(args: {
|
export async function file_move(args: { from: string; to: string }): Promise<any> {
|
||||||
from: string;
|
const fromPath = safePath(args.from);
|
||||||
to: string;
|
const toPath = safePath(args.to);
|
||||||
}): Promise<any> {
|
fs.mkdirSync(path.dirname(toPath), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(safePath(args.to)), { recursive: true });
|
fs.renameSync(fromPath, toPath);
|
||||||
fs.renameSync(safePath(args.from, args.to);
|
return { ok: true, from: fromPath, to: toPath };
|
||||||
return { ok: true, from: args.from, to: args.to };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-008: exec 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-008: exec ─────────────────────────────────────────────
|
||||||
function sanitizeCommand(cmd: string): string {
|
function sanitizeCommand(cmd: string): string {
|
||||||
if (/[;&|`$(){}[\]<>!\\\n\r]/.test(cmd)) throw new Error(`Dangerous chars in: ${cmd.slice(0,50)}`);
|
// Allow: alphanumeric, dash, slash, dot, colon, underscore, space, equals, comma
|
||||||
|
if (/[^a-zA-Z0-9_./\-: =,]/.test(cmd)) throw new Error(`Dangerous chars: ${cmd.slice(0,50)}`);
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exec(args: { command: string; cwd?: string; timeout?: number }): Promise<any> {
|
export async function exec(args: { command: string; cwd?: string; timeout?: number }): Promise<any> {
|
||||||
const command = sanitizeCommand(args.command);
|
const command = sanitizeCommand(args.command);
|
||||||
const timeout = args.timeout ?? 30000;
|
const cwd = args.cwd ? safePath(args.cwd) : process.cwd();
|
||||||
try {
|
try {
|
||||||
const stdout = execSync(command, {
|
const stdout = execSync(command, { cwd, timeout: args.timeout ?? 30000, encoding: "utf8", maxBuffer: 10*1024*1024, stdio: ["pipe","pipe","pipe"] });
|
||||||
cwd: args.cwd ?? process.cwd(),
|
|
||||||
timeout,
|
|
||||||
encoding: "utf8",
|
|
||||||
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
return { ok: true, stdout: stdout.toString(), stderr: "" };
|
return { ok: true, stdout: stdout.toString(), stderr: "" };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return {
|
return { ok: false, stdout: err.stdout?.toString() ?? "", stderr: err.stderr?.toString() ?? "", exitCode: err.status ?? -1, signal: err.signal };
|
||||||
ok: false,
|
|
||||||
stdout: err.stdout?.toString() ?? "",
|
|
||||||
stderr: err.stderr?.toString() ?? "",
|
|
||||||
exitCode: err.status ?? -1,
|
|
||||||
signal: err.signal,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-010: process_list 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-010: process_list ─────────────────────────────────────
|
||||||
export async function process_list(args: {
|
export async function process_list(args: { filter?: string }): Promise<any> {
|
||||||
filter?: string;
|
|
||||||
}): Promise<any> {
|
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
try {
|
try {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@@ -113,10 +79,7 @@ export async function process_list(args: {
|
|||||||
} else {
|
} else {
|
||||||
stdout = execSync("ps aux --no-headers", { encoding: "utf8" }).toString();
|
stdout = execSync("ps aux --no-headers", { encoding: "utf8" }).toString();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { return { error: "Failed to list processes" }; }
|
||||||
return { error: "Failed to list processes" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const processes = stdout.trim().split("\n").map(line => {
|
const processes = stdout.trim().split("\n").map(line => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
const [name, pid, , mem] = line.replace(/"/g, "").split(",");
|
const [name, pid, , mem] = line.replace(/"/g, "").split(",");
|
||||||
@@ -125,34 +88,12 @@ export async function process_list(args: {
|
|||||||
const parts = line.trim().split(/\s+/);
|
const parts = line.trim().split(/\s+/);
|
||||||
return { user: parts[0], pid: Number(parts[1]), cpu: parts[2], mem: parts[3], name: parts[10] };
|
return { user: parts[0], pid: Number(parts[1]), cpu: parts[2], mem: parts[3], name: parts[10] };
|
||||||
});
|
});
|
||||||
|
const filtered = args.filter ? processes.filter(p => p.name?.toLowerCase().includes(args.filter!.toLowerCase())) : processes.slice(0, 50);
|
||||||
const filtered = args.filter
|
|
||||||
? processes.filter(p => p.name?.toLowerCase().includes(args.filter!.toLowerCase()))
|
|
||||||
: processes.slice(0, 50);
|
|
||||||
|
|
||||||
return { count: filtered.length, processes: filtered };
|
return { count: filtered.length, processes: filtered };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鈹€鈹€ C-011: sysinfo 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
// ── C-011: sysinfo ──────────────────────────────────────────
|
||||||
export async function sysinfo(_args: {}): Promise<any> {
|
export async function sysinfo(_args: {}): Promise<any> {
|
||||||
const cpuUsage = os.loadavg();
|
const tm = os.totalmem(); const fm = os.freemem();
|
||||||
const totalMem = os.totalmem();
|
return { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), cpus: os.cpus().length, loadavg: os.loadavg(), memory: { total: tm, free: fm, used: tm - fm, percent: Math.round(((tm-fm)/tm)*100) }, uptime: os.uptime(), node: process.version };
|
||||||
const freeMem = os.freemem();
|
|
||||||
const uptime = os.uptime();
|
|
||||||
|
|
||||||
return {
|
|
||||||
hostname: os.hostname(),
|
|
||||||
platform: os.platform(),
|
|
||||||
arch: os.arch(),
|
|
||||||
cpus: os.cpus().length,
|
|
||||||
loadavg: cpuUsage,
|
|
||||||
memory: {
|
|
||||||
total: totalMem,
|
|
||||||
free: freeMem,
|
|
||||||
used: totalMem - freeMem,
|
|
||||||
percent: Math.round(((totalMem - freeMem) / totalMem) * 100),
|
|
||||||
},
|
|
||||||
uptime,
|
|
||||||
node: process.version,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user