fix(git-gate): review — sandbox naming, ResolverLike Protocol, token-required note

Addresses PR #365 review:
- Type `resolve_sandbox_root`'s resolver param as a `ResolverLike` Protocol
  (structural, `resolve_bottle_id` only) — fixes the pyright errors from
  passing a duck-typed fake resolver in tests; mirrors
  egress_addon_core.PolicyResolverLike.
- Rename `_project_root`/`project_root`/`resolve_repo_root`/
  `DEFAULT_PROJECT_ROOT` -> `_sandbox_root`/`sandbox_root`/
  `resolve_sandbox_root`/`DEFAULT_REPO_ROOT`: "sandbox" names the per-tenant
  scope; "project" was too amorphous. `GIT_PROJECT_ROOT` keeps git's own
  env-var name.
- Note the single-tenant path is transitional (stripped once every backend
  runs the consolidated gateway).
- policy_resolver: document that the optional identity token is
  transitional — the consolidated end state requires it (flips at the
  /resolve boundary once token *delivery* lands, a PRD 0070 open question).
- Split the long line in main() (was 105 cols).

pyright 0 errors; pylint 9.95/10; unit suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-13 18:47:53 -04:00
parent fd7448c6a2
commit 71f40fe528
3 changed files with 59 additions and 34 deletions
+9 -9
View File
@@ -1,4 +1,4 @@
"""Unit: resolve_repo_root — source-IP-keyed git-gate repo namespace (PRD 0070).
"""Unit: resolve_sandbox_root — source-IP-keyed git-gate repo namespace (PRD 0070).
The consolidated git-http backend serves every bottle from one process and
selects each request's repo root by the calling bottle's source IP. This is
@@ -10,7 +10,7 @@ from __future__ import annotations
import unittest
from pathlib import Path
from bot_bottle.git_http_backend import resolve_repo_root
from bot_bottle.git_http_backend import resolve_sandbox_root
from bot_bottle.policy_resolver import PolicyResolveError
_BASE = Path("/git")
@@ -32,32 +32,32 @@ class _FakeResolver:
class TestResolveRepoRoot(unittest.TestCase):
def test_single_tenant_passthrough(self) -> None:
# No resolver → the flat base root, unchanged (legacy per-bottle mode).
self.assertEqual(_BASE, resolve_repo_root(None, _BASE, "10.243.0.1"))
self.assertEqual(_BASE, resolve_sandbox_root(None, _BASE, "10.243.0.1"))
def test_attributed_bottle_gets_namespaced_root(self) -> None:
root = resolve_repo_root(_FakeResolver(bottle_id="ab12cd34"), _BASE, "10.243.0.1")
root = resolve_sandbox_root(_FakeResolver(bottle_id="ab12cd34"), _BASE, "10.243.0.1")
self.assertEqual(Path("/git/ab12cd34"), root)
def test_unattributed_denies(self) -> None:
self.assertIsNone(resolve_repo_root(_FakeResolver(bottle_id=None), _BASE, "10.243.0.9"))
self.assertIsNone(resolve_sandbox_root(_FakeResolver(bottle_id=None), _BASE, "10.243.0.9"))
def test_empty_bottle_id_denies(self) -> None:
self.assertIsNone(resolve_repo_root(_FakeResolver(bottle_id=""), _BASE, "10.243.0.1"))
self.assertIsNone(resolve_sandbox_root(_FakeResolver(bottle_id=""), _BASE, "10.243.0.1"))
def test_resolver_error_denies(self) -> None:
# Orchestrator unreachable/errored must never serve repos.
self.assertIsNone(resolve_repo_root(_FakeResolver(raises=True), _BASE, "10.243.0.1"))
self.assertIsNone(resolve_sandbox_root(_FakeResolver(raises=True), _BASE, "10.243.0.1"))
def test_namespace_escape_denies(self) -> None:
# A bottle_id that would climb out of the root is rejected (defense in
# depth — registry ids are token_hex, but never trust the namespace).
self.assertIsNone(
resolve_repo_root(_FakeResolver(bottle_id="../etc"), _BASE, "10.243.0.1")
resolve_sandbox_root(_FakeResolver(bottle_id="../etc"), _BASE, "10.243.0.1")
)
def test_forwards_source_ip_and_token(self) -> None:
r = _FakeResolver(bottle_id="b1")
resolve_repo_root(r, _BASE, "10.243.0.1", "tok")
resolve_sandbox_root(r, _BASE, "10.243.0.1", "tok")
self.assertEqual(("10.243.0.1", "tok"), r.calls[0])