mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
* fix(desktop): Harden local file tree paths Normalize Electron local path handling across file tree, preview, media, and git-root flows. Reject malformed and Windows device paths, recheck sensitive files after realpath resolution, and preserve external symlink traversal with stable renderer errors. * fix(desktop): Address file tree review feedback
109 lines
2.7 KiB
JavaScript
109 lines
2.7 KiB
JavaScript
'use strict'
|
|
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const { resolveDirectoryForIpc } = require('./hardening.cjs')
|
|
|
|
const FS_READDIR_STAT_CONCURRENCY = 16
|
|
|
|
// Always-hidden noise (covers non-git projects too; gitignore catches many of
|
|
// these, but the project tree should keep the same hygiene without one).
|
|
const FS_READDIR_HIDDEN = new Set([
|
|
'.git',
|
|
'.hg',
|
|
'.svn',
|
|
'.cache',
|
|
'.next',
|
|
'.turbo',
|
|
'.venv',
|
|
'__pycache__',
|
|
'build',
|
|
'dist',
|
|
'node_modules',
|
|
'target',
|
|
'venv'
|
|
])
|
|
|
|
function direntIsDirectory(dirent) {
|
|
return typeof dirent.isDirectory === 'function' && dirent.isDirectory()
|
|
}
|
|
|
|
function direntIsFile(dirent) {
|
|
return typeof dirent.isFile === 'function' && dirent.isFile()
|
|
}
|
|
|
|
function direntIsSymbolicLink(dirent) {
|
|
return typeof dirent.isSymbolicLink === 'function' && dirent.isSymbolicLink()
|
|
}
|
|
|
|
function shouldStatDirent(dirent) {
|
|
if (direntIsDirectory(dirent)) return false
|
|
|
|
return direntIsSymbolicLink(dirent) || !direntIsFile(dirent)
|
|
}
|
|
|
|
async function entryForDirent(dirent, resolved, fsImpl) {
|
|
const fullPath = path.join(resolved, dirent.name)
|
|
let isDirectory = direntIsDirectory(dirent)
|
|
|
|
if (!isDirectory && shouldStatDirent(dirent)) {
|
|
try {
|
|
isDirectory = (await fsImpl.promises.stat(fullPath)).isDirectory()
|
|
} catch {
|
|
isDirectory = false
|
|
}
|
|
}
|
|
|
|
return { name: dirent.name, path: fullPath, isDirectory }
|
|
}
|
|
|
|
async function mapWithStatConcurrency(items, mapper) {
|
|
const results = new Array(items.length)
|
|
let nextIndex = 0
|
|
|
|
async function runWorker() {
|
|
while (nextIndex < items.length) {
|
|
const index = nextIndex
|
|
nextIndex += 1
|
|
results[index] = await mapper(items[index])
|
|
}
|
|
}
|
|
|
|
const workerCount = Math.min(FS_READDIR_STAT_CONCURRENCY, items.length)
|
|
const workers = Array.from({ length: workerCount }, () => runWorker())
|
|
await Promise.all(workers)
|
|
|
|
return results
|
|
}
|
|
|
|
async function readDirForIpc(dirPath, options = {}) {
|
|
const fsImpl = options.fs || fs
|
|
let resolved
|
|
|
|
try {
|
|
;({ resolvedPath: resolved } = await resolveDirectoryForIpc(dirPath, {
|
|
fs: fsImpl,
|
|
purpose: 'Directory read'
|
|
}))
|
|
} catch (error) {
|
|
return { entries: [], error: error?.code || 'read-error' }
|
|
}
|
|
|
|
try {
|
|
const dirents = await fsImpl.promises.readdir(resolved, { withFileTypes: true })
|
|
const visibleDirents = dirents.filter(dirent => !FS_READDIR_HIDDEN.has(dirent.name))
|
|
const entries = await mapWithStatConcurrency(visibleDirents, dirent =>
|
|
entryForDirent(dirent, resolved, fsImpl)
|
|
)
|
|
|
|
entries.sort((a, b) => Number(b.isDirectory) - Number(a.isDirectory) || a.name.localeCompare(b.name))
|
|
|
|
return { entries }
|
|
} catch (error) {
|
|
return { entries: [], error: error?.code || 'read-error' }
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
readDirForIpc
|
|
}
|