From 739f315fb9ed8a138d808f1d5001c6cf0040eeda Mon Sep 17 00:00:00 2001 From: Wany-i <322708540+Wany-i@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:51:28 +0800 Subject: [PATCH] fix: mark blocked invokes as unexecuted --- CHANGELOG.md | 6 +++ pyproject.toml | 2 +- src/taskgateway/invocation.py | 2 +- src/taskgateway/mcp_server.py | 2 +- tests/test_core.py | 71 ++++++++++++++++++++++++++++++++++- tests/test_mcp_server.py | 1 + 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e94d5dd..d64a4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and ## [Unreleased] +## [1.0.1] - 2026-09-20 + +### Fixed + +- Blocked `invoke` responses now explicitly report `executed: false`. + ## [1.0.0] - 2026-09-20 ### Added diff --git a/pyproject.toml b/pyproject.toml index 499dfad..e48bc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "taskgateway-local" -version = "1.0.0" +version = "1.0.1" description = "Dependency-free TaskGateway core with an optional MCP SDK v2 STDIO server" readme = "README.md" license = "MIT" diff --git a/src/taskgateway/invocation.py b/src/taskgateway/invocation.py index 96c7849..4344093 100644 --- a/src/taskgateway/invocation.py +++ b/src/taskgateway/invocation.py @@ -45,7 +45,7 @@ def _blocked( "response": { "status": "blocked", "request_id": request_id, - "data": {}, + "data": {"executed": False}, "errors": [{"code": code, "message": message, "retryable": False}], "warnings": [], "side_effects": [], diff --git a/src/taskgateway/mcp_server.py b/src/taskgateway/mcp_server.py index e148050..ee28059 100644 --- a/src/taskgateway/mcp_server.py +++ b/src/taskgateway/mcp_server.py @@ -20,7 +20,7 @@ def create_server(gateway: TaskGateway) -> MCPServer: """Create a v2 MCP server bound to one configured gateway.""" server = MCPServer( name="taskgateway", - version="1.0.0", + version="1.0.1", description="Read-only search, planning, and fail-closed invocation rendering.", instructions="invoke renders commands only; it never executes them.", ) diff --git a/tests/test_core.py b/tests/test_core.py index 572a718..f33b731 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,8 +1,12 @@ from __future__ import annotations +import json from pathlib import Path +import pytest + from taskgateway import TaskGateway +from taskgateway.invocation import decision_id_for def test_search_plan_invoke_preserve_legacy_contract(fixture_index: Path) -> None: @@ -41,4 +45,69 @@ def test_invoke_invalid_decision_id_is_fail_closed(fixture_index: Path) -> None: assert result["response"]["status"] == "blocked" assert result["response"]["errors"][0]["code"] == "DECISION_UNKNOWN" - assert result["response"]["data"] == {} + assert result["response"]["data"]["executed"] is False + + +@pytest.mark.parametrize( + ("resource_ref", "decision_id", "operation", "code"), + [ + ("tool://readme", "invalid", "render_call", "DECISION_UNKNOWN"), + ( + "tool://missing", + decision_id_for("tool://missing", "read_only"), + "render_call", + "NOT_FOUND", + ), + ( + "tool://disabled", + decision_id_for("tool://disabled", "read_only"), + "render_call", + "GATE_DENIED", + ), + ( + "tool://readme", + decision_id_for("tool://readme", "read_only"), + "execute", + "REQUEST_INVALID", + ), + ], +) +def test_blocked_invoke_returns_executed_false( + fixture_index: Path, + resource_ref: str, + decision_id: str, + operation: str, + code: str, +) -> None: + gateway = TaskGateway(index_path=fixture_index) + + result = gateway.invoke(resource_ref, decision_id, operation) + + assert result["response"]["status"] == "blocked" + assert result["response"]["errors"][0]["code"] == code + assert result["response"]["data"]["executed"] is False + + +def test_path_missing_invoke_returns_executed_false(fixture_index: Path) -> None: + payload = json.loads(fixture_index.read_text(encoding="utf-8")) + payload["items"].append( + { + "ref": "tool://missing-path", + "type": "tool", + "name": "missing path tool", + "path": str(fixture_index.parent / "missing.md"), + "usage": "never", + "disabled": False, + } + ) + fixture_index.write_text(json.dumps(payload), encoding="utf-8") + gateway = TaskGateway(index_path=fixture_index) + + result = gateway.invoke( + "tool://missing-path", + decision_id_for("tool://missing-path", "read_only"), + ) + + assert result["response"]["status"] == "blocked" + assert result["response"]["errors"][0]["code"] == "PATH_MISSING" + assert result["response"]["data"]["executed"] is False diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index fc50620..4cd50f9 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -71,6 +71,7 @@ def test_mcp_invoke_invalid_decision_id_is_blocked(fixture_index: Path) -> None: payload = result.structured_content assert payload["response"]["status"] == "blocked" assert payload["response"]["errors"][0]["code"] == "DECISION_UNKNOWN" + assert payload["response"]["data"]["executed"] is False def test_mcp_plan_denies_budget_outside_read_only(fixture_index: Path) -> None: