fix(cli): confine agent runtime paths

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 16:12:08 -04:00
1 parent c78df90361
commit 856ae5313e
5 files changed
+78 -9

No files matched your search

+9 -8
View File
@@ -4,7 +4,11 @@ import path from 'node:path';
import os from 'node:os';
import { resolveInstallSecrets } from './cli-secrets.js';
import { derivePublicKey } from './crypto.js';
import { ensurePrivateDirectory, writePrivateFile } from './secure-files.js';
import {
ensurePrivateDirectory,
resolvePrivateChildDirectory,
writePrivateFile,
} from './secure-files.js';
import { SUPPORTED_AGENTS } from './plugins/registry.js';
import type { AgentPlugin, InstallConfig } from './plugins/base.js';
import { generateHookScript, generateGuardScript } from './plugins/hook-template.js';
@@ -102,6 +106,7 @@ async function cmdInstall(args: string[]): Promise<void> {
const agentId = values.agent_id;
if (!agentId) die('--agent_id is required');
const agentDir = resolvePrivateChildDirectory(ELYDORA_DIR, agentId);
const kid = values.kid;
if (!kid) die('--kid is required');
@@ -123,7 +128,6 @@ async function cmdInstall(args: string[]): Promise<void> {
console.log(`Verifying private key... Public key: ${publicKey.slice(0, 12)}...`);
// Create ~/.elydora/{agentId}/ directory
const agentDir = path.join(ELYDORA_DIR, agentId);
await ensurePrivateDirectory(ELYDORA_DIR);
await ensurePrivateDirectory(agentDir);
@@ -194,6 +198,7 @@ async function cmdUninstall(args: string[]): Promise<void> {
}
let agentId = values.agent_id;
if (agentId) resolvePrivateChildDirectory(ELYDORA_DIR, agentId);
// If --agent_id not provided, scan ~/.elydora/*/config.json for matching agent_name
if (!agentId) {
@@ -228,12 +233,8 @@ async function cmdUninstall(args: string[]): Promise<void> {
await plugin.uninstall(agentId);
// Remove entire agent directory
const agentDir = path.join(ELYDORA_DIR, agentId);
try {
await fsp.rm(agentDir, { recursive: true, force: true });
} catch {
// Already removed
}
const agentDir = resolvePrivateChildDirectory(ELYDORA_DIR, agentId);
await fsp.rm(agentDir, { recursive: true, force: true });
console.log(`Elydora audit hook uninstalled for ${registryEntry.name}.`);
}
+35
View File
@@ -8,11 +8,46 @@ function isMissingFile(error: unknown): boolean {
export async function ensurePrivateDirectory(directory: string): Promise<void> {
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
const metadata = await fsp.lstat(directory);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error(`Private directory path is not a physical directory: ${directory}`);
}
if (process.platform !== 'win32') {
await fsp.chmod(directory, 0o700);
}
}
export function resolvePrivateChildDirectory(root: string, childName: string): string {
const isWindowsDeviceName = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(childName);
if (
childName.length === 0
|| /[<>:"|?*\u0000-\u001f]/.test(childName)
|| childName.includes('/')
|| childName.includes('\\')
|| childName === '.'
|| childName === '..'
|| childName.endsWith('.')
|| childName.endsWith(' ')
|| isWindowsDeviceName
) {
throw new Error(`Invalid agent ID for local storage: ${JSON.stringify(childName)}`);
}
const resolvedRoot = path.resolve(root);
const candidate = path.resolve(resolvedRoot, childName);
const relative = path.relative(resolvedRoot, candidate);
if (
relative.length === 0
|| path.isAbsolute(relative)
|| relative === '..'
|| relative.startsWith(`..${path.sep}`)
|| relative.includes(path.sep)
) {
throw new Error(`Agent ID escapes the local storage directory: ${JSON.stringify(childName)}`);
}
return candidate;
}
export async function writePrivateFile(filePath: string, contents: string): Promise<void> {
const directory = path.dirname(filePath);
await ensurePrivateDirectory(directory);