mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { readdirSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import path from "path";
|
||||
import { getAdditionalAllowedRoots, normalizeSlashes } from "./allowed-roots";
|
||||
import { isExistingPathWithinRoots } from "./path-security";
|
||||
import { listAllSessions } from "./session-reader";
|
||||
export { allowFileRoot, normalizeSlashes } from "./allowed-roots";
|
||||
|
||||
// Short-TTL cache for the allowed-roots set. Without this, every file list/read
|
||||
// request re-scans every pi session on disk just to check access. 5s is short
|
||||
// enough that newly-created cwds appear promptly; stored on globalThis so it
|
||||
// survives Next.js hot-reload.
|
||||
declare global {
|
||||
var __piAllowedRootsCache: { roots: Set<string>; expiresAt: number } | undefined;
|
||||
}
|
||||
|
||||
const ALLOWED_ROOTS_TTL_MS = 5_000;
|
||||
const WINDOWS_ABSOLUTE_RE = /^[a-zA-Z]:[\\/]/;
|
||||
|
||||
export function isWindowsAbsolutePath(filePath: string): boolean {
|
||||
return WINDOWS_ABSOLUTE_RE.test(filePath) || filePath.startsWith("\\\\") || filePath.startsWith("//");
|
||||
}
|
||||
|
||||
export async function getAllowedFileRoots(): Promise<Set<string>> {
|
||||
const now = Date.now();
|
||||
const cached = globalThis.__piAllowedRootsCache;
|
||||
if (cached && cached.expiresAt > now) return cached.roots;
|
||||
|
||||
const sessions = await listAllSessions();
|
||||
const roots = new Set<string>();
|
||||
for (const s of sessions) {
|
||||
if (s.cwd) roots.add(normalizeSlashes(s.cwd));
|
||||
// The project root (main repo shared by all worktrees) is browsable too —
|
||||
// the project dropdown lists it even when only worktrees have sessions.
|
||||
if (s.projectRoot) roots.add(normalizeSlashes(s.projectRoot));
|
||||
}
|
||||
|
||||
// Also allow ~/pi-cwd-* directories created by the default-cwd endpoint.
|
||||
try {
|
||||
for (const name of readdirSync(homedir())) {
|
||||
if (/^pi-cwd-\d{8}$/.test(name)) {
|
||||
roots.add(normalizeSlashes(path.join(homedir(), name)));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore if home is unreadable
|
||||
}
|
||||
|
||||
for (const root of getAdditionalAllowedRoots()) roots.add(root);
|
||||
|
||||
globalThis.__piAllowedRootsCache = { roots, expiresAt: now + ALLOWED_ROOTS_TTL_MS };
|
||||
return roots;
|
||||
}
|
||||
|
||||
export function isFilePathAllowed(target: string, allowedRoots: Set<string>): boolean {
|
||||
for (const root of allowedRoots) {
|
||||
const useWindowsRules = isWindowsAbsolutePath(target) || isWindowsAbsolutePath(root);
|
||||
const resolver = useWindowsRules ? path.win32 : path;
|
||||
const sep = useWindowsRules ? "\\" : path.sep;
|
||||
const normalized = resolver.resolve(target);
|
||||
const normalizedRoot = resolver.resolve(root);
|
||||
const comparable = useWindowsRules ? normalized.toLowerCase() : normalized;
|
||||
const comparableRoot = useWindowsRules ? normalizedRoot.toLowerCase() : normalizedRoot;
|
||||
const rootWithSep = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
|
||||
if (comparable === comparableRoot || comparable.startsWith(rootWithSep)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Authorize an existing path after resolving symbolic links. */
|
||||
export function isExistingFilePathAllowed(target: string, allowedRoots: Set<string>): boolean {
|
||||
return isExistingPathWithinRoots(target, allowedRoots);
|
||||
}
|
||||
Reference in New Issue
Block a user