fix: casefold repo-binding keys to avoid spurious cross-repo mismatch

GitHub owner/name are case-insensitive. Casefold the owner/name key on both the
write (binding) and read (compare) sides — repo_cache_key and the metadata
bound_repo read — so Org/Repo and org/repo resolve to one repo and a legitimate
same-repo run cannot raise a spurious SandboxRepoMismatchError (Gap 2).
This commit is contained in:
Adam Moussa 2026-06-29 12:10:44 -04:00
parent d880cdabb3
commit ebeb2663fd
No known key found for this signature in database
2 changed files with 12 additions and 4 deletions

View file

@ -28,15 +28,20 @@ class GitHubAuthError(Exception):
def repo_cache_key(repo: Any) -> str | None:
"""Normalize a repo dict/string to ``owner/name`` (None when unknown)."""
"""Normalize a repo dict/string to a casefolded ``owner/name`` (None if unknown).
GitHub owner/name are case-insensitive, so the key is casefolded on both the
write (binding) and read (compare) sides to keep ``Org/Repo`` and ``org/repo``
a single repo and avoid spurious cross-repo mismatches.
"""
if isinstance(repo, str):
cleaned = repo.strip()
return cleaned or None
return cleaned.casefold() or None
if isinstance(repo, Mapping):
owner = repo.get("owner")
name = repo.get("name")
if isinstance(owner, str) and isinstance(name, str) and owner and name:
return f"{owner}/{name}"
return f"{owner}/{name}".casefold()
return None

View file

@ -19,6 +19,7 @@ from deepagents.backends.protocol import (
)
from langgraph.config import get_config
from .github_token import repo_cache_key
from .sandbox import create_sandbox
logger = logging.getLogger(__name__)
@ -167,7 +168,9 @@ async def get_bound_repo_from_metadata(thread_id: str) -> str | None:
if not isinstance(metadata, dict):
return None
bound_repo = metadata.get("bound_repo")
return bound_repo if isinstance(bound_repo, str) and bound_repo else None
# Casefold on read so legacy metadata written before normalization (e.g.
# ``Org/Repo``) still compares equal to the casefolded current repo key.
return repo_cache_key(bound_repo) if isinstance(bound_repo, str) and bound_repo else None
def clear_sandbox_backend(thread_id: str) -> None: