89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
import path from "path"
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
import type { Info as SandboxInfo } from "@/config/sandbox"
|
|
|
|
export function isWithinRoot(candidate: string, root: string): boolean {
|
|
const resolvedCandidate = path.resolve(candidate)
|
|
const resolvedRoot = path.resolve(root)
|
|
const rel = path.relative(resolvedRoot, resolvedCandidate)
|
|
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
|
|
}
|
|
|
|
export function normalizeSandboxPath(filepath: string): string {
|
|
const resolved = path.resolve(filepath)
|
|
return process.platform === "win32" ? AppFileSystem.normalizePath(resolved) : resolved
|
|
}
|
|
|
|
export function collectTempDirs(): string[] {
|
|
const dirs: string[] = []
|
|
for (const key of ["TMP", "TEMP", "TMPDIR"] as const) {
|
|
const value = process.env[key]
|
|
if (value) dirs.push(path.resolve(value))
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
export function collectCompatAppDataDirs(): string[] {
|
|
if (process.platform !== "win32") return []
|
|
const dirs: string[] = []
|
|
for (const key of ["APPDATA", "LOCALAPPDATA"] as const) {
|
|
const value = process.env[key]
|
|
if (value) dirs.push(path.resolve(value))
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
/** Pick the most specific (deepest) directory when one path contains the other. */
|
|
export function pickNarrowestDirectory(...candidates: string[]): string {
|
|
const resolved = [...new Set(candidates.filter(Boolean).map((p) => path.resolve(p)))]
|
|
if (resolved.length === 0) return path.resolve(".")
|
|
let narrowest = resolved[0]!
|
|
for (const candidate of resolved.slice(1)) {
|
|
if (isWithinRoot(candidate, narrowest)) {
|
|
narrowest = candidate
|
|
} else if (!isWithinRoot(narrowest, candidate)) {
|
|
// Unrelated paths — keep the first candidate (HTTP ?directory= wins over stale DB).
|
|
continue
|
|
}
|
|
}
|
|
return narrowest
|
|
}
|
|
|
|
export function resolveWritableRoots(sandbox: SandboxInfo, instanceDirectory: string): string[] {
|
|
const mode = sandbox.sandbox_mode
|
|
if (mode === "permissive") return []
|
|
|
|
const roots = new Set<string>()
|
|
// Strict: only the session/instance directory, never config writable_roots or broad temp.
|
|
if (mode === "strict") {
|
|
roots.add(path.resolve(instanceDirectory))
|
|
} else {
|
|
const configured = sandbox.writable_roots?.map((p) => path.resolve(p)) ?? []
|
|
if (configured.length > 0) {
|
|
for (const root of configured) roots.add(root)
|
|
} else {
|
|
roots.add(path.resolve(instanceDirectory))
|
|
}
|
|
}
|
|
|
|
if (mode !== "strict") {
|
|
for (const temp of collectTempDirs()) {
|
|
roots.add(temp)
|
|
}
|
|
}
|
|
|
|
if (mode === "compat") {
|
|
for (const dir of collectCompatAppDataDirs()) {
|
|
roots.add(dir)
|
|
}
|
|
}
|
|
|
|
return [...roots]
|
|
}
|
|
|
|
export function isPathWritable(targetPath: string, roots: string[]): boolean {
|
|
if (roots.length === 0) return true
|
|
const normalized = normalizeSandboxPath(targetPath)
|
|
return roots.some((root) => isWithinRoot(normalized, root))
|
|
}
|