Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .claude/commands/validate-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Project Build and Fix Command

Run all builds and intelligently fix any failures based on what changed.

## Instructions

1. **Run the build**

```bash
npm run build
```

This builds all packages and the UI application.

2. **If the build succeeds**, report success and stop.

3. **If the build fails**, analyze the failures:
- Note which build step failed and the error messages
- Check for TypeScript compilation errors, missing dependencies, or configuration issues
- Run `git diff main` to see what code has changed

4. **Determine the nature of the failure**:
- **If the failure is due to intentional changes** (new features, refactoring, dependency updates):
- Fix any TypeScript type errors introduced by the changes
- Update build configuration if needed (e.g., tsconfig.json, vite.config.mts)
- Ensure all new dependencies are properly installed
- Fix import paths or module resolution issues

- **If the failure appears to be a regression** (broken imports, missing files, configuration errors):
- Fix the source code to restore the build
- Check for accidentally deleted files or broken references
- Verify build configuration files are correct

5. **Common build issues to check**:
- **TypeScript errors**: Fix type mismatches, missing types, or incorrect imports
- **Missing dependencies**: Run `npm install` if packages are missing
- **Import/export errors**: Fix incorrect import paths or missing exports
- **Build configuration**: Check tsconfig.json, vite.config.mts, or other build configs
- **Package build order**: Ensure `build:packages` completes before building apps

6. **How to decide if it's intentional vs regression**:
- Look at the git diff and commit messages
- If the change was deliberate and introduced new code that needs fixing → fix the new code
- If the change broke existing functionality that should still build → fix the regression
- When in doubt, ask the user

7. **After making fixes**, re-run the build to verify everything compiles successfully.

8. **Report summary** of what was fixed (TypeScript errors, configuration issues, missing dependencies, etc.).
36 changes: 36 additions & 0 deletions .claude/commands/validate-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Project Test and Fix Command

Run all tests and intelligently fix any failures based on what changed.

## Instructions

1. **Run all tests**

```bash
npm run test:all
```

2. **If all tests pass**, report success and stop.

3. **If any tests fail**, analyze the failures:
- Note which tests failed and their error messages
- Run `git diff main` to see what code has changed

4. **Determine the nature of the change**:
- **If the logic change is intentional** (new feature, refactor, behavior change):
- Update the failing tests to match the new expected behavior
- The tests should reflect what the code NOW does correctly

- **If the logic change appears to be a bug** (regression, unintended side effect):
- Fix the source code to restore the expected behavior
- Do NOT modify the tests - they are catching a real bug

5. **How to decide if it's a bug vs intentional change**:
- Look at the git diff and commit messages
- If the change was deliberate and the test expectations are now outdated → update tests
- If the change broke existing functionality that should still work → fix the code
- When in doubt, ask the user

6. **After making fixes**, re-run the tests to verify everything passes.

7. **Report summary** of what was fixed (tests updated vs code fixed).
2 changes: 2 additions & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
22

2 changes: 1 addition & 1 deletion apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ALLOWED_ROOT_DIRECTORY=

# CORS origin - which domains can access the API
# Use "*" for development, set specific origin for production
CORS_ORIGIN=*
CORS_ORIGIN=http://localhost:3007

