Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions docs/MCP_TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ cgc mcp # or configure mcp.json with the same env

Without this variable, sibling directories outside the server cwd are rejected for security.

To disable the sandbox entirely (e.g. containerized setups or dynamic multi-repository
indexing where maintaining `CGC_ALLOWED_ROOTS` is impractical), set `CGC_ALLOW_ALL_PATHS=true`
or `CGC_ALLOWED_ROOTS=*`. This permits any valid filesystem path. Sandboxing stays on by
default; only enable this when you trust every path the server will be asked to touch.

---

## Context management
Expand Down
18 changes: 16 additions & 2 deletions src/codegraphcontext/utils/path_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get_allowed_roots() -> List[Path]:
roots: List[Path] = [Path.cwd().resolve()]

env_roots = os.environ.get("CGC_ALLOWED_ROOTS", "")
if env_roots:
if env_roots and env_roots != "*":
separator = ";" if os.name == "nt" else ":"
for entry in env_roots.split(separator):
entry = entry.strip()
Expand All @@ -33,8 +33,22 @@ def get_allowed_roots() -> List[Path]:
return roots


def is_path_sandbox_disabled() -> bool:
"""True when path sandboxing is explicitly disabled.

Set ``CGC_ALLOW_ALL_PATHS=true`` or ``CGC_ALLOWED_ROOTS=*`` to permit
indexing, watching, and managing any filesystem path. Disabled by
default so sandboxing stays on unless a user opts in.
"""
if os.environ.get("CGC_ALLOW_ALL_PATHS", "").strip().lower() in ("1", "true", "yes"):
return True
return os.environ.get("CGC_ALLOWED_ROOTS", "").strip() == "*"


def is_path_allowed(path: Path) -> bool:
"""True when *path* resolves under an allowed root."""
"""True when *path* resolves under an allowed root, or sandboxing is disabled."""
if is_path_sandbox_disabled():
return True
resolved = path.resolve()
for root in get_allowed_roots():
try:
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/utils/test_path_sandbox_security.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from pathlib import Path

import pytest

from codegraphcontext.utils.path_sandbox import (
clamp_discovery_depth,
is_path_allowed,
is_safe_download_url,
sanitize_bundle_filename,
)
Expand Down Expand Up @@ -50,3 +53,33 @@ def test_clamp_discovery_depth():
assert clamp_discovery_depth(1000) == 10
assert clamp_discovery_depth(-3) == 0
assert clamp_discovery_depth("2") == 2


def test_is_path_allowed_rejects_outside_root_by_default(tmp_path, monkeypatch):
monkeypatch.delenv("CGC_ALLOW_ALL_PATHS", raising=False)
monkeypatch.delenv("CGC_ALLOWED_ROOTS", raising=False)
outside = tmp_path / "outside"
outside.mkdir()
assert not is_path_allowed(outside)


def test_is_path_allowed_bypasses_sandbox_with_allow_all_paths(tmp_path, monkeypatch):
monkeypatch.delenv("CGC_ALLOWED_ROOTS", raising=False)
monkeypatch.setenv("CGC_ALLOW_ALL_PATHS", "true")
outside = tmp_path / "outside"
outside.mkdir()
assert is_path_allowed(outside)


def test_is_path_allowed_bypasses_sandbox_with_wildcard_allowed_roots(tmp_path, monkeypatch):
monkeypatch.delenv("CGC_ALLOW_ALL_PATHS", raising=False)
monkeypatch.setenv("CGC_ALLOWED_ROOTS", "*")
outside = tmp_path / "outside"
outside.mkdir()
assert is_path_allowed(outside)


def test_is_path_allowed_still_allows_cwd_by_default(monkeypatch):
monkeypatch.delenv("CGC_ALLOW_ALL_PATHS", raising=False)
monkeypatch.delenv("CGC_ALLOWED_ROOTS", raising=False)
assert is_path_allowed(Path.cwd())
Loading