mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
@@ -190,12 +190,31 @@ server.on('upgrade', (request, socket, head) => {
|
|||||||
|
|
||||||
// Events WebSocket connection handler
|
// Events WebSocket connection handler
|
||||||
wss.on('connection', (ws: WebSocket) => {
|
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
|
// Subscribe to all events and forward to this client
|
||||||
const unsubscribe = events.subscribe((type, payload) => {
|
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) {
|
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) => {
|
ws.on('error', (error) => {
|
||||||
console.error('[WebSocket] Error:', error);
|
console.error('[WebSocket] ERROR:', error);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ export interface CreateSdkOptionsConfig {
|
|||||||
|
|
||||||
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
|
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
|
||||||
autoLoadClaudeMd?: boolean;
|
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
|
* - Full tool access for code modification
|
||||||
* - Standard turns for interactive sessions
|
* - Standard turns for interactive sessions
|
||||||
* - Model priority: explicit model > session model > chat default
|
* - 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
|
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
|
||||||
*/
|
*/
|
||||||
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
@@ -333,10 +336,12 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
|||||||
maxTurns: MAX_TURNS.standard,
|
maxTurns: MAX_TURNS.standard,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.chat],
|
allowedTools: [...TOOL_PRESETS.chat],
|
||||||
sandbox: {
|
...(config.enableSandboxMode && {
|
||||||
enabled: true,
|
sandbox: {
|
||||||
autoAllowBashIfSandboxed: true,
|
enabled: true,
|
||||||
},
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
...claudeMdOptions,
|
...claudeMdOptions,
|
||||||
...(config.abortController && { abortController: config.abortController }),
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
};
|
};
|
||||||
@@ -349,7 +354,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
|||||||
* - Full tool access for code modification and implementation
|
* - Full tool access for code modification and implementation
|
||||||
* - Extended turns for thorough feature implementation
|
* - Extended turns for thorough feature implementation
|
||||||
* - Uses default model (can be overridden)
|
* - 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
|
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
|
||||||
*/
|
*/
|
||||||
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
@@ -365,10 +370,12 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
|||||||
maxTurns: MAX_TURNS.maximum,
|
maxTurns: MAX_TURNS.maximum,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.fullAccess],
|
allowedTools: [...TOOL_PRESETS.fullAccess],
|
||||||
sandbox: {
|
...(config.enableSandboxMode && {
|
||||||
enabled: true,
|
sandbox: {
|
||||||
autoAllowBashIfSandboxed: true,
|
enabled: true,
|
||||||
},
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
...claudeMdOptions,
|
...claudeMdOptions,
|
||||||
...(config.abortController && { abortController: config.abortController }),
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
* Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
|
||||||
* and rebuilds the formatted prompt without it.
|
* and rebuilds the formatted prompt without it.
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
* Execute a query using Claude Agent SDK
|
* Execute a query using Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||||
|
console.log('[ClaudeProvider] executeQuery() called');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
@@ -35,6 +37,20 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
sdkSessionId,
|
sdkSessionId,
|
||||||
} = options;
|
} = 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
|
// Build Claude SDK options
|
||||||
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||||
const toolsToUse = allowedTools || defaultTools;
|
const toolsToUse = allowedTools || defaultTools;
|
||||||
@@ -45,11 +61,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxTurns,
|
maxTurns,
|
||||||
cwd,
|
cwd,
|
||||||
allowedTools: toolsToUse,
|
allowedTools: toolsToUse,
|
||||||
permissionMode: 'acceptEdits',
|
permissionMode: 'default',
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
abortController,
|
abortController,
|
||||||
// Resume existing SDK session if we have a session ID
|
// Resume existing SDK session if we have a session ID
|
||||||
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
|
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
|
||||||
@@ -59,6 +71,15 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
...(options.settingSources && { settingSources: options.settingSources }),
|
...(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
|
// Build prompt payload
|
||||||
let promptPayload: string | AsyncIterable<any>;
|
let promptPayload: string | AsyncIterable<any>;
|
||||||
|
|
||||||
@@ -83,14 +104,84 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
|
|
||||||
// Execute via Claude Agent SDK
|
// Execute via Claude Agent SDK
|
||||||
try {
|
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
|
// CRITICAL DEBUG: Log exact SDK options being passed
|
||||||
for await (const msg of stream) {
|
console.log('[ClaudeProvider] EXACT sdkOptions being passed to query():');
|
||||||
yield msg as ProviderMessage;
|
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) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ export function createSendHandler(agentService: AgentService) {
|
|||||||
model?: string;
|
model?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[Send Handler] Received request:', {
|
||||||
|
sessionId,
|
||||||
|
messageLength: message?.length,
|
||||||
|
workingDirectory,
|
||||||
|
imageCount: imagePaths?.length || 0,
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
|
||||||
if (!sessionId || !message) {
|
if (!sessionId || !message) {
|
||||||
|
console.log('[Send Handler] ERROR: Validation failed - missing sessionId or message');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'sessionId and message are required',
|
error: 'sessionId and message are required',
|
||||||
@@ -27,6 +36,8 @@ export function createSendHandler(agentService: AgentService) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[Send Handler] Validation passed, calling agentService.sendMessage()');
|
||||||
|
|
||||||
// Start the message processing (don't await - it streams via WebSocket)
|
// Start the message processing (don't await - it streams via WebSocket)
|
||||||
agentService
|
agentService
|
||||||
.sendMessage({
|
.sendMessage({
|
||||||
@@ -37,12 +48,16 @@ export function createSendHandler(agentService: AgentService) {
|
|||||||
model,
|
model,
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.error('[Send Handler] ERROR: Background error in sendMessage():', error);
|
||||||
logError(error, 'Send message failed (background)');
|
logError(error, 'Send message failed (background)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[Send Handler] Returning immediate response to client');
|
||||||
|
|
||||||
// Return immediately - responses come via WebSocket
|
// Return immediately - responses come via WebSocket
|
||||||
res.json({ success: true, message: 'Message sent' });
|
res.json({ success: true, message: 'Message sent' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[Send Handler] ERROR: Synchronous error:', error);
|
||||||
logError(error, 'Send message failed');
|
logError(error, 'Send message failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { ProviderFactory } from '../providers/provider-factory.js';
|
|||||||
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import type { SettingsService } from './settings-service.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';
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -140,12 +144,29 @@ export class AgentService {
|
|||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
}) {
|
}) {
|
||||||
|
console.log('[AgentService] sendMessage() called:', {
|
||||||
|
sessionId,
|
||||||
|
messageLength: message?.length,
|
||||||
|
workingDirectory,
|
||||||
|
imageCount: imagePaths?.length || 0,
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
|
||||||
const session = this.sessions.get(sessionId);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
console.error('[AgentService] ERROR: Session not found:', sessionId);
|
||||||
throw new Error(`Session ${sessionId} not found`);
|
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) {
|
if (session.isRunning) {
|
||||||
|
console.error('[AgentService] ERROR: Agent already running for session:', sessionId);
|
||||||
throw new Error('Agent is already processing a message');
|
throw new Error('Agent is already processing a message');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,16 +213,19 @@ export class AgentService {
|
|||||||
session.abortController = new AbortController();
|
session.abortController = new AbortController();
|
||||||
|
|
||||||
// Emit started event so UI can show thinking indicator
|
// Emit started event so UI can show thinking indicator
|
||||||
|
console.log('[AgentService] Emitting "started" event for session:', sessionId);
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: 'started',
|
type: 'started',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit user message event
|
// Emit user message event
|
||||||
|
console.log('[AgentService] Emitting "message" event for session:', sessionId);
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[AgentService] Saving session messages');
|
||||||
await this.saveSession(sessionId, session.messages);
|
await this.saveSession(sessionId, session.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -215,6 +239,12 @@ export class AgentService {
|
|||||||
'[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.)
|
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
|
||||||
const contextResult = await loadContextFiles({
|
const contextResult = await loadContextFiles({
|
||||||
projectPath: effectiveWorkDir,
|
projectPath: effectiveWorkDir,
|
||||||
@@ -239,6 +269,7 @@ export class AgentService {
|
|||||||
systemPrompt: combinedSystemPrompt,
|
systemPrompt: combinedSystemPrompt,
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
autoLoadClaudeMd,
|
autoLoadClaudeMd,
|
||||||
|
enableSandboxMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract model, maxTurns, and allowedTools from SDK options
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
@@ -247,6 +278,7 @@ export class AgentService {
|
|||||||
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
// Get provider for this model
|
// Get provider for this model
|
||||||
|
console.log('[AgentService] Getting provider for model:', effectiveModel);
|
||||||
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -267,6 +299,7 @@ export class AgentService {
|
|||||||
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[AgentService] Building prompt with images...');
|
||||||
// Build prompt content with images
|
// Build prompt content with images
|
||||||
const { content: promptContent } = await buildPromptWithImages(
|
const { content: promptContent } = await buildPromptWithImages(
|
||||||
message,
|
message,
|
||||||
@@ -278,14 +311,32 @@ export class AgentService {
|
|||||||
// Set the prompt in options
|
// Set the prompt in options
|
||||||
options.prompt = promptContent;
|
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
|
// Execute via provider
|
||||||
const stream = provider.executeQuery(options);
|
const stream = provider.executeQuery(options);
|
||||||
|
console.log('[AgentService] Stream created, starting to iterate...');
|
||||||
|
|
||||||
let currentAssistantMessage: Message | null = null;
|
let currentAssistantMessage: Message | null = null;
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
const toolUses: Array<{ name: string; input: unknown }> = [];
|
||||||
|
let messageCount = 0;
|
||||||
|
|
||||||
|
console.log('[AgentService] Entering stream loop...');
|
||||||
for await (const msg of stream) {
|
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
|
// Capture SDK session ID from any message and persist it
|
||||||
if (msg.session_id && !session.sdkSessionId) {
|
if (msg.session_id && !session.sdkSessionId) {
|
||||||
session.sdkSessionId = msg.session_id;
|
session.sdkSessionId = msg.session_id;
|
||||||
@@ -295,6 +346,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'assistant') {
|
if (msg.type === 'assistant') {
|
||||||
|
console.log('[AgentService] Processing assistant message...');
|
||||||
if (msg.message?.content) {
|
if (msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === 'text') {
|
if (block.type === 'text') {
|
||||||
@@ -312,6 +364,10 @@ export class AgentService {
|
|||||||
currentAssistantMessage.content = responseText;
|
currentAssistantMessage.content = responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[AgentService] Emitting "stream" event, text length:',
|
||||||
|
responseText.length
|
||||||
|
);
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: 'stream',
|
type: 'stream',
|
||||||
messageId: currentAssistantMessage.id,
|
messageId: currentAssistantMessage.id,
|
||||||
@@ -325,6 +381,7 @@ export class AgentService {
|
|||||||
};
|
};
|
||||||
toolUses.push(toolUse);
|
toolUses.push(toolUse);
|
||||||
|
|
||||||
|
console.log('[AgentService] Tool use detected:', toolUse.name);
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: 'tool_use',
|
type: 'tool_use',
|
||||||
tool: toolUse,
|
tool: toolUse,
|
||||||
@@ -333,6 +390,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'result') {
|
} else if (msg.type === 'result') {
|
||||||
|
console.log('[AgentService] Result message received, subtype:', (msg as any).subtype);
|
||||||
if (msg.subtype === 'success' && msg.result) {
|
if (msg.subtype === 'success' && msg.result) {
|
||||||
if (currentAssistantMessage) {
|
if (currentAssistantMessage) {
|
||||||
currentAssistantMessage.content = msg.result;
|
currentAssistantMessage.content = msg.result;
|
||||||
@@ -340,6 +398,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[AgentService] Emitting "complete" event');
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: 'complete',
|
type: 'complete',
|
||||||
messageId: currentAssistantMessage?.id,
|
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);
|
await this.saveSession(sessionId, session.messages);
|
||||||
|
|
||||||
session.isRunning = false;
|
session.isRunning = false;
|
||||||
@@ -757,7 +818,13 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private emitAgentEvent(sessionId: string, data: Record<string, unknown>): void {
|
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 });
|
this.events.emit('agent:stream', { sessionId, ...data });
|
||||||
|
console.log('[AgentService] Event emitted to EventEmitter');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSystemPrompt(): string {
|
private getSystemPrompt(): string {
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ import {
|
|||||||
} from '../lib/sdk-options.js';
|
} from '../lib/sdk-options.js';
|
||||||
import { FeatureLoader } from './feature-loader.js';
|
import { FeatureLoader } from './feature-loader.js';
|
||||||
import type { SettingsService } from './settings-service.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);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -1833,12 +1837,16 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
|||||||
? options.autoLoadClaudeMd
|
? options.autoLoadClaudeMd
|
||||||
: await getAutoLoadClaudeMdSetting(finalProjectPath, this.settingsService, '[AutoMode]');
|
: 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
|
// Build SDK options using centralized configuration for feature implementation
|
||||||
const sdkOptions = createAutoModeOptions({
|
const sdkOptions = createAutoModeOptions({
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
model: model,
|
model: model,
|
||||||
abortController,
|
abortController,
|
||||||
autoLoadClaudeMd,
|
autoLoadClaudeMd,
|
||||||
|
enableSandboxMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract model, maxTurns, and allowedTools from SDK options
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export function SettingsView() {
|
|||||||
setValidationModel,
|
setValidationModel,
|
||||||
autoLoadClaudeMd,
|
autoLoadClaudeMd,
|
||||||
setAutoLoadClaudeMd,
|
setAutoLoadClaudeMd,
|
||||||
|
enableSandboxMode,
|
||||||
|
setEnableSandboxMode,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Hide usage tracking when using API key (only show for Claude Code CLI users)
|
// Hide usage tracking when using API key (only show for Claude Code CLI users)
|
||||||
@@ -108,6 +110,8 @@ export function SettingsView() {
|
|||||||
<ClaudeMdSettings
|
<ClaudeMdSettings
|
||||||
autoLoadClaudeMd={autoLoadClaudeMd}
|
autoLoadClaudeMd={autoLoadClaudeMd}
|
||||||
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
||||||
|
enableSandboxMode={enableSandboxMode}
|
||||||
|
onEnableSandboxModeChange={setEnableSandboxMode}
|
||||||
/>
|
/>
|
||||||
{showUsageTracking && <ClaudeUsageSection />}
|
{showUsageTracking && <ClaudeUsageSection />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { FileCode } from 'lucide-react';
|
import { FileCode, Shield } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ClaudeMdSettingsProps {
|
interface ClaudeMdSettingsProps {
|
||||||
autoLoadClaudeMd: boolean;
|
autoLoadClaudeMd: boolean;
|
||||||
onAutoLoadClaudeMdChange: (enabled: boolean) => void;
|
onAutoLoadClaudeMdChange: (enabled: boolean) => void;
|
||||||
|
enableSandboxMode: boolean;
|
||||||
|
onEnableSandboxModeChange: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +27,8 @@ interface ClaudeMdSettingsProps {
|
|||||||
export function ClaudeMdSettings({
|
export function ClaudeMdSettings({
|
||||||
autoLoadClaudeMd,
|
autoLoadClaudeMd,
|
||||||
onAutoLoadClaudeMdChange,
|
onAutoLoadClaudeMdChange,
|
||||||
|
enableSandboxMode,
|
||||||
|
onEnableSandboxModeChange,
|
||||||
}: ClaudeMdSettingsProps) {
|
}: ClaudeMdSettingsProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -76,6 +80,32 @@ export function ClaudeMdSettings({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3 mt-2">
|
||||||
|
<Checkbox
|
||||||
|
id="enable-sandbox-mode"
|
||||||
|
checked={enableSandboxMode}
|
||||||
|
onCheckedChange={(checked) => onEnableSandboxModeChange(checked === true)}
|
||||||
|
className="mt-1"
|
||||||
|
data-testid="enable-sandbox-mode-checkbox"
|
||||||
|
/>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="enable-sandbox-mode"
|
||||||
|
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Shield className="w-4 h-4 text-brand-500" />
|
||||||
|
Enable Sandbox Mode
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||||
|
Run bash commands in an isolated sandbox environment for additional security.
|
||||||
|
<span className="block mt-1 text-warning/80">
|
||||||
|
Note: On some systems, enabling sandbox mode may cause the agent to hang without
|
||||||
|
responding. If you experience issues, try disabling this option.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -480,6 +480,7 @@ export interface AppState {
|
|||||||
|
|
||||||
// Claude Agent SDK Settings
|
// Claude Agent SDK Settings
|
||||||
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
|
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
|
||||||
|
enableSandboxMode: boolean; // Enable sandbox mode for bash commands (may cause issues on some systems)
|
||||||
|
|
||||||
// Project Analysis
|
// Project Analysis
|
||||||
projectAnalysis: ProjectAnalysis | null;
|
projectAnalysis: ProjectAnalysis | null;
|
||||||
@@ -756,6 +757,7 @@ export interface AppActions {
|
|||||||
|
|
||||||
// Claude Agent SDK Settings actions
|
// Claude Agent SDK Settings actions
|
||||||
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
||||||
|
setEnableSandboxMode: (enabled: boolean) => Promise<void>;
|
||||||
|
|
||||||
// AI Profile actions
|
// AI Profile actions
|
||||||
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
|
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
|
||||||
@@ -929,6 +931,7 @@ const initialState: AppState = {
|
|||||||
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
||||||
validationModel: 'opus', // Default to opus for GitHub issue validation
|
validationModel: 'opus', // Default to opus for GitHub issue validation
|
||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
|
enableSandboxMode: true, // Default to enabled for security (can be disabled if issues occur)
|
||||||
aiProfiles: DEFAULT_AI_PROFILES,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
@@ -1561,6 +1564,12 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
||||||
await syncSettingsToServer();
|
await syncSettingsToServer();
|
||||||
},
|
},
|
||||||
|
setEnableSandboxMode: async (enabled) => {
|
||||||
|
set({ enableSandboxMode: enabled });
|
||||||
|
// Sync to server settings file
|
||||||
|
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
||||||
|
await syncSettingsToServer();
|
||||||
|
},
|
||||||
|
|
||||||
// AI Profile actions
|
// AI Profile actions
|
||||||
addAIProfile: (profile) => {
|
addAIProfile: (profile) => {
|
||||||
@@ -2706,6 +2715,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
enhancementModel: state.enhancementModel,
|
enhancementModel: state.enhancementModel,
|
||||||
validationModel: state.validationModel,
|
validationModel: state.validationModel,
|
||||||
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
||||||
|
enableSandboxMode: state.enableSandboxMode,
|
||||||
// Profiles and sessions
|
// Profiles and sessions
|
||||||
aiProfiles: state.aiProfiles,
|
aiProfiles: state.aiProfiles,
|
||||||
chatSessions: state.chatSessions,
|
chatSessions: state.chatSessions,
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ export interface GlobalSettings {
|
|||||||
// Claude Agent SDK Settings
|
// Claude Agent SDK Settings
|
||||||
/** Auto-load CLAUDE.md files using SDK's settingSources option */
|
/** Auto-load CLAUDE.md files using SDK's settingSources option */
|
||||||
autoLoadClaudeMd?: boolean;
|
autoLoadClaudeMd?: boolean;
|
||||||
|
/** Enable sandbox mode for bash commands (default: true, disable if issues occur) */
|
||||||
|
enableSandboxMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -459,6 +461,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
worktreePanelCollapsed: false,
|
worktreePanelCollapsed: false,
|
||||||
lastSelectedSessionByProject: {},
|
lastSelectedSessionByProject: {},
|
||||||
autoLoadClaudeMd: false,
|
autoLoadClaudeMd: false,
|
||||||
|
enableSandboxMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default credentials (empty strings - user must provide API keys) */
|
/** Default credentials (empty strings - user must provide API keys) */
|
||||||
|
|||||||
Reference in New Issue
Block a user