feat: add configurable sandbox mode setting

Add a global setting to enable/disable sandbox mode for Claude Agent SDK.
This allows users to control sandbox behavior based on their authentication
setup and system compatibility.

Changes:
- Add enableSandboxMode to GlobalSettings (default: true)
- Add sandbox mode checkbox in Claude settings UI
- Wire up setting through app store and settings service
- Update createChatOptions and createAutoModeOptions to use setting
- Add getEnableSandboxModeSetting helper function
- Remove hardcoded sandbox configuration from ClaudeProvider
- Add detailed logging throughout agent execution flow

The sandbox mode requires API key or OAuth token authentication. Users
experiencing issues with CLI-only auth can disable it in settings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stephan Rieche
2025-12-27 12:24:28 +01:00
parent 0fe6a12d20
commit 920dcd105f
11 changed files with 308 additions and 26 deletions

View File

@@ -190,12 +190,31 @@ server.on('upgrade', (request, socket, head) => {
// Events WebSocket connection handler
wss.on('connection', (ws: WebSocket) => {
console.log('[WebSocket] Client connected');
console.log('[WebSocket] Client connected, ready state:', ws.readyState);
// Subscribe to all events and forward to this client
const unsubscribe = events.subscribe((type, payload) => {
console.log('[WebSocket] Event received:', {
type,
hasPayload: !!payload,
payloadKeys: payload ? Object.keys(payload) : [],
wsReadyState: ws.readyState,
wsOpen: ws.readyState === WebSocket.OPEN,
});
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type, payload }));
const message = JSON.stringify({ type, payload });
console.log('[WebSocket] Sending event to client:', {
type,
messageLength: message.length,
sessionId: (payload as any)?.sessionId,
});
ws.send(message);
} else {
console.log(
'[WebSocket] WARNING: Cannot send event, WebSocket not open. ReadyState:',
ws.readyState
);
}
});
@@ -205,7 +224,7 @@ wss.on('connection', (ws: WebSocket) => {
});
ws.on('error', (error) => {
console.error('[WebSocket] Error:', error);
console.error('[WebSocket] ERROR:', error);
unsubscribe();
});
});

View File

