Security
This guide covers security best practices for deploying Helix Agents in production.
Error Response Sanitization
As of v0.10, error responses from AgentServer are sanitized to prevent information leakage. This protects against exposing:
- Internal implementation details
- Zod validation schema structure
- Stack traces and debug information
- Credentials or API keys in error messages
- Database query details
Server-Side Behavior
Error responses now contain only:
- HTTP status code (4xx for client errors, 500 for server errors)
- Generic error message
// What clients receive for validation errors
{ "error": "Invalid request body" }
// What clients receive for server errors
{ "error": "Internal server error" }Full error details are logged server-side for debugging:
// Server logs include full details
this.logger.error('Validation failed', {
runId,
error: error.message,
issues: zodError.issues, // Only in server logs
stack: error.stack, // Only in server logs
});Client-Side Error Handling
Use HTTP status codes for error categorization:
try {
const response = await fetch('/chat/session-123', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello' }),
});
if (!response.ok) {
switch (response.status) {
case 400:
// Client error: Invalid request, validation failed
showUserError('Invalid input. Please check your request.');
break;
case 409:
// Conflict: Already executing
showUserError('An agent is already running.');
break;
case 500:
// Server error: Log and show generic message
console.error('Server error');
showUserError('Something went wrong. Please try again.');
break;
}
}
} catch (error) {
// Network error
showUserError('Connection failed. Please try again.');
}Input Validation
Request Body Validation
The AgentServer validates all request bodies using Zod schemas. Invalid requests receive a 400 status code with a generic error message.
// Server automatically validates:
// - /start endpoint: StartAgentRequestSchema
// - /resume endpoint: ResumeAgentRequestSchema
// - /interrupt endpoint: InterruptRequestSchema
// - /abort endpoint: AbortRequestSchema
// Validation errors return:
// Status: 400
// Body: { "error": "Invalid request body" }Query Parameter Validation
Query parameters for pagination are validated:
// /messages?offset=X&limit=Y
// - offset: must be non-negative integer, defaults to 0
// - limit: must be positive integer, max 1000, defaults to 50
// Invalid parameters return:
// Status: 400
// Body: { "error": "Invalid offset parameter" }Agent Configuration Security
Sensitive Data in State
Avoid storing sensitive data in agent state:
// Don't do this
const agent = defineAgent({
name: 'my-agent',
stateSchema: z.object({
apiKey: z.string(), // Sensitive data in state
}),
});
// Do this instead
const agent = defineAgent({
name: 'my-agent',
stateSchema: z.object({
// Non-sensitive state only
results: z.array(z.string()),
}),
});
// Pass sensitive data via environment
const searchTool = defineTool({
name: 'search',
execute: async (params, context) => {
// Access API key from environment, not state
const apiKey = process.env.SEARCH_API_KEY;
// ...
},
});System Prompt Injection
Be cautious with user input in system prompts:
// Potentially dangerous
const agent = defineAgent({
systemPrompt: `You are helping ${userInput}`, // User input in prompt
});
// Safer approach
const agent = defineAgent({
systemPrompt: 'You are a helpful assistant.',
// Pass user context via input, not system prompt
});Tool Security
Validate Tool Inputs
Always validate tool inputs, even with Zod schemas:
const fileTool = defineTool({
name: 'read_file',
inputSchema: z.object({
path: z.string().refine(
(path) => !path.includes('..'), // Prevent path traversal
'Path cannot contain ..'
),
}),
execute: async ({ path }) => {
// Additional runtime validation
const resolvedPath = resolve(baseDir, path);
if (!resolvedPath.startsWith(baseDir)) {
throw new Error('Access denied');
}
return readFile(resolvedPath);
},
});Limit Tool Capabilities
Use requiresConfirmation for dangerous operations:
const deleteTool = defineTool({
name: 'delete_file',
requiresConfirmation: true, // Pauses for user approval
execute: async ({ path }) => {
// Only executes after user confirms
await deleteFile(path);
},
});Streaming Security
SSE/WebSocket Authentication
Implement authentication for streaming endpoints:
// Worker entry point
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Verify authentication before forwarding to DO
const token = request.headers.get('Authorization');
if (!verifyToken(token, env.JWT_SECRET)) {
return new Response('Unauthorized', { status: 401 });
}
// Forward authenticated request to DO
const stub = env.AGENTS.get(doId);
return stub.fetch(request);
},
};Rate Limiting
Implement rate limiting at the worker level:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const clientIP = request.headers.get('CF-Connecting-IP');
// Check rate limit
const allowed = await checkRateLimit(env.RATE_LIMITER, clientIP);
if (!allowed) {
return new Response('Too many requests', { status: 429 });
}
// Process request
// ...
},
};Logging Best Practices
What to Log
// Good: Log operation metadata
logger.info('Agent started', {
runId,
agentType: agent.name,
userId,
sessionId,
});
// Good: Log errors with context
logger.error('Tool execution failed', {
runId,
toolName,
error: error.message,
stack: error.stack,
});What NOT to Log
// Bad: Logging sensitive data
logger.info('Request', {
body: request.body, // May contain credentials
});
// Bad: Logging full state
logger.info('State', {
state: agentState, // May contain PII
});
// Bad: Logging API responses
logger.info('LLM response', {
response: llmResponse, // May contain sensitive content
});Environment Variables
Required Secrets
Store all sensitive configuration in environment variables:
# Cloudflare Workers
wrangler secret put OPENAI_API_KEY
wrangler secret put ANTHROPIC_API_KEY
wrangler secret put JWT_SECRET
# Node.js
export OPENAI_API_KEY=sk-...
export REDIS_URL=redis://...Access Pattern
// Cloudflare Workers
interface Env {
OPENAI_API_KEY: string;
// Never expose in responses
}
export class MyAgentServer extends AgentServer<Env> {
protected createLLMAdapter(): LLMAdapter {
const env = this.getEnv();
return new VercelAIAdapter({
model: openai('gpt-4o', { apiKey: env.OPENAI_API_KEY }),
});
}
}Production Checklist
Before deploying to production:
- [ ] Error responses are sanitized (default in v0.10+)
- [ ] Authentication implemented for all endpoints
- [ ] Rate limiting configured
- [ ] Sensitive data not stored in agent state
- [ ] Tool inputs validated and sanitized
- [ ] Dangerous tools require confirmation
- [ ] API keys stored in environment variables
- [ ] Logging excludes sensitive data
- [ ] HTTPS enforced for all connections
- [ ] CORS configured appropriately
Next Steps
- Distributed Coordination - Secure multi-process deployments
- Hooks - Add security logging via hooks
- Cloudflare DO - AgentServer security features