Skip to content

Commit 9486d1d

Browse files
authored
Update dependencies, improve security, and add CI config (#11)
* Update dependencies, improve security, and add CI config - Update dependencies to latest versions (FastAPI, Pydantic, Uvicorn) - Bump Python requirement to 3.11+ - Fix license metadata to GPL-3.0-or-later - Remove unused asyncio-mqtt dependency - Implement path sanitization in project creation - Improve CORS configuration (restrict defaults, explicit methods) - Add pytest-cov for test coverage reporting - Update Makefile with coverage and install-dev targets - Add Renovate configuration - Remove dead code in claude_manager.py * Potential fix for pull request finding 'Unused import' * Update claude_code_api/api/projects.py
1 parent b882705 commit 9486d1d

10 files changed

Lines changed: 151 additions & 50 deletions

File tree

Makefile

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,26 @@ install:
55
pip install -e .
66
pip install requests
77

8+
install-dev:
9+
pip install -e ".[test,dev]"
10+
pip install requests
11+
812
test:
13+
python -m pytest --cov=claude_code_api --cov-report=html tests/ -v
14+
15+
test-no-cov:
916
python -m pytest tests/ -v
1017

18+
coverage:
19+
@echo "Opening coverage report..."
20+
@if [ "$$(uname)" = "Darwin" ]; then \
21+
open htmlcov/index.html; \
22+
elif [ "$$(uname)" = "Linux" ]; then \
23+
xdg-open htmlcov/index.html || echo "Please open htmlcov/index.html in your browser"; \
24+
else \
25+
echo "Please open htmlcov/index.html in your browser"; \
26+
fi
27+
1128
test-real:
1229
python tests/test_real_api.py
1330

@@ -44,7 +61,10 @@ help:
4461
@echo ""
4562
@echo "Python API:"
4663
@echo " make install - Install Python dependencies"
47-
@echo " make test - Run Python unit tests with real Claude integration"
64+
@echo " make install-dev - Install Python development dependencies"
65+
@echo " make test - Run Python unit tests with coverage"
66+
@echo " make test-no-cov - Run Python unit tests without coverage"
67+
@echo " make coverage - Open HTML coverage report"
4868
@echo " make test-real - Run REAL end-to-end tests (curls actual API)"
4969
@echo " make start - Start Python API server (development with reload)"
5070
@echo " make start-prod - Start Python API server (production)"

claude_code_api/api/projects.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
)
1717
from claude_code_api.core.database import db_manager, Project
1818
from claude_code_api.core.claude_manager import create_project_directory, cleanup_project_directory
19+
from claude_code_api.core.security import validate_path
20+
from claude_code_api.core.config import settings
1921

2022
logger = structlog.get_logger()
2123
router = APIRouter()
@@ -69,7 +71,14 @@ async def create_project(
6971

7072
# Create project directory
7173
if project_request.path:
72-
project_path = project_request.path
74+
# Validate path
75+
try:
76+
project_path = validate_path(project_request.path, settings.project_root)
77+
try:
78+
project_path = validate_path(project_request.path, settings.project_root)
79+
except HTTPException:
80+
raise
81+
7382
os.makedirs(project_path, exist_ok=True)
7483
else:
7584
project_path = create_project_directory(project_id)

claude_code_api/core/claude_manager.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,32 +192,6 @@ async def send_input(self, text: str):
192192
error=str(e)
193193
)
194194

195-
async def _start_mock_process(self, prompt: str, model: str):
196-
"""Start mock process for testing."""
197-
self.is_running = True
198-
199-
# Create mock Claude response
200-
mock_response = {
201-
"type": "result",
202-
"sessionId": self.session_id,
203-
"model": model or "claude-3-5-haiku-20241022",
204-
"message": {
205-
"role": "assistant",
206-
"content": f"Hello! You said: '{prompt}'. This is a mock response from Claude Code API Gateway."
207-
},
208-
"usage": {
209-
"input_tokens": len(prompt.split()),
210-
"output_tokens": 15,
211-
"total_tokens": len(prompt.split()) + 15
212-
},
213-
"cost_usd": 0.001,
214-
"duration_ms": 100
215-
}
216-
217-
# Put the response in the queue
218-
await self.output_queue.put(mock_response)
219-
await self.output_queue.put(None) # End signal
220-
221195
async def stop(self):
222196
"""Stop Claude process."""
223197
self.is_running = False

