Skip to content

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
typescript
// 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:

typescript
// 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:

typescript
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.

typescript
// 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:

typescript
// /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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
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

typescript
// 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

typescript
// 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:

bash
# 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

typescript
// 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

Released under the MIT License.