Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
403 changes: 299 additions & 104 deletions gateway/app/routers/mcp.py

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions gateway/tests/test_adversarial_five_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
)
from tests.test_trust_helpers import create_tool_permit, provision_agent_wallet


def _error_without_request_id(error: dict) -> dict:
data = dict(error.get("data") or {})
data.pop("request_id", None)
return {**error, "data": data}


TOOL_COST = 2.0
WALLET_START = Decimal("1000") # provision_agent_wallet seeds budget_credits=1000

Expand Down Expand Up @@ -702,7 +709,9 @@ async def test_claim3_induced_timeout_stays_charged_and_never_redispatches(
assert Decimal(str(receipt["credits_charged"])) == Decimal("2")

# Replay is byte-identical and did NOT redispatch.
assert replay.json()["error"] == error
assert _error_without_request_id(
replay.json()["error"]
) == _error_without_request_id(error)
assert executor.dispatch_count == 1
assert len(executor.calls) == 1

Expand Down Expand Up @@ -761,7 +770,9 @@ async def test_claim3_pre_dispatch_death_refunds_to_net_zero_without_executing(
assert Decimal(str(receipt["credits_charged"])) == Decimal("0")

# Replay is identical and still never dispatched.
assert replay.json()["error"] == error
assert _error_without_request_id(
replay.json()["error"]
) == _error_without_request_id(error)
assert executor.dispatch_count == 0

# Debit and its refund both landed; the net effect on spend is zero.
Expand Down
13 changes: 11 additions & 2 deletions gateway/tests/test_cross_tenant_refusal_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@ async def _call_twice(client, tool_name, tenant_b, mcp_context):
"arguments": {"text": "one"},
"mcp_context": {**mcp_context, "idempotency_key": TOOL_CALL_ID},
}
headers = {**tenant_b["agent_headers"], "Idempotency-Key": TOOL_CALL_ID}
# A caller-provided correlation id is echoed into each refusal. Both calls
# carry the same one, so byte-identical refusals stay byte-identical.
headers = {
**tenant_b["agent_headers"],
"Idempotency-Key": TOOL_CALL_ID,
"X-Request-ID": "sel475-fixed-request-id",
}
url = f"/mcp/tools/{tool_name}/invoke"
first = await client.post(url, json=body, headers=headers)
replay = await client.post(url, json=body, headers=headers)
Expand Down Expand Up @@ -130,7 +136,10 @@ async def test_foreign_wallet_refusal_replays_identically(
)

_assert_refusal_envelope(first, "wallet_access_denied")
assert first.json()["detail"] == {"error": "wallet_access_denied"}
assert first.json()["detail"] == {
"error": "wallet_access_denied",
"request_id": "sel475-fixed-request-id",
}
# Same status, same headers that matter, same bytes.
assert replay.status_code == first.status_code
assert replay.headers["content-type"] == first.headers["content-type"]
Expand Down
12 changes: 10 additions & 2 deletions gateway/tests/test_delivery_uncertain_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ def _upstream_result(payload: dict[str, Any]) -> UpstreamMcpResult:
)


def _without_request_id(payload: dict[str, Any]) -> dict[str, Any]:
"""Replay identity ignores the per-call correlation id."""
error = dict(payload.get("error") or {})
data = dict(error.get("data") or {})
data.pop("request_id", None)
return {**payload, "error": {**error, "data": data}}


@dataclass
class AmbiguousExecutor:
"""Executor that always raises delivery_uncertain after the dispatch checkpoint."""
Expand Down Expand Up @@ -318,8 +326,8 @@ async def test_delivery_uncertain_replay_never_redispatches(
assert replay.status_code == 200
replay_data = replay.json()

# Should be identical
assert replay_data == first_data
# Should be identical apart from the per-call request id.
assert _without_request_id(replay_data) == _without_request_id(first_data)

# Verify NO additional dispatch
assert executor.dispatch_count == 1, "Should not redispatch on replay"
Expand Down
6 changes: 5 additions & 1 deletion gateway/tests/test_jev_risk_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ async def _setup(


async def _invoke(client, agent, permit, key="jev-invoke", *, headers=None):
merged = dict(headers or agent["agent_headers"])
# Refusals echo the caller correlation id. One fixed id keeps
# re-evaluated refusals byte-identical across first and replay.
merged.setdefault("X-Request-ID", "jev-fixed-request-id")
return await client.post(
f"/mcp/tools/{TOOL}/invoke",
json={
Expand All @@ -177,7 +181,7 @@ async def _invoke(client, agent, permit, key="jev-invoke", *, headers=None):
"idempotency_key": key,
},
},
headers=headers or agent["agent_headers"],
headers=merged,
)


Expand Down
Loading
Loading