diff --git a/packages/server/src/tools.ts b/packages/server/src/tools.ts index 8b7a6ed..bf95a91 100644 --- a/packages/server/src/tools.ts +++ b/packages/server/src/tools.ts @@ -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 path from "node:path"; import { execSync } from "node:child_process"; 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()); function safePath(p: string): string { - const resolved = path.resolve(BASE_DIR, p); - if (!resolved.startsWith(BASE_DIR)) throw new Error(`Path traversal blocked: ${p}`); - return resolved; + const r = path.resolve(BASE_DIR, p); + if (!r.startsWith(BASE_DIR)) throw new Error(`Path traversal blocked: ${p}`); + return r; } -// 鈹€鈹€ C-002: file_list 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ -export async function file_list(args: { - path: string; - recursive?: boolean; -}): Promise { +// ── C-002: file_list ──────────────────────────────────────── +export async function file_list(args: { path: string; recursive?: boolean }): Promise { const dir = safePath(args.path || "."); const entries = fs.readdirSync(dir, { withFileTypes: true }); const result = entries.map(e => ({ @@ -34,78 +23,55 @@ export async function file_list(args: { return { path: dir, entries: result, count: result.length }; } -// 鈹€鈹€ C-003: file_read 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ -export async function file_read(args: { - path: string; - encoding?: string; - offset?: number; - limit?: number; -}): Promise { - const encoding = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding; - let content = fs.readFileSync(safePath(args.path, encoding); - if (encoding === "utf8") { +// ── C-003: file_read ──────────────────────────────────────── +export async function file_read(args: { path: string; encoding?: string; offset?: number; limit?: number }): Promise { + const filePath = safePath(args.path); + const enc = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding; + let content = fs.readFileSync(filePath, enc); + if (enc === "utf8") { const lines = (content as string).split("\n"); - const start = args.offset ?? 0; - const end = args.limit ? start + args.limit : lines.length; - content = lines.slice(start, end).join("\n"); + content = lines.slice(args.offset ?? 0, args.limit ? (args.offset ?? 0) + args.limit : undefined).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 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ -export async function file_write(args: { - path: string; - content: string; - encoding?: string; -}): Promise { - 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-004: file_write ─────────────────────────────────────── +export async function file_write(args: { path: string; content: string; encoding?: string }): Promise { + const filePath = safePath(args.path); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, args.content, (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding); + return { ok: true, path: filePath, size: fs.statSync(filePath).size }; } -// 鈹€鈹€ C-005: file_move 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ -export async function file_move(args: { - from: string; - to: string; -}): Promise { - fs.mkdirSync(path.dirname(safePath(args.to)), { recursive: true }); - fs.renameSync(safePath(args.from, args.to); - return { ok: true, from: args.from, to: args.to }; +// ── C-005: file_move ──────────────────────────────────────── +export async function file_move(args: { from: string; to: string }): Promise { + const fromPath = safePath(args.from); + const toPath = safePath(args.to); + fs.mkdirSync(path.dirname(toPath), { recursive: true }); + fs.renameSync(fromPath, toPath); + return { ok: true, from: fromPath, to: toPath }; } -// 鈹€鈹€ C-008: exec 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ +// ── C-008: exec ───────────────────────────────────────────── 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; } + export async function exec(args: { command: string; cwd?: string; timeout?: number }): Promise { const command = sanitizeCommand(args.command); - const timeout = args.timeout ?? 30000; + const cwd = args.cwd ? safePath(args.cwd) : process.cwd(); try { - const stdout = execSync(command, { - cwd: args.cwd ?? process.cwd(), - timeout, - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, // 10MB - stdio: ["pipe", "pipe", "pipe"], - }); + const stdout = execSync(command, { cwd, timeout: args.timeout ?? 30000, encoding: "utf8", maxBuffer: 10*1024*1024, stdio: ["pipe","pipe","pipe"] }); return { ok: true, stdout: stdout.toString(), stderr: "" }; } catch (err: any) { - return { - ok: false, - stdout: err.stdout?.toString() ?? "", - stderr: err.stderr?.toString() ?? "", - exitCode: err.status ?? -1, - signal: err.signal, - }; + return { ok: false, stdout: err.stdout?.toString() ?? "", stderr: err.stderr?.toString() ?? "", exitCode: err.status ?? -1, signal: err.signal }; } } -// 鈹€鈹€ C-010: process_list 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ -export async function process_list(args: { - filter?: string; -}): Promise { +// ── C-010: process_list ───────────────────────────────────── +export async function process_list(args: { filter?: string }): Promise { let stdout: string; try { if (process.platform === "win32") { @@ -113,10 +79,7 @@ export async function process_list(args: { } else { stdout = execSync("ps aux --no-headers", { encoding: "utf8" }).toString(); } - } catch { - return { error: "Failed to list processes" }; - } - + } catch { return { error: "Failed to list processes" }; } const processes = stdout.trim().split("\n").map(line => { if (process.platform === "win32") { const [name, pid, , mem] = line.replace(/"/g, "").split(","); @@ -125,34 +88,12 @@ export async function process_list(args: { const parts = line.trim().split(/\s+/); 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 }; } -// 鈹€鈹€ C-011: sysinfo 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ +// ── C-011: sysinfo ────────────────────────────────────────── export async function sysinfo(_args: {}): Promise { - const cpuUsage = os.loadavg(); - const totalMem = os.totalmem(); - 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, - }; + const tm = os.totalmem(); const fm = os.freemem(); + 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 }; }