# ============================================
# OPTIONAL - Server
Expand Down
57 changes: 30 additions & 27 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE",
"private": true,
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"type": "module",
"main": "dist/index.js",
"scripts": {
Expand All @@ -21,35 +24,35 @@
"test:unit": "vitest run tests/unit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.72",
"@automaker/dependency-resolver": "^1.0.0",
"@automaker/git-utils": "^1.0.0",
"@automaker/model-resolver": "^1.0.0",
"@automaker/platform": "^1.0.0",
"@automaker/prompts": "^1.0.0",
"@automaker/types": "^1.0.0",
"@automaker/utils": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"morgan": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "0.1.72",
"@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "1.0.0",
"@automaker/platform": "1.0.0",
"@automaker/prompts": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0",
"@modelcontextprotocol/sdk": "1.25.1",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"dotenv": "17.2.3",
"express": "5.2.1",
"morgan": "1.10.1",
"node-pty": "1.1.0-beta41",
"ws": "^8.18.3"
"ws": "8.18.3"
},
"devDependencies": {
"@types/cookie": "^0.6.0",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^22",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"tsx": "^4.21.0",
"typescript": "^5",
"vitest": "^4.0.16"
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/express": "5.0.6",
"@types/morgan": "1.9.10",
"@types/node": "22.19.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "4.0.16",
"@vitest/ui": "4.0.16",
"tsx": "4.21.0",
"typescript": "5.9.3",
"vitest": "4.0.16"
}
}
18 changes: 9 additions & 9 deletions apps/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import type { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import * as secureFs from './secure-fs.js';

const DATA_DIR = process.env.DATA_DIR || './data';
const API_KEY_FILE = path.join(DATA_DIR, '.api-key');
Expand Down Expand Up @@ -41,8 +41,8 @@ setInterval(() => {
*/
function loadSessions(): void {
try {
if (fs.existsSync(SESSIONS_FILE)) {
const data = fs.readFileSync(SESSIONS_FILE, 'utf-8');
if (secureFs.existsSync(SESSIONS_FILE)) {
const data = secureFs.readFileSync(SESSIONS_FILE, 'utf-8') as string;
const sessions = JSON.parse(data) as Array<
[string, { createdAt: number; expiresAt: number }]
>;
Expand Down Expand Up @@ -74,9 +74,9 @@ function loadSessions(): void {
*/
async function saveSessions(): Promise<void> {
try {
await fs.promises.mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
await secureFs.mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries());
await fs.promises.writeFile(SESSIONS_FILE, JSON.stringify(sessions), {
await secureFs.writeFile(SESSIONS_FILE, JSON.stringify(sessions), {
encoding: 'utf-8',
mode: 0o600,
});
Expand All @@ -101,8 +101,8 @@ function ensureApiKey(): string {

// Try to read from file
try {
if (fs.existsSync(API_KEY_FILE)) {
const key = fs.readFileSync(API_KEY_FILE, 'utf-8').trim();
if (secureFs.existsSync(API_KEY_FILE)) {
const key = (secureFs.readFileSync(API_KEY_FILE, 'utf-8') as string).trim();
if (key) {
console.log('[Auth] Loaded API key from file');
return key;
Expand All @@ -115,8 +115,8 @@ function ensureApiKey(): string {
// Generate new key
const newKey = crypto.randomUUID();
try {
fs.mkdirSync(path.dirname(API_KEY_FILE), { recursive: true });
fs.writeFileSync(API_KEY_FILE, newKey, { encoding: 'utf-8', mode: 0o600 });
secureFs.mkdirSync(path.dirname(API_KEY_FILE), { recursive: true });
secureFs.writeFileSync(API_KEY_FILE, newKey, { encoding: 'utf-8', mode: 0o600 });
console.log('[Auth] Generated new API key');
} catch (error) {
console.error('[Auth] Failed to save API key:', error);
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/lib/secure-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { secureFs } from '@automaker/platform';

export const {
// Async methods
access,
readFile,
writeFile,
Expand All @@ -20,6 +21,16 @@ export const {
lstat,
joinPath,
resolvePath,
// Sync methods
existsSync,
readFileSync,
writeFileSync,
mkdirSync,
readdirSync,
statSync,
accessSync,
unlinkSync,
rmSync,
// Throttling configuration and monitoring
configureThrottling,
getThrottlingConfig,
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@ import type {
ModelDefinition,
} from './types.js';

// Explicit allowlist of environment variables to pass to the SDK.
// Only these vars are passed - nothing else from process.env leaks through.
const ALLOWED_ENV_VARS = [
'ANTHROPIC_API_KEY',
'PATH',
'HOME',
'SHELL',
'TERM',
'USER',
'LANG',
'LC_ALL',
];

/**
* Build environment for the SDK with only explicitly allowed variables
*/
function buildEnv(): Record<string, string | undefined> {
const env: Record<string, string | undefined> = {};
for (const key of ALLOWED_ENV_VARS) {
if (process.env[key]) {
env[key] = process.env[key];
}
}
return env;
}

export class ClaudeProvider extends BaseProvider {
getName(): string {
return 'claude';
Expand Down Expand Up @@ -57,6 +83,8 @@ export class ClaudeProvider extends BaseProvider {
systemPrompt,
maxTurns,
cwd,
// Pass only explicitly allowed environment variables to SDK
env: buildEnv(),
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
...(allowedTools && shouldRestrictTools && { allowedTools }),
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
Expand Down
14 changes: 7 additions & 7 deletions apps/server/src/routes/context/routes/describe-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger, readImageAsBase64 } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/types';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import * as fs from 'fs';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
Expand Down Expand Up @@ -57,13 +57,13 @@ function filterSafeHeaders(headers: Record<string, unknown>): Record<string, unk
*/
function findActualFilePath(requestedPath: string): string | null {
// First, try the exact path
if (fs.existsSync(requestedPath)) {
if (secureFs.existsSync(requestedPath)) {
return requestedPath;
}

// Try with Unicode normalization
const normalizedPath = requestedPath.normalize('NFC');
if (fs.existsSync(normalizedPath)) {
if (secureFs.existsSync(normalizedPath)) {
return normalizedPath;
}

Expand All @@ -72,12 +72,12 @@ function findActualFilePath(requestedPath: string): string | null {
const dir = path.dirname(requestedPath);
const baseName = path.basename(requestedPath);

if (!fs.existsSync(dir)) {
if (!secureFs.existsSync(dir)) {
return null;
}

try {
const files = fs.readdirSync(dir);
const files = secureFs.readdirSync(dir);

// Normalize the requested basename for comparison
// Replace various space-like characters with regular space for comparison
Expand Down Expand Up @@ -281,9 +281,9 @@ export function createDescribeImageHandler(
}

// Log path + stats (this is often where issues start: missing file, perms, size)
let stat: fs.Stats | null = null;
let stat: ReturnType<typeof secureFs.statSync> | null = null;
try {
stat = fs.statSync(actualPath);
stat = secureFs.statSync(actualPath);
logger.info(
`[${requestId}] fileStats size=${stat.size} bytes mtime=${stat.mtime.toISOString()}`
);
Expand Down
17 changes: 12 additions & 5 deletions apps/server/src/routes/fs/routes/browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
import * as secureFs from '../../../lib/secure-fs.js';
import os from 'os';
import path from 'path';
import { getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform';
import { getAllowedRootDirectory, PathNotAllowedError, isPathAllowed } from '@automaker/platform';
import { getErrorMessage, logError } from '../common.js';

export function createBrowseHandler() {
Expand Down Expand Up @@ -40,9 +40,16 @@ export function createBrowseHandler() {
return drives;
};

// Get parent directory
// Get parent directory - only if it's within the allowed root
const parentPath = path.dirname(targetPath);
const hasParent = parentPath !== targetPath;

// Determine if parent navigation should be allowed:
// 1. Must have a different parent (not at filesystem root)
// 2. If ALLOWED_ROOT_DIRECTORY is set, parent must be within it
const hasParent = parentPath !== targetPath && isPathAllowed(parentPath);

// Security: Don't expose parent path outside allowed root
const safeParentPath = hasParent ? parentPath : null;

// Get available drives
const drives = await detectDrives();
Expand Down Expand Up @@ -70,7 +77,7 @@ export function createBrowseHandler() {
res.json({
success: true,
currentPath: targetPath,
parentPath: hasParent ? parentPath : null,
parentPath: safeParentPath,
directories,
drives,
});
Expand All @@ -84,7 +91,7 @@ export function createBrowseHandler() {
res.json({
success: true,
currentPath: targetPath,
parentPath: hasParent ? parentPath : null,
parentPath: safeParentPath,
directories: [],
drives,
warning:
Expand Down
Loading