fix(mcp): harden project context issue provider and prove shared audit path
- Share cursor decoding in foundation.decode_offset_cursor so both tools reject an invalid cursor identically, before any upstream call. - Add regression coverage that a malformed repo slug (owner/repo/extra, missing owner, empty segment) is refused before any network call. - Add evidence that BOTH project context tools inherit the shared MCP client audit record and untrusted-content fence, instead of each tool shipping its own. No audit subsystem is duplicated.
This commit is contained in:
@@ -62,6 +62,23 @@ class ProviderError(RuntimeError):
|
||||
self.retryable = retryable
|
||||
|
||||
|
||||
def decode_offset_cursor(cursor: str | None) -> int:
|
||||
"""Shared opaque-cursor decoding for every paginated provider.
|
||||
|
||||
Rejected before any backend call so an invalid cursor never costs an
|
||||
upstream request.
|
||||
"""
|
||||
if cursor is None:
|
||||
return 0
|
||||
try:
|
||||
offset = int(cursor)
|
||||
except ValueError as exc:
|
||||
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False) from exc
|
||||
if offset < 0:
|
||||
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False)
|
||||
return offset
|
||||
|
||||
|
||||
ToolHandler = Callable[[ContractModel, Any], dict[str, Any]]
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import Any, Protocol
|
||||
|
||||
import requests
|
||||
|
||||
from ..foundation import IdentityContext, ProviderError
|
||||
from ..foundation import IdentityContext, ProviderError, decode_offset_cursor
|
||||
|
||||
# ---- tunables (documented, not hardcoded secrets) -------------------------
|
||||
_REQUEST_TIMEOUT_SECONDS = 10
|
||||
@@ -203,7 +203,7 @@ class GiteaIssueProvider:
|
||||
"issue_key must be a positive work item number.",
|
||||
retryable=False,
|
||||
)
|
||||
offset = self._decode_cursor(cursor)
|
||||
offset = decode_offset_cursor(cursor)
|
||||
|
||||
payload = self._fetch_issue(issue_key)
|
||||
|
||||
@@ -256,17 +256,6 @@ class GiteaIssueProvider:
|
||||
}
|
||||
|
||||
# ---- internals ---------------------------------------------------
|
||||
def _decode_cursor(self, cursor: str | None) -> int:
|
||||
if cursor is None:
|
||||
return 0
|
||||
try:
|
||||
offset = int(cursor)
|
||||
except ValueError as exc:
|
||||
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False) from exc
|
||||
if offset < 0:
|
||||
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False)
|
||||
return offset
|
||||
|
||||
def _build_description(self, body: str, detail: str) -> str:
|
||||
text = body.strip()
|
||||
if detail == "summary":
|
||||
|
||||
Reference in New Issue
Block a user