/** * WorkspacePathPolicy — blocks external absolute paths and Windows path issues. * * Checkpoints: pre-tool, pre-write * * Logic migrated from: * - src/platform/path.ts: workspacePathBlockReason, windowsPathHint, isExternalAbsolutePath * - src/harness/tool-gateway.ts: external write/read blocking, windows path blocking */ import type { GateContext, GateFinding } from "../findings.ts"; export const WORKSPACE_PATH_POLICY_NAME = "workspace-path-policy"; const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/; const HOME_PATH_RE = /^~(?:[\\/]|$)/; export function evaluateWorkspacePathPolicy(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; if (ctx.checkpoint === "pre-tool") { findings.push(...evaluatePreTool(ctx)); } else if (ctx.checkpoint === "pre-write") { findings.push(...evaluatePreWrite(ctx)); } return findings; } function evaluatePreTool(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; const { path, toolName, cwd } = ctx; if (!path || !toolName) return findings; const isWriteTool = isWriteLikeTool(toolName); const isReadTool = isReadLikeTool(toolName); // Block external writes if (isWriteTool && isExternalAbsolutePath(path) && !isPathInsideWorkspace(cwd, path)) { findings.push({ id: "WSP-001", severity: "hard-deny", policy: WORKSPACE_PATH_POLICY_NAME, message: `拒绝写入工作区外路径 ${path}。`, nextAction: "先确认当前业务项目根目录;生产源码必须使用当前项目相对路径,并受 PLAN.md 批准文件、execute 阶段和证据门禁约束。禁止直接写入 C:/Users、~\\Desktop、Desktop 或其他非当前工作区路径。", }); } // Block external reads if (isReadTool && isExternalAbsolutePath(path)) { const blockReason = workspacePathBlockReason(cwd, path, "读取"); if (blockReason) { findings.push({ id: "WSP-002", severity: "hard-deny", policy: WORKSPACE_PATH_POLICY_NAME, message: blockReason, nextAction: "改用当前业务项目内的相对路径;确需处理外部文件时,先复制到当前工作区。", }); } } // Block Windows path issues if (isWriteTool || isReadTool) { const hint = windowsPathHint(path); if (hint) { findings.push({ id: "WSP-003", severity: "warning", policy: WORKSPACE_PATH_POLICY_NAME, message: `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。`, nextAction: `改用项目相对路径;必须使用绝对路径时,使用 Windows 路径 ${hint}。`, }); } } return findings; } function evaluatePreWrite(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; const { path, cwd } = ctx; if (!path) return findings; // Block external path writes if (isExternalAbsolutePath(path) && !isPathInsideWorkspace(cwd, path)) { findings.push({ id: "WSP-001", severity: "hard-deny", policy: WORKSPACE_PATH_POLICY_NAME, message: `拒绝写入工作区外路径 ${path}。`, nextAction: "先确认当前业务项目根目录;生产源码必须使用当前项目相对路径。禁止直接写入 C:/Users、~\\Desktop、Desktop 或其他非当前工作区路径。", }); } // Block Windows path issues const hint = windowsPathHint(path); if (hint) { findings.push({ id: "WSP-003", severity: "warning", policy: WORKSPACE_PATH_POLICY_NAME, message: `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。`, nextAction: `改用项目相对路径;必须使用绝对路径时,使用 Windows 路径 ${hint}。`, }); } return findings; } // ── Utility functions (reimplemented from src/platform/path.ts) ────────────── function isExternalAbsolutePath(inputPath: string): boolean { const normalized = normalizeExternalPath(inputPath); return HOME_PATH_RE.test(inputPath.trim()) || isAbsolute(normalized) || WINDOWS_DRIVE_RE.test(normalized); } function isPathInsideWorkspace(cwd: string, inputPath: string): boolean { const workspace = resolve(cwd); const target = resolveWorkspacePath(cwd, inputPath); const rel = relative(workspace, target); return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); } function workspacePathBlockReason(cwd: string, inputPath: string, action: string): string | undefined { if (isPathInsideWorkspace(cwd, inputPath)) return undefined; return `拒绝${action}工作区外路径 ${inputPath}。执行指令:改用当前业务项目内的相对路径;确需处理外部文件时,先复制到当前工作区并确认不会泄露凭证、Token、连接串或个人文件。`; } function windowsPathHint(inputPath: string): string | undefined { if (process.platform !== "win32" || !hasUnixDrivePath(inputPath)) return undefined; return normalizeExternalPath(inputPath); } function hasUnixDrivePath(inputPath: string): boolean { return /^\/mnt\/[a-zA-Z]\//.test(inputPath.trim()) || /^\/[a-zA-Z]\//.test(inputPath.trim()); } function normalizeExternalPath(inputPath: string): string { let value = inputPath.trim(); if (HOME_PATH_RE.test(value)) { const rest = value.slice(1).replace(/^[\\/]/, ""); value = rest ? join(homedir(), rest) : homedir(); } if (value.startsWith("file://")) { try { value = new URL(value).pathname; } catch { // Keep original value when it is not a valid file URL. } } if (process.platform === "win32") { const wsl = value.match(/^\/mnt\/([a-zA-Z])\/(.*)$/); if (wsl) return `${wsl[1].toUpperCase()}:\\${wsl[2].replace(/\//g, "\\")}`; const msys = value.match(/^\/([a-zA-Z])\/(.*)$/); if (msys) return `${msys[1].toUpperCase()}:\\${msys[2].replace(/\//g, "\\")}`; } return value; } function resolveWorkspacePath(cwd: string, inputPath: string): string { const normalized = normalizeExternalPath(inputPath); return isAbsolute(normalized) || WINDOWS_DRIVE_RE.test(normalized) ? resolve(normalized) : resolve(cwd, normalized); } function isWriteLikeTool(toolName: string): boolean { return toolName === "write" || toolName === "edit"; } function isReadLikeTool(toolName: string): boolean { return toolName === "read" || toolName === "kd_doc_read" || toolName === "kd_anchor_read"; } // Import from node:path and node:os import { isAbsolute, join, relative, resolve } from "node:path"; import { homedir } from "node:os";