claude_code_api/core/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,15 @@ def parse_api_keys(cls, v):
9494
log_format: str = "json"
9595

9696
# CORS Configuration
97-
allowed_origins: List[str] = Field(default=["*"])
98-
allowed_methods: List[str] = Field(default=["*"])
97+
allowed_origins: List[str] = Field(default=["http://localhost:8000", "http://127.0.0.1:8000"])
98+
allowed_methods: List[str] = Field(default=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
9999
allowed_headers: List[str] = Field(default=["*"])
100100

101101
@field_validator('allowed_origins', 'allowed_methods', 'allowed_headers', mode='before')
102102
def parse_cors_lists(cls, v):
103103
if isinstance(v, str):
104104
return [x.strip() for x in v.split(',') if x.strip()]
105-
return v or ["*"]
105+
return v
106106

107107
# Rate Limiting
108108
rate_limit_requests_per_minute: int = 100

claude_code_api/core/security.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Security utilities."""
2+
3+
import os
4+
import structlog
5+
from fastapi import HTTPException, status
6+
7+
logger = structlog.get_logger()
8+
9+
def validate_path(path: str, base_path: str) -> str:
10+
"""
11+
Validate that a path is safe and within the base path.
12+
Prevents directory traversal attacks.
13+
14+
Args:
15+
path: The path to validate (can be absolute or relative)
16+
base_path: The allowed base directory
17+
18+
Returns:
19+
The normalized absolute path if valid
20+
21+
Raises:
22+
HTTPException: If path is invalid or outside base_path
23+
"""
24+
try:
25+
# Normalize base path to absolute path
26+
abs_base_path = os.path.abspath(base_path)
27+
28+
# Handle relative paths by joining with base_path
29+
if not os.path.isabs(path):
30+
abs_path = os.path.abspath(os.path.join(abs_base_path, path))
31+
else:
32+
abs_path = os.path.abspath(path)
33+
34+
# Check if path is within base_path
35+
# os.path.commonpath returns the longest common sub-path
36+
# If valid, commonpath should be equal to base_path
37+
if os.path.commonpath([abs_base_path, abs_path]) != abs_base_path:
38+
logger.warning(
39+
"Path traversal attempt detected",
40+
path=path,
41+
resolved_path=abs_path,
42+
base_path=abs_base_path
43+
)
44+
raise HTTPException(
45+
status_code=status.HTTP_400_BAD_REQUEST,
46+
detail="Invalid path: Path traversal detected"
47+
)
48+
49+
return abs_path
50+
51+
except HTTPException:
52+
raise
53+
except Exception as e:
54+
logger.error("Path validation error", error=str(e), path=path)
55+
raise HTTPException(
56+
status_code=status.HTTP_400_BAD_REQUEST,
57+
detail=f"Invalid path: {str(e)}"
58+
)

claude_code_api/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
9696
CORSMiddleware,
9797
allow_origins=settings.allowed_origins,
9898
allow_credentials=True,
99-
allow_methods=["*"],
100-
allow_headers=["*"],
99+
allow_methods=settings.allowed_methods,
100+
allow_headers=settings.allowed_headers,
101101
)
102102

103103
# Authentication middleware

pyproject.toml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,30 @@ name = "claude-code-api"
77
version = "1.0.0"
88
description = "OpenAI-compatible API gateway for Claude Code with streaming support"
99
readme = "README.md"
10-
license = {text = "MIT"}
10+
license = {text = "GPL-3.0-or-later"}
1111
authors = [
1212
{name = "Claude Code API Team"}
1313
]
1414
keywords = ["claude", "api", "openai", "streaming", "ai"]
1515
classifiers = [
1616
"Development Status :: 4 - Beta",
1717
"Intended Audience :: Developers",
18-
"License :: OSI Approved :: MIT License",
18+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
1919
"Programming Language :: Python :: 3",
20-
"Programming Language :: Python :: 3.10",
2120
"Programming Language :: Python :: 3.11",
2221
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
2323
"Topic :: Software Development :: Libraries :: Python Modules",
2424
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
2525
]
26-
requires-python = ">=3.10"
26+
requires-python = ">=3.11"
2727
dependencies = [
28-
"fastapi>=0.104.0",
29-
"uvicorn[standard]>=0.24.0",
30-
"pydantic>=2.5.0",
28+
"fastapi>=0.115.0",
29+
"uvicorn[standard]>=0.32.0",
30+
"pydantic>=2.9.0",
3131
"httpx>=0.25.0",
3232
"aiofiles>=23.2.1",
3333
"structlog>=23.2.0",
34-
"asyncio-mqtt>=0.16.1",
3534
"python-multipart>=0.0.6",
3635
"pydantic-settings>=2.1.0",
3736
"sqlalchemy>=2.0.23",
@@ -105,7 +104,7 @@ exclude_lines = [
105104

106105
[tool.black]
107106
line-length = 88
108-
target-version = ['py310']
107+
target-version = ['py311']
109108
include = '\\.pyi?$'
110109
extend-exclude = '''
111110
/(
@@ -128,7 +127,7 @@ line_length = 88
128127
known_first_party = ["claude_code_api"]
129128

130129
[tool.mypy]
131-
python_version = "3.10"
130+
python_version = "3.11"
132131
check_untyped_defs = true
133132
disallow_any_generics = true
134133
disallow_incomplete_defs = true

renovate.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"config:base"
5+
],
6+
"packageRules": [
7+
{
8+
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
9+
"automerge": true
10+
}
11+
],
12+
"labels": ["dependencies"]
13+
}

setup.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@
1313
author="Claude Code API Team",
1414
url="https://github.com/claude-code-api/claude-code-api",
1515
packages=find_packages(),
16-
python_requires=">=3.10",
16+
python_requires=">=3.11",
1717
install_requires=[
18-
"fastapi>=0.104.0",
19-
"uvicorn[standard]>=0.24.0",
20-
"pydantic>=2.5.0",
18+
"fastapi>=0.115.0",
19+
"uvicorn[standard]>=0.32.0",
20+
"pydantic>=2.9.0",
2121
"httpx>=0.25.0",
2222
"aiofiles>=23.2.1",
2323
"structlog>=23.2.0",
24-
"asyncio-mqtt>=0.16.1",
2524
"python-multipart>=0.0.6",
2625
"pydantic-settings>=2.1.0",
2726
"sqlalchemy>=2.0.23",
@@ -55,11 +54,11 @@
5554
classifiers=[
5655
"Development Status :: 4 - Beta",
5756
"Intended Audience :: Developers",
58-
"License :: OSI Approved :: MIT License",
57+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
5958
"Programming Language :: Python :: 3",
60-
"Programming Language :: Python :: 3.10",
6159
"Programming Language :: Python :: 3.11",
6260
"Programming Language :: Python :: 3.12",
61+
"Programming Language :: Python :: 3.13",
6362
"Topic :: Software Development :: Libraries :: Python Modules",
6463
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
6564
],

tests/test_security.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
from fastapi import HTTPException
3+
from claude_code_api.core.security import validate_path
4+
5+
def test_validate_path_valid():
6+
base = "/tmp/projects"
7+
path = "project1"
8+
assert validate_path(path, base) == "/tmp/projects/project1"
9+
10+
def test_validate_path_traversal():
11+
base = "/tmp/projects"
12+
path = "../etc/passwd"
13+
with pytest.raises(HTTPException) as exc:
14+
validate_path(path, base)
15+
assert exc.value.status_code == 400
16+
assert "Path traversal detected" in exc.value.detail
17+
18+
def test_validate_path_absolute_traversal():
19+
base = "/tmp/projects"
20+
path = "/etc/passwd"
21+
with pytest.raises(HTTPException) as exc:
22+
validate_path(path, base)
23+
assert exc.value.status_code == 400
24+
assert "Path traversal detected" in exc.value.detail
25+
26+
def test_validate_path_absolute_valid():
27+
base = "/tmp/projects"
28+
path = "/tmp/projects/project1"
29+
assert validate_path(path, base) == "/tmp/projects/project1"

0 commit comments

Comments
 (0)