111 lines
2.7 KiB
JavaScript
111 lines
2.7 KiB
JavaScript
/**
|
|
* Local storage for the standalone harness (sessions, models, vision stills).
|
|
* Override with AGENT_HARNESS_HOME.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
function getHome() {
|
|
return (
|
|
process.env.AGENT_HARNESS_HOME ||
|
|
path.join(os.homedir(), '.agent-harness')
|
|
);
|
|
}
|
|
|
|
function getStorageRoot() {
|
|
return getHome();
|
|
}
|
|
|
|
function getQvacRoot() {
|
|
return path.join(getStorageRoot(), 'qvac');
|
|
}
|
|
|
|
function getAgentRoot() {
|
|
return path.join(getStorageRoot(), 'agent');
|
|
}
|
|
|
|
function ensureDir(dir) {
|
|
try {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
} catch (_) {}
|
|
return dir;
|
|
}
|
|
|
|
function ensureQvacRoot() {
|
|
return ensureDir(getQvacRoot());
|
|
}
|
|
|
|
function ensureAgentRoot() {
|
|
return ensureDir(getAgentRoot());
|
|
}
|
|
|
|
function sanitizeId(id) {
|
|
return String(id)
|
|
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
.slice(0, 128);
|
|
}
|
|
|
|
function originHash(origin) {
|
|
const s = String(origin || 'local');
|
|
let h = 5381;
|
|
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
|
|
return Math.abs(h).toString(16);
|
|
}
|
|
|
|
function getAgentOriginRoot(origin) {
|
|
return path.join(ensureAgentRoot(), 'workspaces', sanitizeId(originHash(origin) || 'default'));
|
|
}
|
|
|
|
function isPathInside(root, candidate) {
|
|
const absRoot = path.resolve(root);
|
|
const abs = path.resolve(candidate);
|
|
const rel = path.relative(absRoot, abs);
|
|
return !(rel.startsWith('..') || path.isAbsolute(rel));
|
|
}
|
|
|
|
function resolveUnderRoot(root, userPath) {
|
|
if (!userPath || typeof userPath !== 'string') throw new Error('path is required');
|
|
const absRoot = path.resolve(ensureDir(root));
|
|
const resolved = path.isAbsolute(userPath)
|
|
? path.resolve(userPath)
|
|
: path.resolve(absRoot, userPath.replace(/^\/+/, ''));
|
|
const rel = path.relative(absRoot, resolved);
|
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
throw new Error('path escapes allowlisted root');
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
function resolveUnderAnyRoot(roots, userPath, fallbackRoot) {
|
|
if (!userPath || typeof userPath !== 'string') throw new Error('path is required');
|
|
const list = (roots || []).filter(Boolean).map((r) => path.resolve(r));
|
|
if (fallbackRoot) list.unshift(path.resolve(fallbackRoot));
|
|
if (!list.length) throw new Error('no allowlisted roots');
|
|
if (path.isAbsolute(userPath)) {
|
|
const resolved = path.resolve(userPath);
|
|
for (const root of list) {
|
|
if (isPathInside(root, resolved)) return resolved;
|
|
}
|
|
throw new Error('path escapes allowlisted roots');
|
|
}
|
|
return resolveUnderRoot(list[0], userPath);
|
|
}
|
|
|
|
module.exports = {
|
|
getHome,
|
|
getStorageRoot,
|
|
getQvacRoot,
|
|
getAgentRoot,
|
|
getAgentOriginRoot,
|
|
originHash,
|
|
ensureDir,
|
|
ensureQvacRoot,
|
|
ensureAgentRoot,
|
|
sanitizeId,
|
|
isPathInside,
|
|
resolveUnderRoot,
|
|
resolveUnderAnyRoot,
|
|
};
|