Skip to content
Closed
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
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ COPY libs ./libs
COPY apps/ui ./apps/ui

# Build packages in dependency order, then build UI
# VITE_SERVER_URL tells the UI where to find the API server
# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com
ARG VITE_SERVER_URL=http://localhost:3008
# When VITE_SERVER_URL is empty, the UI uses relative URLs (e.g., /api/...) which nginx proxies
# to the server container. This avoids CORS issues entirely in Docker Compose setups.
# Override at build time if needed: --build-arg VITE_SERVER_URL=http://api.example.com
ARG VITE_SERVER_URL=
ENV VITE_SKIP_ELECTRON=true
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
RUN npm run build:packages && npm run build --workspace=apps/ui
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@automaker/server",
"version": "0.13.0",
"version": "0.15.0",
"description": "Backend server for Automaker - provides API for both web and Electron modes",
"author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE",
Expand Down
58 changes: 34 additions & 24 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,26 @@ app.use(
// CORS configuration
// When using credentials (cookies), origin cannot be '*'
// We dynamically allow the requesting origin for local development

// Check if origin is a local/private network address
function isLocalOrigin(origin: string): boolean {
try {
const url = new URL(origin);
const hostname = url.hostname;
return (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '[::1]' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
);
} catch {
return false;
}
}

app.use(
cors({
origin: (origin, callback) => {
Expand All @@ -277,35 +297,25 @@ app.use(
}

// If CORS_ORIGIN is set, use it (can be comma-separated list)
const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim());
if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') {
const allowedOrigins = process.env.CORS_ORIGIN?.split(',')
.map((o) => o.trim())
.filter(Boolean);
if (allowedOrigins && allowedOrigins.length > 0) {
if (allowedOrigins.includes('*')) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed by CORS'));
return;
}
return;
// Fall through to local network check below
}

// For local development, allow all localhost/loopback origins (any port)
try {
const url = new URL(origin);
const hostname = url.hostname;

if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
callback(null, origin);
return;
}
} catch {
// Ignore URL parsing errors
// Allow all localhost/loopback/private network origins (any port)
if (isLocalOrigin(origin)) {
callback(null, origin);
return;
}

// Reject other origins by default for security
Expand Down
74 changes: 49 additions & 25 deletions apps/server/src/lib/sdk-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,33 @@ export const TOOL_PRESETS = {
'Read',
'Write',
'Edit',
'MultiEdit',
'Glob',
'Grep',
'LS',
'Bash',
'WebSearch',
'WebFetch',
'TodoWrite',
'Task',
'Skill',
] as const,

/** Tools for chat/interactive mode */
chat: [
'Read',
'Write',
'Edit',
'MultiEdit',
'Glob',
'Grep',
'LS',
'Bash',
'WebSearch',
'WebFetch',
'TodoWrite',
'Task',
'Skill',
] as const,
} as const;

Expand Down Expand Up @@ -282,11 +290,15 @@ function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial<Options> {
}

/**
* Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true:
* - Uses preset mode with 'claude_code' to enable CLAUDE.md auto-loading
* - If there's a custom systemPrompt, appends it to the preset
* - Sets settingSources to ['project'] for SDK to load CLAUDE.md files
* Build system prompt and settingSources based on two independent settings:
* - useClaudeCodeSystemPrompt: controls whether to use the 'claude_code' preset as the base prompt
* - autoLoadClaudeMd: controls whether to add settingSources for SDK to load CLAUDE.md files
*
* These combine independently (4 possible states):
* 1. Both ON: preset + settingSources (full Claude Code experience)
* 2. useClaudeCodeSystemPrompt ON, autoLoadClaudeMd OFF: preset only (no CLAUDE.md auto-loading)
* 3. useClaudeCodeSystemPrompt OFF, autoLoadClaudeMd ON: plain string + settingSources
* 4. Both OFF: plain string only
*
* @param config - The SDK options config
* @returns Object with systemPrompt and settingSources for SDK options
Expand All @@ -295,40 +307,49 @@ function buildClaudeMdOptions(config: CreateSdkOptionsConfig): {
systemPrompt?: string | SystemPromptConfig;
settingSources?: Array<'user' | 'project' | 'local'>;
} {
if (!config.autoLoadClaudeMd) {
// Standard mode - just pass through the system prompt as-is
return config.systemPrompt ? { systemPrompt: config.systemPrompt } : {};
}

// Auto-load CLAUDE.md mode - use preset with settingSources
const result: {
systemPrompt: SystemPromptConfig;
settingSources: Array<'user' | 'project' | 'local'>;
} = {
systemPrompt: {
systemPrompt?: string | SystemPromptConfig;
settingSources?: Array<'user' | 'project' | 'local'>;
} = {};

// Determine system prompt format based on useClaudeCodeSystemPrompt
if (config.useClaudeCodeSystemPrompt) {
// Use Claude Code's built-in system prompt as the base
const presetConfig: SystemPromptConfig = {
type: 'preset',
preset: 'claude_code',
},
// Load both user (~/.claude/CLAUDE.md) and project (.claude/CLAUDE.md) settings
settingSources: ['user', 'project'],
};
};
// If there's a custom system prompt, append it to the preset
if (config.systemPrompt) {
presetConfig.append = config.systemPrompt;
}
result.systemPrompt = presetConfig;
} else {
// Standard mode - just pass through the system prompt as-is
if (config.systemPrompt) {
result.systemPrompt = config.systemPrompt;
}
}

// If there's a custom system prompt, append it to the preset
if (config.systemPrompt) {
result.systemPrompt.append = config.systemPrompt;
// Determine settingSources based on autoLoadClaudeMd
if (config.autoLoadClaudeMd) {
// Load both user (~/.claude/CLAUDE.md) and project (.claude/CLAUDE.md) settings
result.settingSources = ['user', 'project'];
}

return result;
}

/**
* System prompt configuration for SDK options
* When using preset mode with claude_code, CLAUDE.md files are automatically loaded
* The 'claude_code' preset provides the system prompt only — it does NOT auto-load
* CLAUDE.md files. CLAUDE.md auto-loading is controlled independently by
* settingSources (set via autoLoadClaudeMd). These two settings are orthogonal.
*/
export interface SystemPromptConfig {
/** Use preset mode with claude_code to enable CLAUDE.md auto-loading */
/** Use preset mode to select the base system prompt */
type: 'preset';
/** The preset to use - 'claude_code' enables CLAUDE.md loading */
/** The preset to use - 'claude_code' uses the Claude Code system prompt */
preset: 'claude_code';
/** Optional additional prompt to append to the preset */
append?: string;
Expand Down Expand Up @@ -362,6 +383,9 @@ export interface CreateSdkOptionsConfig {
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;

/** Use Claude Code's built-in system prompt (claude_code preset) as the base prompt */
useClaudeCodeSystemPrompt?: boolean;

/** MCP servers to make available to the agent */
mcpServers?: Record<string, McpServerConfig>;

Expand Down
47 changes: 45 additions & 2 deletions apps/server/src/lib/settings-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import {
const logger = createLogger('SettingsHelper');

/** Default number of agent turns used when no value is configured. */
export const DEFAULT_MAX_TURNS = 1000;
export const DEFAULT_MAX_TURNS = 10000;

/** Upper bound for the max-turns clamp; values above this are capped here. */
export const MAX_ALLOWED_TURNS = 2000;
export const MAX_ALLOWED_TURNS = 10000;

/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
Expand Down Expand Up @@ -80,6 +80,49 @@ export async function getAutoLoadClaudeMdSetting(
}
}

/**
* Get the useClaudeCodeSystemPrompt setting, with project settings taking precedence over global.
* Falls back to global settings and defaults to true when unset.
* Returns true if settings service is not available.
*
* @param projectPath - Path to the project
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to the useClaudeCodeSystemPrompt setting value
*/
export async function getUseClaudeCodeSystemPromptSetting(
projectPath: string,
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<boolean> {
if (!settingsService) {
logger.info(
`${logPrefix} SettingsService not available, useClaudeCodeSystemPrompt defaulting to true`
);
return true;
}

try {
// Check project settings first (takes precedence)
const projectSettings = await settingsService.getProjectSettings(projectPath);
if (projectSettings.useClaudeCodeSystemPrompt !== undefined) {
logger.info(
`${logPrefix} useClaudeCodeSystemPrompt from project settings: ${projectSettings.useClaudeCodeSystemPrompt}`
);
return projectSettings.useClaudeCodeSystemPrompt;
}

// Fall back to global settings
const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.useClaudeCodeSystemPrompt ?? true;
logger.info(`${logPrefix} useClaudeCodeSystemPrompt from global settings: ${result}`);
return result;
} catch (error) {
logger.error(`${logPrefix} Failed to load useClaudeCodeSystemPrompt setting:`, error);
throw error;
}
}

/**
* Get the default max turns setting from global settings.
*
Expand Down
21 changes: 19 additions & 2 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,23 @@ const logger = createLogger('ClaudeProvider');
*/
type ProviderConfig = ClaudeApiProfile | ClaudeCompatibleProvider;

// System vars are always passed from process.env regardless of profile
const SYSTEM_ENV_VARS = ['PATH', 'HOME', 'SHELL', 'TERM', 'USER', 'LANG', 'LC_ALL'];
// System vars are always passed from process.env regardless of profile.
// Includes filesystem, locale, and temp directory vars that the Claude CLI
// needs internally for config resolution and temp file creation.
const SYSTEM_ENV_VARS = [
'PATH',
'HOME',
'SHELL',
'TERM',
'USER',
'LANG',
'LC_ALL',
'TMPDIR',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_CACHE_HOME',
'XDG_STATE_HOME',
];

/**
* Check if the config is a ClaudeCompatibleProvider (new system)
Expand Down Expand Up @@ -213,6 +228,8 @@ export class ClaudeProvider extends BaseProvider {
env: buildEnv(providerConfig, credentials),
// Pass through allowedTools if provided by caller (decided by sdk-options.ts)
...(allowedTools && { allowedTools }),
// Restrict available built-in tools if specified (tools: [] disables all tools)
...(options.tools && { tools: options.tools }),
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
Expand Down
Loading