Skip to content

fix: guard_sync wraps TimeoutError, cancels pending future on timeout#43

Closed
beonde wants to merge 4 commits into
mainfrom
fix/dx-audit-guard-sync
Closed

fix: guard_sync wraps TimeoutError, cancels pending future on timeout#43
beonde wants to merge 4 commits into
mainfrom
fix/dx-audit-guard-sync

Conversation

@beonde
Copy link
Copy Markdown
Member

@beonde beonde commented May 22, 2026

DX Audit: guard_sync Error Handling

Discovered during developer experience audit — guard_sync() let raw TimeoutError escape and leaked futures.

Changes

  • guard_sync() wraps TimeoutError as GuardError with a descriptive message including the timeout value
  • Cancels the pending future on timeout to prevent resource leaks
  • Version bump to 2.7.2

2 files, +13/-2

beonde added 3 commits May 21, 2026 12:25
…sage

TimeoutError from concurrent.futures was leaking to users when guard_sync
was called outside MCP context. Now catches it and raises GuardError with
a helpful message explaining the likely cause.
Copilot AI review requested due to automatic review settings May 22, 2026 02:01
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the developer experience of the synchronous guard decorator (guard_sync) by converting timeout failures into a consistent GuardError and attempting to prevent leaked pending futures, alongside a patch version bump.

Changes:

  • Wrap future.result(timeout=30.0) timeouts in guard_sync() as GuardError and cancel the pending future on timeout.
  • Bump package version in pyproject.toml from 2.7.1 to 2.7.2.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
capiscio_mcp/guard.py Adds timeout handling around run_coroutine_threadsafe(...).result() including future cancellation and a GuardError wrapper.
pyproject.toml Updates the distribution version to 2.7.2.
Comments suppressed due to low confidence (3)

capiscio_mcp/guard.py:702

  • The timeout detail currently suggests the guard is being called outside an MCP request context, but in this branch the more likely cause is calling guard_sync while an event loop is already running in the current thread (future.result blocks the loop, so it can never complete). Consider updating the message to explicitly instruct using the async @guard (or running the sync tool in a worker thread) when already in an async context.
                            "Guard evaluation timed out. This usually means the "
                            "guard is being called outside of an MCP request context "
                            "or the gRPC core server is not responding."
                        ),
                    )

capiscio_mcp/guard.py:697

  • This wraps the timeout into GuardError, but it drops the original exception context. Capturing the exception as as e and re-raising with from e would preserve the cause for debugging while still presenting a GuardError to callers.
                except (TimeoutError, concurrent.futures.TimeoutError):
                    future.cancel()
                    raise GuardError(
                        reason=DenyReason.INTERNAL_ERROR,
                        detail=(

capiscio_mcp/guard.py:702

  • New behavior on timeout (cancel pending future + raise GuardError) isn’t covered by tests. Consider adding a unit test that mocks asyncio.run_coroutine_threadsafe(...).result() to raise a timeout and asserts future.cancel() is called and GuardError is raised with the expected reason/detail.
                try:
                    result = future.result(timeout=30.0)
                except (TimeoutError, concurrent.futures.TimeoutError):
                    future.cancel()
                    raise GuardError(
                        reason=DenyReason.INTERNAL_ERROR,
                        detail=(
                            "Guard evaluation timed out. This usually means the "
                            "guard is being called outside of an MCP request context "
                            "or the gRPC core server is not responding."
                        ),
                    )

Comment thread capiscio_mcp/guard.py Outdated
Comment on lines +691 to +700
try:
result = future.result(timeout=30.0)
except (TimeoutError, concurrent.futures.TimeoutError):
future.cancel()
raise GuardError(
reason=DenyReason.INTERNAL_ERROR,
detail=(
"Guard evaluation timed out. This usually means the "
"guard is being called outside of an MCP request context "
"or the gRPC core server is not responding."
Comment thread pyproject.toml
Comment on lines 6 to 8
name = "capiscio-mcp"
version = "2.7.1"
version = "2.7.2"
description = "Trust badges for MCP tool calls - RFC-006 & RFC-007 implementation"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is intentional — pyproject.toml version tracks the package distribution version (aligned with the capiscio ecosystem release), while MCP_VERSION is the internal MCP protocol version constant. They serve different purposes and are expected to diverge.

Extract timeout to named constant and interpolate into error detail
so callers can see how long guard_sync waited.
@beonde
Copy link
Copy Markdown
Member Author

beonde commented May 22, 2026

Closing: duplicate of already-merged #41. Branch was erroneously re-pushed.

@beonde beonde closed this May 22, 2026
@beonde beonde deleted the fix/dx-audit-guard-sync branch May 22, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants