Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ dependencies = [
"typer>=0.16,<0.17",
"questionary>=2.0.1,<3",
"rich>=13.9.2,<14",
"fastapi>=0.115.0,<0.116",
"fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 fix needs starlette>=0.49.1
"starlette>=0.49.1,<1.0.0", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln; cap <1.0.0 avoids BaseHTTPMiddleware streaming regression
"tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release
"uvicorn>=0.31.1",
"watchfiles>=0.24.0,<1.0",
"python-on-whales>=0.73.0,<0.74",
Expand Down
30 changes: 19 additions & 11 deletions src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from fastapi import FastAPI, Request
from pydantic import TypeAdapter, ValidationError
from fastapi.responses import StreamingResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp, Receive, Scope, Send

from agentex.lib.types.acp import (
RPC_SYNC_METHODS,
Expand Down Expand Up @@ -43,17 +43,25 @@
task_message_update_adapter = TypeAdapter(TaskMessageUpdate)


class RequestIDMiddleware(BaseHTTPMiddleware):
"""Middleware to extract or generate request IDs and add them to logs and response headers"""
class RequestIDMiddleware:
"""Pure ASGI middleware to extract or generate request IDs and set them in the logging context.

async def dispatch(self, request: Request, call_next): # type: ignore[override]
# Extract request ID from header or generate a new one if there isn't one
request_id = request.headers.get("x-request-id") or uuid.uuid4().hex
# Store request ID in request state for access in handlers
ctx_var_request_id.set(request_id)
# Process request
response = await call_next(request)
return response
Implemented as a pure ASGI middleware (rather than BaseHTTPMiddleware) so that it never
buffers the response body. BaseHTTPMiddleware's call_next() silently swallows
StreamingResponse bodies in several starlette versions, which caused message/send handlers
to return result=null through the Agentex server proxy.
"""

def __init__(self, app: ASGIApp) -> None:
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
headers = dict(scope.get("headers", []))
raw_request_id = headers.get(b"x-request-id", b"")
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 decode() can raise UnicodeDecodeError on malformed headers

raw_request_id.decode() uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the x-request-id header, this will raise an unhandled UnicodeDecodeError that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so decode('latin-1') is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

Suggested change
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Line: 62

Comment:
**`decode()` can raise `UnicodeDecodeError` on malformed headers**

`raw_request_id.decode()` uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the `x-request-id` header, this will raise an unhandled `UnicodeDecodeError` that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so `decode('latin-1')` is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

```suggestion
            request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
```

How can I resolve this? If you propose a fix, please make it concise.

ctx_var_request_id.set(request_id)
await self.app(scope, receive, send)


class BaseACPServer(FastAPI):
Expand Down
93 changes: 73 additions & 20 deletions src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,20 @@ async def mock_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
"id": "test-1",
Expand Down Expand Up @@ -218,11 +227,20 @@ async def mock_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
# No ID = notification
Expand Down Expand Up @@ -256,7 +274,17 @@ async def mock_handler(params):
request = {
"jsonrpc": "2.0",
"method": "task/cancel",
"params": {"task_id": "test-task-123"},
"params": {
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-123"},
},
"id": "test-request-1",
}

Expand All @@ -280,7 +308,7 @@ def test_send_message_synchronous_response(self, base_acp_server):
async def mock_execute_handler(params):
return {
"task_id": params.task.id,
"message_content": params.message.content,
"message_content": params.content.content,
"status": "executed_synchronously",
"custom_data": {"processed": True, "timestamp": "2024-01-01T12:00:00Z"},
}
Expand All @@ -291,8 +319,16 @@ async def mock_execute_handler(params):
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"task": {"id": "test-task-123", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-123"},
"content": {
"type": "text",
"author": "user",
"content": "Execute this task please",
Expand Down Expand Up @@ -340,7 +376,15 @@ async def mock_init_handler(params):
"jsonrpc": "2.0",
"method": "task/create",
"params": {
"task": {"id": "test-task-456", "agent_id": "test-agent", "status": "RUNNING"}
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-456"},
},
"id": "test-init-1",
}
Expand Down Expand Up @@ -428,11 +472,20 @@ async def failing_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
"id": "test-1",
Expand Down
Loading
Loading