Prefix Redis keys with docket name for ACL isolation (#2812)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Guidry 2026-01-08 19:04:15 -05:00 committed by GitHub
commit 3df88260eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 34 additions and 26 deletions

View file

@ -10,7 +10,7 @@ dependencies = [
"mcp>=1.24.0,<2.0",
"openapi-pydantic>=0.5.1",
"platformdirs>=4.0.0",
"pydocket>=0.16.3",
"pydocket>=0.16.4",
"rich>=13.9.4",
"cyclopts>=4.0.0",
"authlib>=1.6.5",

View file

@ -72,13 +72,15 @@ async def handle_tool_as_task(
tool = await server.get_tool(tool_name)
# Store task key mapping and creation timestamp in Redis for protocol handlers
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
created_at_key = docket.key(
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
)
ttl_seconds = int(
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
)
async with docket.redis() as redis:
await redis.set(redis_key, task_key, ex=ttl_seconds)
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
await redis.set(created_at_key, created_at, ex=ttl_seconds)
# Send notifications/tasks/created per SEP-1686 (mandatory)
@ -181,13 +183,15 @@ async def handle_prompt_as_task(
prompt = await server.get_prompt(prompt_name)
# Store task key mapping and creation timestamp in Redis for protocol handlers
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
created_at_key = docket.key(
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
)
ttl_seconds = int(
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
)
async with docket.redis() as redis:
await redis.set(redis_key, task_key, ex=ttl_seconds)
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
await redis.set(created_at_key, created_at, ex=ttl_seconds)
# Send notifications/tasks/created per SEP-1686 (mandatory)
@ -285,13 +289,15 @@ async def handle_resource_as_task(
task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
# Store task key mapping and creation timestamp in Redis for protocol handlers
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
created_at_key = docket.key(
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
)
ttl_seconds = int(
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
)
async with docket.redis() as redis:
await redis.set(redis_key, task_key, ex=ttl_seconds)
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
await redis.set(created_at_key, created_at, ex=ttl_seconds)
# Send notifications/tasks/created per SEP-1686 (mandatory)

View file

@ -77,10 +77,12 @@ async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskR
)
# Look up full task key and creation timestamp from Redis
redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
created_at_key = docket.key(
f"fastmcp:task:{session_id}:{client_task_id}:created_at"
)
async with docket.redis() as redis:
task_key_bytes = await redis.get(redis_key)
task_key_bytes = await redis.get(task_meta_key)
created_at_bytes = await redis.get(created_at_key)
task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
@ -176,9 +178,9 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
)
# Look up full task key from Redis
redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
async with docket.redis() as redis:
task_key_bytes = await redis.get(redis_key)
task_key_bytes = await redis.get(task_meta_key)
task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
@ -309,10 +311,12 @@ async def tasks_cancel_handler(
)
# Look up full task key and creation timestamp from Redis
redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at"
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
created_at_key = docket.key(
f"fastmcp:task:{session_id}:{client_task_id}:created_at"
)
async with docket.redis() as redis:
task_key_bytes = await redis.get(redis_key)
task_key_bytes = await redis.get(task_meta_key)
created_at_bytes = await redis.get(created_at_key)
task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")

View file

@ -101,8 +101,7 @@ async def _send_status_notification(
key_parts = parse_task_key(task_key)
session_id = key_parts["session_id"]
# Retrieve createdAt timestamp from Redis
created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at"
created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at")
async with docket.redis() as redis:
created_at_bytes = await redis.get(created_at_key)
@ -175,8 +174,7 @@ async def _send_progress_notification(
key_parts = parse_task_key(task_key)
session_id = key_parts["session_id"]
# Retrieve createdAt timestamp from Redis
created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at"
created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at")
async with docket.redis() as redis:
created_at_bytes = await redis.get(created_at_key)

8
uv.lock generated
View file

@ -752,7 +752,7 @@ requires-dist = [
{ name = "platformdirs", specifier = ">=4.0.0" },
{ name = "py-key-value-aio", extras = ["disk", "keyring", "memory"], specifier = ">=0.3.0,<0.4.0" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
{ name = "pydocket", specifier = ">=0.16.3" },
{ name = "pydocket", specifier = ">=0.16.4" },
{ name = "pyperclip", specifier = ">=1.9.0" },
{ name = "python-dotenv", specifier = ">=1.1.0" },
{ name = "rich", specifier = ">=13.9.4" },
@ -1775,7 +1775,7 @@ wheels = [
[[package]]
name = "pydocket"
version = "0.16.3"
version = "0.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cloudpickle" },
@ -1792,9 +1792,9 @@ dependencies = [
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/c6/eb7f3af72fa5c04b52a3f9390ff0c948987441987f9526dd992d2a6b3524/pydocket-0.16.4.tar.gz", hash = "sha256:d034d1ac75877560d86329fb3643e7b862fcbcdac407d876a62f5d9e386e8753", size = 297949, upload-time = "2026-01-08T21:58:31.637Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" },
{ url = "https://files.pythonhosted.org/packages/74/c5/e6ffed3902ead6cb906758749c42211f7f24ea6d8fdd772f531f5a81c9fa/pydocket-0.16.4-py3-none-any.whl", hash = "sha256:cdcdf74b987c2cd5d03c7353d15f8dd2ac9bd43f2a91f7441748d5a8ebd617c9", size = 67374, upload-time = "2026-01-08T21:58:30.01Z" },
]
[[package]]