diff --git a/CHANGELOG.md b/CHANGELOG.md index d6aee90d..d7b8e7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -786,6 +786,10 @@ All notable changes to this project are documented here. The format follows ### Fixed +- **`continuum_confirm` hid handler refusals (#371).** `confirm_gate` now + invokes the handler inside `_refusal_reaches_the_caller`, so domain errors + such as a missing run retain their useful `ToolError` message. + - **JSON booleans passed every integer check in the budget registry (#429).** `isinstance(True, int)` holds in Python, so `true` was accepted wherever `.continuum/budgets.json` requires a positive integer: as diff --git a/src/continuum/mcp/server.py b/src/continuum/mcp/server.py index 17949a83..91fe3d8a 100644 --- a/src/continuum/mcp/server.py +++ b/src/continuum/mcp/server.py @@ -661,7 +661,7 @@ def wrapper(*args: Any, ctx: Context | None = None, **kwargs: Any) -> str: with _refusal_reaches_the_caller(): confirm_auth.verify(token_from(ctx)) policy.require(caller, fn.__name__) - return fn(*args, **kwargs) + return fn(*args, **kwargs) # Same fix-up as ``guard``: re-advertise the context parameter or the # SDK never hands us one and every caller looks tokenless. diff --git a/tests/test_mcp_authz.py b/tests/test_mcp_authz.py index 31f3242c..08dbd106 100644 --- a/tests/test_mcp_authz.py +++ b/tests/test_mcp_authz.py @@ -879,3 +879,21 @@ async def test_a_validation_refusal_also_carries_its_reason(store: SQLiteStorage {"run_id": "r", "completed": 999, "total": 10, "goal": "g"}, context=fake_context(ALLOWED), ) + + +@pytest.mark.asyncio +async def test_confirm_handler_refusal_carries_its_reason(store: SQLiteStorage) -> None: + from mcp.server.mcpserver.exceptions import ToolError + + server, _ = build_server( + storage=store, + policy=AuthorizationPolicy([ALLOWED]), + confirm_auth=ConfirmPolicy("confirm-secret"), + ) + + with pytest.raises(ToolError, match="no such run: 'missing'"): + await server.call_tool( + "continuum_confirm", + {"run_id": "missing"}, + context=fake_context(ALLOWED, auth_token="confirm-secret"), + )