Skip to content
Open
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
14 changes: 12 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ CORS_ORIGINS=["*"]

# Model Configuration
# Default Claude model to use when none specified in request
DEFAULT_MODEL=claude-sonnet-4-5-20250929
DEFAULT_MODEL=claude-sonnet-4-6

# Rate Limiting Configuration
RATE_LIMIT_ENABLED=true
Expand All @@ -35,4 +35,14 @@ RATE_LIMIT_CHAT_PER_MINUTE=10
RATE_LIMIT_DEBUG_PER_MINUTE=2
RATE_LIMIT_AUTH_PER_MINUTE=10
RATE_LIMIT_SESSION_PER_MINUTE=15
RATE_LIMIT_HEALTH_PER_MINUTE=30
RATE_LIMIT_HEALTH_PER_MINUTE=30

# Security Configuration
# Comma-separated list of trusted proxy IPs (for X-Forwarded-For rate limiting)
# TRUSTED_PROXIES=10.0.0.1,10.0.0.2
# Base directory for CLAUDE_CWD sandboxing (default: system temp dir)
# CLAUDE_CWD_ALLOWED_BASE=/tmp
# Maximum concurrent sessions (default: 1000)
# MAX_SESSIONS=1000
# Maximum messages per session history (default: 100)
# MAX_SESSION_MESSAGES=100
58 changes: 43 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
FROM python:3.12-slim
# Stage 1: Builder — install Poetry and dependencies
FROM python:3.12-slim AS builder

# Install system deps (curl for Poetry installer)
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install Poetry globally
RUN curl -sSL https://install.python-poetry.org | python3 -

# Add Poetry to PATH
ENV PATH="/root/.local/bin:${PATH}"

# Note: Claude Code CLI is bundled with claude-agent-sdk >= 0.1.8
# No separate Node.js/npm installation required
WORKDIR /app

# Copy dependency files first (cache-friendly)
COPY pyproject.toml poetry.lock ./

# Copy the app code
COPY . /app
# Install dependencies into a virtualenv
RUN poetry config virtualenvs.in-project true && \
poetry install --no-root --no-interaction

# Copy application code
COPY . .

# Install the project itself
RUN poetry install --no-interaction


# Stage 2: Runtime — minimal image with non-root user
FROM python:3.12-slim

# Install Node.js (required by Claude Agent SDK bundled CLI)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd --gid 1000 appuser && \
useradd --uid 1000 --gid appuser --create-home appuser

# Set working directory
WORKDIR /app

# Install Python dependencies with Poetry
RUN poetry install --no-root
# Copy virtualenv and app code from builder (owned by appuser)
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appuser /app/src /app/src
COPY --from=builder --chown=appuser:appuser /app/pyproject.toml /app/pyproject.toml

# Ensure virtualenv binaries are on PATH
ENV PATH="/app/.venv/bin:${PATH}"
ENV VIRTUAL_ENV="/app/.venv"

# Switch to non-root user
USER appuser

# Expose the port (default 8000)
EXPOSE 8000

# Run the app with Uvicorn (development mode with reload; switch to --no-reload for prod)
CMD ["poetry", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# Production CMD — no --reload
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ services:
ports:
- "8000:8000"
volumes:
- ~/.claude:/root/.claude
- ~/.claude:/home/appuser/.claude
# Optional: Mount a specific workspace directory
# Uncomment and modify the line below to use a custom workspace
# - ./workspace:/workspace
Expand Down
15 changes: 8 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ python-dotenv = "^1.0.1"
httpx = "^0.27.2"
sse-starlette = "^2.1.3"
python-multipart = "^0.0.18"
claude-agent-sdk = "^0.1.18"
claude-agent-sdk = "^0.1.52"
slowapi = "^0.1.9"

[tool.poetry.group.dev.dependencies]
Expand Down
11 changes: 11 additions & 0 deletions src/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,14 @@ def get_claude_code_auth_info() -> Dict[str, Any]:
"status": auth_manager.auth_status,
"environment_variables": list(auth_manager.get_claude_code_env_vars().keys()),
}


def redact_key(value: str) -> str:
"""Redact a credential value for safe logging (FR-4.1, FR-4.2).

Strings >= 8 chars show first 3 and last 3 characters with masking in between.
Strings < 8 chars are fully masked to avoid leaking short secrets.
"""
if len(value) >= 8:
return f"{value[:3]}***...***{value[-3:]}"
return "***"
12 changes: 9 additions & 3 deletions src/claude_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging

from claude_agent_sdk import query, ClaudeAgentOptions
from src.constants import CLAUDE_CWD_ALLOWED_BASE

logger = logging.getLogger(__name__)

Expand All @@ -19,16 +20,21 @@ def __init__(self, timeout: int = 600000, cwd: Optional[str] = None):
# If cwd is provided (from CLAUDE_CWD env var), use it
# Otherwise create an isolated temp directory
if cwd:
self.cwd = Path(cwd)
self.cwd = Path(cwd).resolve()
# Check if the directory exists
if not self.cwd.exists():
logger.error(f"ERROR: Specified working directory does not exist: {self.cwd}")
logger.error(
"Please create the directory first or unset CLAUDE_CWD to use a temporary directory"
)
raise ValueError(f"Working directory does not exist: {self.cwd}")
else:
logger.info(f"Using CLAUDE_CWD: {self.cwd}")
# Sandbox check: reject paths outside the allowed base directory
allowed_base = Path(CLAUDE_CWD_ALLOWED_BASE).resolve()
if not self.cwd.is_relative_to(allowed_base):
raise ValueError(
f"Working directory {self.cwd} is outside allowed base {allowed_base}"
)
logger.info(f"Using CLAUDE_CWD: {self.cwd}")
else:
# Create isolated temp directory (cross-platform)
self.temp_dir = tempfile.mkdtemp(prefix="claude_code_workspace_")
Expand Down
30 changes: 15 additions & 15 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@
from src.constants import DEFAULT_ALLOWED_TOOLS
options = {"allowed_tools": DEFAULT_ALLOWED_TOOLS}

# Use rate limits in FastAPI
from src.constants import RATE_LIMIT_CHAT
@limiter.limit(f"{RATE_LIMIT_CHAT}/minute")
async def chat_endpoint(): ...

Note:
- Tool configurations are managed by ToolManager (see tool_manager.py)
- Model validation uses graceful degradation (warns but allows unknown models)
- Rate limits can be overridden via environment variables
"""

import os
import tempfile

# Claude Agent SDK Tool Names
# These are the built-in tools available in the Claude Agent SDK
Expand Down Expand Up @@ -66,12 +62,15 @@ async def chat_endpoint(): ...
]

# Claude Models
# Models supported by Claude Agent SDK (as of November 2025)
# Models supported by Claude Agent SDK (as of March 2026)
# NOTE: Claude Agent SDK only supports Claude 4+ models, not Claude 3.x
CLAUDE_MODELS = [
# Claude 4.5 Family (Latest - Fall 2025) - RECOMMENDED
"claude-opus-4-5-20250929", # Latest Opus 4.5 - Most capable
"claude-sonnet-4-5-20250929", # Recommended - best coding model
# Claude 4.6 Family (Latest - 2026) - RECOMMENDED
"claude-opus-4-6", # Latest Opus 4.6 - Most capable
"claude-sonnet-4-6", # Recommended - best coding model
# Claude 4.5 Family (Fall 2025)
"claude-opus-4-5-20250929",
"claude-sonnet-4-5-20250929",
"claude-haiku-4-5-20251001", # Fast & cheap
# Claude 4.1
"claude-opus-4-1-20250805", # Upgraded Opus 4
Expand All @@ -88,7 +87,7 @@ async def chat_endpoint(): ...

# Default model (recommended for most use cases)
# Can be overridden via DEFAULT_MODEL environment variable
DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "claude-sonnet-4-5-20250929")
DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "claude-sonnet-4-6")

# Fast model (for speed/cost optimization)
FAST_MODEL = "claude-haiku-4-5-20251001"
Expand All @@ -109,8 +108,9 @@ async def chat_endpoint(): ...
SESSION_CLEANUP_INTERVAL_MINUTES = 5
SESSION_MAX_AGE_MINUTES = 60

# Rate Limiting (requests per minute)
RATE_LIMIT_DEFAULT = 60
RATE_LIMIT_CHAT = 30
RATE_LIMIT_MODELS = 100
RATE_LIMIT_HEALTH = 200
# Security Configuration
MAX_SESSIONS = int(os.getenv("MAX_SESSIONS", "1000"))
MAX_SESSION_MESSAGES = int(os.getenv("MAX_SESSION_MESSAGES", "100"))
_trusted_proxies_raw = os.getenv("TRUSTED_PROXIES", "")
TRUSTED_PROXIES = [p.strip() for p in _trusted_proxies_raw.split(",") if p.strip()]
CLAUDE_CWD_ALLOWED_BASE = os.getenv("CLAUDE_CWD_ALLOWED_BASE", tempfile.gettempdir())
Loading
Loading