@@ -216,6 +216,9 @@ export interface CreateSdkOptionsConfig {
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;
/** Enable sandbox mode for bash command isolation */
enableSandboxMode?: boolean;
}
/**
@@ -314,7 +317,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
* - Full tool access for code modification
* - Standard turns for interactive sessions
* - Model priority: explicit model > session model > chat default
* - Sandbox enabled for bash safety
* - Sandbox mode controlled by enableSandboxMode setting
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
@@ -333,10 +336,12 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
maxTurns: MAX_TURNS.standard,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.chat],
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.enableSandboxMode && {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
}),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
@@ -349,7 +354,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Full tool access for code modification and implementation
* - Extended turns for thorough feature implementation
* - Uses default model (can be overridden)
* - Sandbox enabled for bash safety
* - Sandbox mode controlled by enableSandboxMode setting
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
@@ -365,10 +370,12 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.fullAccess],
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.enableSandboxMode && {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
}),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};

View File

@@ -45,6 +45,34 @@ export async function getAutoLoadClaudeMdSetting(
}
}
/**
* Get the enableSandboxMode setting from global settings.
* Returns false if settings service is not available.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to the enableSandboxMode setting value
*/
export async function getEnableSandboxModeSetting(
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<boolean> {
if (!settingsService) {
console.log(`${logPrefix} SettingsService not available, sandbox mode disabled`);
return false;
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.enableSandboxMode ?? false;
console.log(`${logPrefix} enableSandboxMode from global settings: ${result}`);
return result;
} catch (error) {
console.error(`${logPrefix} Failed to load enableSandboxMode setting:`, error);
throw error;
}
}
/**
* Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
* and rebuilds the formatted prompt without it.

View File

@@ -23,6 +23,8 @@ export class ClaudeProvider extends BaseProvider {
* Execute a query using Claude Agent SDK
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
console.log('[ClaudeProvider] executeQuery() called');
const {
prompt,
model,
@@ -35,6 +37,20 @@ export class ClaudeProvider extends BaseProvider {
sdkSessionId,
} = options;
console.log('[ClaudeProvider] Options:', {
model,
cwd,
maxTurns,
promptType: typeof prompt,
promptLength: typeof prompt === 'string' ? prompt.length : 'array',
hasSystemPrompt: !!systemPrompt,
systemPromptLength: systemPrompt?.length,
hasConversationHistory: !!conversationHistory?.length,
conversationHistoryLength: conversationHistory?.length || 0,
sdkSessionId,
allowedToolsCount: allowedTools?.length,
});
// Build Claude SDK options
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
const toolsToUse = allowedTools || defaultTools;
@@ -45,11 +61,7 @@ export class ClaudeProvider extends BaseProvider {
maxTurns,
cwd,
allowedTools: toolsToUse,
permissionMode: 'acceptEdits',
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
permissionMode: 'default',
abortController,
// Resume existing SDK session if we have a session ID
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
@@ -59,6 +71,15 @@ export class ClaudeProvider extends BaseProvider {
...(options.settingSources && { settingSources: options.settingSources }),
};
console.log('[ClaudeProvider] SDK options prepared:', {
model: sdkOptions.model,
maxTurns: sdkOptions.maxTurns,
permissionMode: sdkOptions.permissionMode,
sandboxEnabled: sdkOptions.sandbox?.enabled || false,
hasResume: !!(sdkOptions as any).resume,
toolsCount: sdkOptions.allowedTools?.length,
});
// Build prompt payload
let promptPayload: string | AsyncIterable<any>;
@@ -83,14 +104,84 @@ export class ClaudeProvider extends BaseProvider {
// Execute via Claude Agent SDK
try {
const stream = query({ prompt: promptPayload, options: sdkOptions });
console.log('[ClaudeProvider] ANTHROPIC_API_KEY exists:', !!process.env.ANTHROPIC_API_KEY);
console.log(
'[ClaudeProvider] ANTHROPIC_API_KEY length:',
process.env.ANTHROPIC_API_KEY?.length || 0
);
console.log('[ClaudeProvider] HOME directory:', process.env.HOME);
console.log('[ClaudeProvider] User:', process.env.USER);
console.log('[ClaudeProvider] Current working directory:', process.cwd());
// Stream messages directly - they're already in the correct format
for await (const msg of stream) {
yield msg as ProviderMessage;
// CRITICAL DEBUG: Log exact SDK options being passed
console.log('[ClaudeProvider] EXACT sdkOptions being passed to query():');
console.log(
JSON.stringify(
{
model: sdkOptions.model,
maxTurns: sdkOptions.maxTurns,
cwd: sdkOptions.cwd,
allowedTools: sdkOptions.allowedTools,
permissionMode: sdkOptions.permissionMode,
hasSandbox: !!sdkOptions.sandbox,
hasAbortController: !!sdkOptions.abortController,
hasResume: !!(sdkOptions as any).resume,
hasSettingSources: !!sdkOptions.settingSources,
settingSources: sdkOptions.settingSources,
},
null,
2
)
);
console.log('[ClaudeProvider] Calling Claude Agent SDK query()...');
console.log(
'[ClaudeProvider] About to call query() with prompt payload type:',
typeof promptPayload
);
const stream = query({ prompt: promptPayload, options: sdkOptions });
console.log('[ClaudeProvider] query() call returned, stream object type:', typeof stream);
console.log('[ClaudeProvider] SDK query() returned stream, starting iteration...');
let streamMessageCount = 0;
// Add a watchdog timer to detect if stream is hanging
let lastMessageTime = Date.now();
const watchdogInterval = setInterval(() => {
const timeSinceLastMessage = Date.now() - lastMessageTime;
if (timeSinceLastMessage > 10000) {
console.log(
`[ClaudeProvider] WARNING: No messages received for ${Math.floor(timeSinceLastMessage / 1000)}s`
);
}
}, 5000);
try {
// Stream messages directly - they're already in the correct format
for await (const msg of stream) {
lastMessageTime = Date.now();
streamMessageCount++;
console.log(`[ClaudeProvider] Stream message #${streamMessageCount}:`, {
type: msg.type,
subtype: (msg as any).subtype,
hasMessage: !!(msg as any).message,
hasResult: !!(msg as any).result,
session_id: msg.session_id,
});
yield msg as ProviderMessage;
}
} finally {
clearInterval(watchdogInterval);
}
console.log(
'[ClaudeProvider] Stream iteration completed, total messages:',
streamMessageCount
);
} catch (error) {
console.error('[ClaudeProvider] executeQuery() error during execution:', error);
console.error('[ClaudeProvider] ERROR: executeQuery() error during execution:', error);
console.error('[ClaudeProvider] ERROR stack:', (error as Error).stack);
throw error;
}
}

View File

@@ -19,7 +19,16 @@ export function createSendHandler(agentService: AgentService) {
model?: string;
};
console.log('[Send Handler] Received request:', {
sessionId,
messageLength: message?.length,
workingDirectory,
imageCount: imagePaths?.length || 0,
model,
});
if (!sessionId || !message) {
console.log('[Send Handler] ERROR: Validation failed - missing sessionId or message');
res.status(400).json({
success: false,
error: 'sessionId and message are required',
@@ -27,6 +36,8 @@ export function createSendHandler(agentService: AgentService) {
return;
}
console.log('[Send Handler] Validation passed, calling agentService.sendMessage()');
// Start the message processing (don't await - it streams via WebSocket)
agentService
.sendMessage({
@@ -37,12 +48,16 @@ export function createSendHandler(agentService: AgentService) {
model,
})
.catch((error) => {
console.error('[Send Handler] ERROR: Background error in sendMessage():', error);
logError(error, 'Send message failed (background)');
});
console.log('[Send Handler] Returning immediate response to client');
// Return immediately - responses come via WebSocket
res.json({ success: true, message: 'Message sent' });
} catch (error) {
console.error('[Send Handler] ERROR: Synchronous error:', error);
logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}

View File

@@ -17,7 +17,11 @@ import { ProviderFactory } from '../providers/provider-factory.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { PathNotAllowedError } from '@automaker/platform';
import type { SettingsService } from './settings-service.js';
import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getEnableSandboxModeSetting,
filterClaudeMdFromContext,
} from '../lib/settings-helpers.js';
interface Message {
id: string;
@@ -140,12 +144,29 @@ export class AgentService {
imagePaths?: string[];
model?: string;
}) {
console.log('[AgentService] sendMessage() called:', {
sessionId,
messageLength: message?.length,
workingDirectory,
imageCount: imagePaths?.length || 0,
model,
});
const session = this.sessions.get(sessionId);
if (!session) {
console.error('[AgentService] ERROR: Session not found:', sessionId);
throw new Error(`Session ${sessionId} not found`);
}
console.log('[AgentService] Session found:', {
sessionId,
messageCount: session.messages.length,
isRunning: session.isRunning,
workingDirectory: session.workingDirectory,
});
if (session.isRunning) {
console.error('[AgentService] ERROR: Agent already running for session:', sessionId);
throw new Error('Agent is already processing a message');
}
@@ -192,16 +213,19 @@ export class AgentService {
session.abortController = new AbortController();
// Emit started event so UI can show thinking indicator
console.log('[AgentService] Emitting "started" event for session:', sessionId);
this.emitAgentEvent(sessionId, {
type: 'started',
});
// Emit user message event
console.log('[AgentService] Emitting "message" event for session:', sessionId);
this.emitAgentEvent(sessionId, {
type: 'message',
message: userMessage,
});
console.log('[AgentService] Saving session messages');
await this.saveSession(sessionId, session.messages);
try {
@@ -215,6 +239,12 @@ export class AgentService {
'[AgentService]'
);
// Load enableSandboxMode setting (global setting only)
const enableSandboxMode = await getEnableSandboxModeSetting(
this.settingsService,
'[AgentService]'
);
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
const contextResult = await loadContextFiles({
projectPath: effectiveWorkDir,
@@ -239,6 +269,7 @@ export class AgentService {
systemPrompt: combinedSystemPrompt,
abortController: session.abortController!,
autoLoadClaudeMd,
enableSandboxMode,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -247,6 +278,7 @@ export class AgentService {
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
// Get provider for this model
console.log('[AgentService] Getting provider for model:', effectiveModel);
const provider = ProviderFactory.getProviderForModel(effectiveModel);
console.log(
@@ -267,6 +299,7 @@ export class AgentService {
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
};
console.log('[AgentService] Building prompt with images...');
// Build prompt content with images
const { content: promptContent } = await buildPromptWithImages(
message,
@@ -278,14 +311,32 @@ export class AgentService {
// Set the prompt in options
options.prompt = promptContent;
console.log('[AgentService] Executing query via provider:', {
model: effectiveModel,
promptLength: typeof promptContent === 'string' ? promptContent.length : 'array',
hasConversationHistory: !!conversationHistory.length,
sdkSessionId: session.sdkSessionId,
});
// Execute via provider
const stream = provider.executeQuery(options);
console.log('[AgentService] Stream created, starting to iterate...');
let currentAssistantMessage: Message | null = null;
let responseText = '';
const toolUses: Array<{ name: string; input: unknown }> = [];
let messageCount = 0;
console.log('[AgentService] Entering stream loop...');
for await (const msg of stream) {
messageCount++;
console.log(`[AgentService] Stream message #${messageCount}:`, {
type: msg.type,
subtype: (msg as any).subtype,
hasContent: !!(msg as any).message?.content,
session_id: msg.session_id,
});
// Capture SDK session ID from any message and persist it
if (msg.session_id && !session.sdkSessionId) {
session.sdkSessionId = msg.session_id;
@@ -295,6 +346,7 @@ export class AgentService {
}
if (msg.type === 'assistant') {
console.log('[AgentService] Processing assistant message...');
if (msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
@@ -312,6 +364,10 @@ export class AgentService {
currentAssistantMessage.content = responseText;
}
console.log(
'[AgentService] Emitting "stream" event, text length:',
responseText.length
);
this.emitAgentEvent(sessionId, {
type: 'stream',
messageId: currentAssistantMessage.id,
@@ -325,6 +381,7 @@ export class AgentService {
};
toolUses.push(toolUse);
console.log('[AgentService] Tool use detected:', toolUse.name);
this.emitAgentEvent(sessionId, {
type: 'tool_use',
tool: toolUse,
@@ -333,6 +390,7 @@ export class AgentService {
}
}
} else if (msg.type === 'result') {
console.log('[AgentService] Result message received, subtype:', (msg as any).subtype);
if (msg.subtype === 'success' && msg.result) {
if (currentAssistantMessage) {
currentAssistantMessage.content = msg.result;
@@ -340,6 +398,7 @@ export class AgentService {
}
}
console.log('[AgentService] Emitting "complete" event');
this.emitAgentEvent(sessionId, {
type: 'complete',
messageId: currentAssistantMessage?.id,
@@ -349,6 +408,8 @@ export class AgentService {
}
}
console.log('[AgentService] Stream loop completed, total messages:', messageCount);
await this.saveSession(sessionId, session.messages);
session.isRunning = false;
@@ -757,7 +818,13 @@ export class AgentService {
}
private emitAgentEvent(sessionId: string, data: Record<string, unknown>): void {
console.log('[AgentService] emitAgentEvent() called:', {
sessionId,
eventType: data.type,
dataKeys: Object.keys(data),
});
this.events.emit('agent:stream', { sessionId, ...data });
console.log('[AgentService] Event emitted to EventEmitter');
}
private getSystemPrompt(): string {

View File

@@ -32,7 +32,11 @@ import {
} from '../lib/sdk-options.js';
import { FeatureLoader } from './feature-loader.js';
import type { SettingsService } from './settings-service.js';
import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getEnableSandboxModeSetting,
filterClaudeMdFromContext,
} from '../lib/settings-helpers.js';
const execAsync = promisify(exec);
@@ -1833,12 +1837,16 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
? options.autoLoadClaudeMd
: await getAutoLoadClaudeMdSetting(finalProjectPath, this.settingsService, '[AutoMode]');
// Load enableSandboxMode setting (global setting only)
const enableSandboxMode = await getEnableSandboxModeSetting(this.settingsService, '[AutoMode]');
// Build SDK options using centralized configuration for feature implementation
const sdkOptions = createAutoModeOptions({
cwd: workDir,
model: model,
abortController,
autoLoadClaudeMd,
enableSandboxMode,
});
// Extract model, maxTurns, and allowedTools from SDK options