Skip to content
Closed
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
39 changes: 27 additions & 12 deletions engine/antigravity_engine/hub/ask_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pathlib import Path

from antigravity_engine.hub._constants import SKIP_DIRS
from antigravity_engine.hub._utils import is_safe_path
from antigravity_engine.hub.contracts import (
ClaimVerification,
ModuleClaim,
Expand Down Expand Up @@ -486,6 +487,7 @@ def _load_project_context(
per_source_cap = max(1000, max_chars // 3)
budget = max_chars
parts: list[str] = []
workspace = ag_dir.parent

def _push(label: str, content: str) -> None:
nonlocal budget
Expand All @@ -495,19 +497,35 @@ def _push(label: str, content: str) -> None:
parts.append(f"### {label}\n{chunk}")
budget -= len(chunk)

def _read_project_doc(path: Path) -> str:
"""Read a bounded project doc only when it resolves inside workspace."""
if budget <= 0:
return ""
limit = min(per_source_cap, budget)
try:
resolved = path.resolve()
except OSError:
return ""
if not is_safe_path(workspace, resolved):
return ""
try:
if not resolved.is_file():
return ""
with resolved.open("r", encoding="utf-8", errors="replace") as f:
return f.read(limit)
except OSError:
return ""

file_sources: list[tuple[str, Path]] = [
("Project Conventions (.antigravity/conventions.md)", ag_dir / "conventions.md"),
("Document Index (.antigravity/document_index.md)", ag_dir / "document_index.md"),
]
for label, path in file_sources:
if budget <= 0:
break
if not path.is_file():
continue
try:
_push(label, path.read_text(encoding="utf-8"))
except OSError:
continue
content = _read_project_doc(path)
if content:
_push(label, content)

if budget > 0 and map_content.strip():
_push("Module Map (.antigravity/map.md)", map_content)
Expand All @@ -519,12 +537,9 @@ def _push(label: str, content: str) -> None:
for label, path in file_sources_after_map:
if budget <= 0:
break
if not path.is_file():
continue
try:
_push(label, path.read_text(encoding="utf-8"))
except OSError:
continue
content = _read_project_doc(path)
if content:
_push(label, content)

if not parts:
return ""
Expand Down
14 changes: 14 additions & 0 deletions engine/tests/test_hub_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,20 @@ def test_load_project_context_respects_total_budget(tmp_path: Path) -> None:
assert "REGISTRY_MARKER" in section


def test_load_project_context_skips_symlink_outside_workspace(tmp_path: Path) -> None:
"""Symlinked project docs resolving outside workspace must be ignored."""
from antigravity_engine.hub.ask_pipeline import _load_project_context

ag_dir = tmp_path / ".antigravity"
ag_dir.mkdir()
outside = tmp_path.parent / "outside-secret.txt"
outside.write_text("SECRET_TOKEN_OUTSIDE\n", encoding="utf-8")
(ag_dir / "conventions.md").symlink_to(outside)

section = _load_project_context(ag_dir, map_content="")
assert section == ""


# ---------------------------------------------------------------------------
# Phase 1: config/entry/git in _format_scan_report
# ---------------------------------------------------------------------------
Expand Down