mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
refactor: integrate secure file system operations across services
This commit replaces direct file system operations with a secure file system adapter to enhance security by enforcing path validation. The changes include:
- Replaced `fs` imports with `secureFs` in various services and utilities.
- Updated file operations in `agent-service`, `auto-mode-service`, `feature-loader`, and `settings-service` to use the secure file system methods.
- Ensured that all file I/O operations are validated against the ALLOWED_ROOT_DIRECTORY.
This refactor aims to prevent unauthorized file access and improve overall security posture.
Tests: All unit tests passing.
🤖 Generated with Claude Code
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { AbortError } from "@anthropic-ai/claude-agent-sdk";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import * as secureFs from "../lib/secure-fs.js";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||
import type { ExecuteOptions } from "../providers/types.js";
|
||||
@@ -63,7 +63,7 @@ export class AgentService {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await fs.mkdir(this.stateDir, { recursive: true });
|
||||
await secureFs.mkdir(this.stateDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +401,7 @@ export class AgentService {
|
||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(sessionFile, "utf-8");
|
||||
const data = await secureFs.readFile(sessionFile, "utf-8") as string;
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -412,7 +412,7 @@ export class AgentService {
|
||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
await secureFs.writeFile(
|
||||
sessionFile,
|
||||
JSON.stringify(messages, null, 2),
|
||||
"utf-8"
|
||||
@@ -425,7 +425,7 @@ export class AgentService {
|
||||
|
||||
async loadMetadata(): Promise<Record<string, SessionMetadata>> {
|
||||
try {
|
||||
const data = await fs.readFile(this.metadataFile, "utf-8");
|
||||
const data = await secureFs.readFile(this.metadataFile, "utf-8") as string;
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return {};
|
||||
@@ -433,7 +433,7 @@ export class AgentService {
|
||||
}
|
||||
|
||||
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
||||
await fs.writeFile(
|
||||
await secureFs.writeFile(
|
||||
this.metadataFile,
|
||||
JSON.stringify(metadata, null, 2),
|
||||
"utf-8"
|
||||
@@ -551,7 +551,7 @@ export class AgentService {
|
||||
// Delete session file
|
||||
try {
|
||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||
await fs.unlink(sessionFile);
|
||||
await secureFs.unlink(sessionFile);
|
||||
} catch {
|
||||
// File may not exist
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { ExecuteOptions } from "../providers/types.js";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import * as secureFs from "../lib/secure-fs.js";
|
||||
import type { EventEmitter } from "../lib/events.js";
|
||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
||||
@@ -698,7 +698,7 @@ export class AutoModeService {
|
||||
|
||||
let hasContext = false;
|
||||
try {
|
||||
await fs.access(contextPath);
|
||||
await secureFs.access(contextPath);
|
||||
hasContext = true;
|
||||
} catch {
|
||||
// No context
|
||||
@@ -706,7 +706,7 @@ export class AutoModeService {
|
||||
|
||||
if (hasContext) {
|
||||
// Load previous context and continue
|
||||
const context = await fs.readFile(contextPath, "utf-8");
|
||||
const context = await secureFs.readFile(contextPath, "utf-8") as string;
|
||||
return this.executeFeatureWithContext(
|
||||
projectPath,
|
||||
featureId,
|
||||
@@ -766,7 +766,7 @@ export class AutoModeService {
|
||||
const contextPath = path.join(featureDir, "agent-output.md");
|
||||
let previousContext = "";
|
||||
try {
|
||||
previousContext = await fs.readFile(contextPath, "utf-8");
|
||||
previousContext = await secureFs.readFile(contextPath, "utf-8") as string;
|
||||
} catch {
|
||||
// No previous context
|
||||
}
|
||||
@@ -832,7 +832,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
const featureDirForImages = getFeatureDir(projectPath, featureId);
|
||||
const featureImagesDir = path.join(featureDirForImages, "images");
|
||||
|
||||
await fs.mkdir(featureImagesDir, { recursive: true });
|
||||
await secureFs.mkdir(featureImagesDir, { recursive: true });
|
||||
|
||||
for (const imagePath of imagePaths) {
|
||||
try {
|
||||
@@ -841,7 +841,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
const destPath = path.join(featureImagesDir, filename);
|
||||
|
||||
// Copy the image
|
||||
await fs.copyFile(imagePath, destPath);
|
||||
await secureFs.copyFile(imagePath, destPath);
|
||||
|
||||
// Store the absolute path (external storage uses absolute paths)
|
||||
copiedImagePaths.push(destPath);
|
||||
@@ -883,7 +883,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
const featurePath = path.join(featureDirForSave, "feature.json");
|
||||
|
||||
try {
|
||||
await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
await secureFs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`[AutoMode] Failed to save feature.json:`, error);
|
||||
}
|
||||
@@ -949,7 +949,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
let workDir = projectPath;
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
await secureFs.access(worktreePath);
|
||||
workDir = worktreePath;
|
||||
} catch {
|
||||
// No worktree
|
||||
@@ -1018,7 +1018,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
// Use the provided worktree path if given
|
||||
if (providedWorktreePath) {
|
||||
try {
|
||||
await fs.access(providedWorktreePath);
|
||||
await secureFs.access(providedWorktreePath);
|
||||
workDir = providedWorktreePath;
|
||||
console.log(`[AutoMode] Committing in provided worktree: ${workDir}`);
|
||||
} catch {
|
||||
@@ -1034,7 +1034,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
featureId
|
||||
);
|
||||
try {
|
||||
await fs.access(legacyWorktreePath);
|
||||
await secureFs.access(legacyWorktreePath);
|
||||
workDir = legacyWorktreePath;
|
||||
console.log(`[AutoMode] Committing in legacy worktree: ${workDir}`);
|
||||
} catch {
|
||||
@@ -1097,7 +1097,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
const contextPath = path.join(featureDir, "agent-output.md");
|
||||
|
||||
try {
|
||||
await fs.access(contextPath);
|
||||
await secureFs.access(contextPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -1115,9 +1115,9 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
|
||||
try {
|
||||
// Check if directory exists first
|
||||
await fs.access(contextDir);
|
||||
await secureFs.access(contextDir);
|
||||
|
||||
const files = await fs.readdir(contextDir);
|
||||
const files = await secureFs.readdir(contextDir) as string[];
|
||||
// Filter for text-based context files (case-insensitive for Windows)
|
||||
const textFiles = files.filter((f) => {
|
||||
const lower = f.toLowerCase();
|
||||
@@ -1130,7 +1130,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
for (const file of textFiles) {
|
||||
// Use path.join for cross-platform path construction
|
||||
const filePath = path.join(contextDir, file);
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const content = await secureFs.readFile(filePath, "utf-8") as string;
|
||||
contents.push(`## ${file}\n\n${content}`);
|
||||
}
|
||||
|
||||
@@ -1229,8 +1229,8 @@ Format your response as a structured markdown document.`;
|
||||
// Save analysis to .automaker directory
|
||||
const automakerDir = getAutomakerDir(projectPath);
|
||||
const analysisPath = path.join(automakerDir, "project-analysis.md");
|
||||
await fs.mkdir(automakerDir, { recursive: true });
|
||||
await fs.writeFile(analysisPath, analysisResult);
|
||||
await secureFs.mkdir(automakerDir, { recursive: true });
|
||||
await secureFs.writeFile(analysisPath, analysisResult);
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_feature_complete", {
|
||||
featureId: analysisFeatureId,
|
||||
@@ -1498,7 +1498,7 @@ Format your response as a structured markdown document.`;
|
||||
const featurePath = path.join(featureDir, "feature.json");
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(featurePath, "utf-8");
|
||||
const data = await secureFs.readFile(featurePath, "utf-8") as string;
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -1515,7 +1515,7 @@ Format your response as a structured markdown document.`;
|
||||
const featurePath = path.join(featureDir, "feature.json");
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(featurePath, "utf-8");
|
||||
const data = await secureFs.readFile(featurePath, "utf-8") as string;
|
||||
const feature = JSON.parse(data);
|
||||
feature.status = status;
|
||||
feature.updatedAt = new Date().toISOString();
|
||||
@@ -1527,7 +1527,7 @@ Format your response as a structured markdown document.`;
|
||||
// Clear the timestamp when moving to other statuses
|
||||
feature.justFinishedAt = undefined;
|
||||
}
|
||||
await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
await secureFs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
} catch {
|
||||
// Feature file may not exist
|
||||
}
|
||||
@@ -1550,7 +1550,7 @@ Format your response as a structured markdown document.`;
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(featurePath, "utf-8");
|
||||
const data = await secureFs.readFile(featurePath, "utf-8") as string;
|
||||
const feature = JSON.parse(data);
|
||||
|
||||
// Initialize planSpec if it doesn't exist
|
||||
@@ -1571,7 +1571,7 @@ Format your response as a structured markdown document.`;
|
||||
}
|
||||
|
||||
feature.updatedAt = new Date().toISOString();
|
||||
await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
await secureFs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`[AutoMode] Failed to update planSpec for ${featureId}:`, error);
|
||||
}
|
||||
@@ -1582,7 +1582,7 @@ Format your response as a structured markdown document.`;
|
||||
const featuresDir = getFeaturesDir(projectPath);
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
||||
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
|
||||
const allFeatures: Feature[] = [];
|
||||
const pendingFeatures: Feature[] = [];
|
||||
|
||||
@@ -1595,7 +1595,7 @@ Format your response as a structured markdown document.`;
|
||||
"feature.json"
|
||||
);
|
||||
try {
|
||||
const data = await fs.readFile(featurePath, "utf-8");
|
||||
const data = await secureFs.readFile(featurePath, "utf-8") as string;
|
||||
const feature = JSON.parse(data);
|
||||
allFeatures.push(feature);
|
||||
|
||||
@@ -1799,7 +1799,7 @@ This helps parse your summary correctly in the output logs.`;
|
||||
|
||||
// Create a mock file with "yellow" content as requested in the test
|
||||
const mockFilePath = path.join(workDir, "yellow.txt");
|
||||
await fs.writeFile(mockFilePath, "yellow");
|
||||
await secureFs.writeFile(mockFilePath, "yellow");
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_progress", {
|
||||
featureId,
|
||||
@@ -1824,8 +1824,8 @@ This is a mock agent response for CI/CD testing.
|
||||
This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
`;
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, mockOutput);
|
||||
await secureFs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await secureFs.writeFile(outputPath, mockOutput);
|
||||
|
||||
console.log(
|
||||
`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`
|
||||
@@ -1901,8 +1901,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
// Helper to write current responseText to file
|
||||
const writeToFile = async (): Promise<void> => {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, responseText);
|
||||
await secureFs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await secureFs.writeFile(outputPath, responseText);
|
||||
} catch (error) {
|
||||
// Log but don't crash - file write errors shouldn't stop execution
|
||||
console.error(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import * as secureFs from "../lib/secure-fs.js";
|
||||
import {
|
||||
getFeaturesDir,
|
||||
getFeatureDir,
|
||||
@@ -88,7 +88,7 @@ export class FeatureLoader {
|
||||
if (!newPathSet.has(oldPath)) {
|
||||
try {
|
||||
// Paths are now absolute
|
||||
await fs.unlink(oldPath);
|
||||
await secureFs.unlink(oldPath);
|
||||
console.log(`[FeatureLoader] Deleted orphaned image: ${oldPath}`);
|
||||
} catch (error) {
|
||||
// Ignore errors when deleting (file may already be gone)
|
||||
@@ -116,7 +116,7 @@ export class FeatureLoader {
|
||||
}
|
||||
|
||||
const featureImagesDir = this.getFeatureImagesDir(projectPath, featureId);
|
||||
await fs.mkdir(featureImagesDir, { recursive: true });
|
||||
await secureFs.mkdir(featureImagesDir, { recursive: true });
|
||||
|
||||
const updatedPaths: Array<string | { path: string; [key: string]: unknown }> =
|
||||
[];
|
||||
@@ -139,7 +139,7 @@ export class FeatureLoader {
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(fullOriginalPath);
|
||||
await secureFs.access(fullOriginalPath);
|
||||
} catch {
|
||||
console.warn(
|
||||
`[FeatureLoader] Image not found, skipping: ${fullOriginalPath}`
|
||||
@@ -152,14 +152,14 @@ export class FeatureLoader {
|
||||
const newPath = path.join(featureImagesDir, filename);
|
||||
|
||||
// Copy the file
|
||||
await fs.copyFile(fullOriginalPath, newPath);
|
||||
await secureFs.copyFile(fullOriginalPath, newPath);
|
||||
console.log(
|
||||
`[FeatureLoader] Copied image: ${originalPath} -> ${newPath}`
|
||||
);
|
||||
|
||||
// Try to delete the original temp file
|
||||
try {
|
||||
await fs.unlink(fullOriginalPath);
|
||||
await secureFs.unlink(fullOriginalPath);
|
||||
} catch {
|
||||
// Ignore errors when deleting temp file
|
||||
}
|
||||
@@ -217,13 +217,13 @@ export class FeatureLoader {
|
||||
|
||||
// Check if features directory exists
|
||||
try {
|
||||
await fs.access(featuresDir);
|
||||
await secureFs.access(featuresDir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Read all feature directories
|
||||
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
||||
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
|
||||
const featureDirs = entries.filter((entry) => entry.isDirectory());
|
||||
|
||||
// Load each feature
|
||||
@@ -233,7 +233,7 @@ export class FeatureLoader {
|
||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(featureJsonPath, "utf-8");
|
||||
const content = await secureFs.readFile(featureJsonPath, "utf-8") as string;
|
||||
const feature = JSON.parse(content);
|
||||
|
||||
if (!feature.id) {
|
||||
@@ -280,7 +280,7 @@ export class FeatureLoader {
|
||||
async get(projectPath: string, featureId: string): Promise<Feature | null> {
|
||||
try {
|
||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||
const content = await fs.readFile(featureJsonPath, "utf-8");
|
||||
const content = await secureFs.readFile(featureJsonPath, "utf-8") as string;
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
@@ -309,7 +309,7 @@ export class FeatureLoader {
|
||||
await ensureAutomakerDir(projectPath);
|
||||
|
||||
// Create feature directory
|
||||
await fs.mkdir(featureDir, { recursive: true });
|
||||
await secureFs.mkdir(featureDir, { recursive: true });
|
||||
|
||||
// Migrate images from temp directory to feature directory
|
||||
const migratedImagePaths = await this.migrateImages(
|
||||
@@ -328,7 +328,7 @@ export class FeatureLoader {
|
||||
};
|
||||
|
||||
// Write feature.json
|
||||
await fs.writeFile(
|
||||
await secureFs.writeFile(
|
||||
featureJsonPath,
|
||||
JSON.stringify(feature, null, 2),
|
||||
"utf-8"
|
||||
@@ -380,7 +380,7 @@ export class FeatureLoader {
|
||||
|
||||
// Write back to file
|
||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||
await fs.writeFile(
|
||||
await secureFs.writeFile(
|
||||
featureJsonPath,
|
||||
JSON.stringify(updatedFeature, null, 2),
|
||||
"utf-8"
|
||||
@@ -396,7 +396,7 @@ export class FeatureLoader {
|
||||
async delete(projectPath: string, featureId: string): Promise<boolean> {
|
||||
try {
|
||||
const featureDir = this.getFeatureDir(projectPath, featureId);
|
||||
await fs.rm(featureDir, { recursive: true, force: true });
|
||||
await secureFs.rm(featureDir, { recursive: true, force: true });
|
||||
console.log(`[FeatureLoader] Deleted feature ${featureId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -417,7 +417,7 @@ export class FeatureLoader {
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
|
||||
const content = await fs.readFile(agentOutputPath, "utf-8");
|
||||
const content = await secureFs.readFile(agentOutputPath, "utf-8") as string;
|
||||
return content;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
@@ -440,10 +440,10 @@ export class FeatureLoader {
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const featureDir = this.getFeatureDir(projectPath, featureId);
|
||||
await fs.mkdir(featureDir, { recursive: true });
|
||||
await secureFs.mkdir(featureDir, { recursive: true });
|
||||
|
||||
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
|
||||
await fs.writeFile(agentOutputPath, content, "utf-8");
|
||||
await secureFs.writeFile(agentOutputPath, content, "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,7 +455,7 @@ export class FeatureLoader {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
|
||||
await fs.unlink(agentOutputPath);
|
||||
await secureFs.unlink(agentOutputPath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
* - Per-project settings ({projectPath}/.automaker/settings.json)
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import * as secureFs from "../lib/secure-fs.js";
|
||||
import { createLogger } from "../lib/logger.js";
|
||||
import {
|
||||
getGlobalSettingsPath,
|
||||
@@ -47,12 +46,12 @@ async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
|
||||
try {
|
||||
await fs.writeFile(tempPath, content, "utf-8");
|
||||
await fs.rename(tempPath, filePath);
|
||||
await secureFs.writeFile(tempPath, content, "utf-8");
|
||||
await secureFs.rename(tempPath, filePath);
|
||||
} catch (error) {
|
||||
// Clean up temp file if it exists
|
||||
try {
|
||||
await fs.unlink(tempPath);
|
||||
await secureFs.unlink(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
@@ -65,7 +64,7 @@ async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
|
||||
*/
|
||||
async function readJsonFile<T>(filePath: string, defaultValue: T): Promise<T> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const content = await secureFs.readFile(filePath, "utf-8") as string;
|
||||
return JSON.parse(content) as T;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
@@ -81,7 +80,7 @@ async function readJsonFile<T>(filePath: string, defaultValue: T): Promise<T> {
|
||||
*/
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await secureFs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user