import * as plugins from './plugins.js'; export const aglHomeEnvironmentVariable = 'AGL_HOME'; export interface IAGLHomeOptions { packageRoot?: string; homeDirectory?: string; environment?: NodeJS.ProcessEnv; /** per-user runtime directory; defaults to the platform's `/run/user/` */ userRuntimeDirectory?: string; } export interface IAGLHomePaths { development: boolean; root: string; database: string; credentials: string; gitReversion: string; logs: string; legacyLogs: string; upgrade: string; cache: string; runtime: string; mcpRuntime: string; browserRuntime: string; uploads: string; sockets: string; openCodeRuntime: string; migration: string; } const modulePackageRoot = plugins.path.resolve( plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)), '..', ); const isDevelopmentCheckout = (packageRootArg: string): boolean => { try { const stats = plugins.fs.lstatSync(plugins.path.join(packageRootArg, '.git')); return stats.isDirectory() || stats.isFile(); } catch { return false; } }; const assertAbsoluteNormalizedPath = (pathArg: string, labelArg: string): string => { if ( typeof pathArg !== 'string' || !plugins.path.isAbsolute(pathArg) || plugins.path.normalize(pathArg) !== pathArg || plugins.path.parse(pathArg).root === pathArg || pathArg.includes('\0') || Buffer.byteLength(pathArg, 'utf8') > 4096 ) { throw new Error(`${labelArg} must be an absolute normalized non-root path.`); } return pathArg; }; const defaultConfigHome = ( environmentArg: NodeJS.ProcessEnv, homeDirectoryArg: string, ): string => { const configured = environmentArg.XDG_CONFIG_HOME?.trim(); return configured && plugins.path.isAbsolute(configured) ? plugins.path.normalize(configured) : plugins.path.join(homeDirectoryArg, '.config'); }; const platformUserRuntimeDirectory = (): string | undefined => { const userId = process.getuid?.(); return userId === undefined ? undefined : plugins.path.join('/run/user', String(userId)); }; const isPrivateOwnedDirectory = (directoryArg: string): boolean => { try { const stats = plugins.fs.lstatSync(directoryArg); return stats.isDirectory() && stats.uid === process.getuid?.() && (stats.mode & 0o077) === 0; } catch { return false; } }; /** * Unix socket paths are capped near 100 bytes. A development checkout keeps * its home under the package root, so deriving sockets from the home would let * the checkout's depth decide whether the embedded database can start. The * per-user runtime directory is short, private to the user and cleared at * logout. It is taken from the platform convention rather than from * XDG_RUNTIME_DIR: every process of a checkout must resolve the same socket * path however it was started, and a shell without a login session carries no * such variable. Installed controllers keep their sockets under the home: a * second process attaches to the running engine by that published path. */ const resolveSocketsDirectory = ( developmentArg: boolean, rootArg: string, runtimeArg: string, userRuntimeArg: string | undefined, ): string => { const homeSockets = plugins.path.join(runtimeArg, 'sockets'); if (!developmentArg || !userRuntimeArg || !plugins.path.isAbsolute(userRuntimeArg)) { return homeSockets; } const userRuntime = plugins.path.normalize(userRuntimeArg); if (!isPrivateOwnedDirectory(userRuntime)) return homeSockets; const rootHash = plugins.crypto .createHash('sha256') .update(rootArg) .digest('hex') .slice(0, 16); return plugins.path.join(userRuntime, `agl-${rootHash}`); }; export const resolveAGLHomePaths = ( optionsArg: IAGLHomeOptions = {}, ): IAGLHomePaths => { const environment = optionsArg.environment ?? process.env; const packageRoot = assertAbsoluteNormalizedPath( plugins.path.normalize(optionsArg.packageRoot ?? modulePackageRoot), 'AGL package root', ); const homeDirectory = assertAbsoluteNormalizedPath( plugins.path.normalize(optionsArg.homeDirectory ?? plugins.os.homedir()), 'User home directory', ); const configuredHome = environment[aglHomeEnvironmentVariable]?.trim(); const development = isDevelopmentCheckout(packageRoot); const root = assertAbsoluteNormalizedPath( configuredHome ? configuredHome : development ? plugins.path.join(packageRoot, '.nogit', 'agl') : plugins.path.join(defaultConfigHome(environment, homeDirectory), 'agl'), aglHomeEnvironmentVariable, ); const runtime = plugins.path.join(root, 'runtime'); const logs = plugins.path.join(root, 'logs'); return { development, root, database: plugins.path.join(root, 'database'), credentials: plugins.path.join(root, 'credentials'), gitReversion: plugins.path.join(root, 'git-reversion'), logs, legacyLogs: plugins.path.join(logs, 'legacy'), upgrade: plugins.path.join(root, 'upgrade'), cache: plugins.path.join(root, 'cache'), runtime, mcpRuntime: plugins.path.join(runtime, 'mcp'), browserRuntime: plugins.path.join(runtime, 'browser'), uploads: plugins.path.join(runtime, 'uploads'), sockets: resolveSocketsDirectory( development, root, runtime, optionsArg.userRuntimeDirectory ?? platformUserRuntimeDirectory(), ), openCodeRuntime: plugins.path.join(runtime, 'opencode'), migration: plugins.path.join(root, 'migration'), }; }; export const bindAGLHomeEnvironment = ( environmentArg: NodeJS.ProcessEnv, pathsArg: IAGLHomePaths = resolveAGLHomePaths({ environment: environmentArg }), ): NodeJS.ProcessEnv => ({ ...environmentArg, [aglHomeEnvironmentVariable]: pathsArg.root, });