昨天我們確立了 15 題驗收題目。今天我們要動手建立 codebase-agent 的實體專案骨架。
很多人寫 Agent 的第一個動作是 pip install langchain 或 pip install llama-index。但本系列的第一條鐵律是:在確定性分析引擎做完之前,不引入任何肥大的外部 AI 框架。
所有分析只依賴 Python 3.11+ 原生標準函式庫:
ast
sqlite3
pathlib
pytest
首先在終端機建立骨架目錄:
codebase-agent/
├─ pyproject.toml # 專案依賴宣告 (uv 管理)
├─ app/
│ ├─ __init__.py
│ ├─ codebase.py # 核心層:安全邊界、AST 解析與 SQLite 儲存
│ ├─ tools.py # 工具層:無副作用的唯讀查詢介面
│ └─ cli.py # 介面層:人類可直接執行的 CLI
├─ data/ # 本地 SQLite 索引存放區 (與目標 repo 完全隔離)
└─ tests/
└─ unit/
├─ __init__.py
└─ test_boundary.py # 核心安全邊界測試
在 pyproject.toml 中宣告極簡依賴:
[project]
name = "codebase-agent"
version = "0.1.0"
description = "Deterministic Codebase Intelligence Engine"
requires-python = ">=3.11"
dependencies = []
[dependency-groups]
dev = [
"pytest>=8.0.0",
]
app/codebase.py在分析任何程式碼前,第一件事是實作「路徑安全守門員」。我們要保證任何傳進來的路徑,經過解析後絕不可能跳脫目標專案根目錄。
"""app/codebase.py - 核心靜態分析與安全邊界引擎"""
from pathlib import Path
class RepositoryBoundaryError(Exception):
"""嘗試存取 repository 外部路徑時拋出此例外"""
pass
class CodebaseAnalyzer:
"""管理目標專案的檔案邊界、AST 解析與索引儲存"""
def __init__(self, repo_root: str):
# 取得目標 repo 的絕對路徑並解析 symlink
self.repo_root = Path(repo_root).resolve()
if not self.repo_root.exists():
raise FileNotFoundError(f"目標目錄不存在: {self.repo_root}")
def resolve_safe_path(self, relative_path: str) -> Path:
"""
將相對路徑解析為絕對路徑,並強制阻斷 Path Traversal 越界存取。
"""
# 使用 resolve() 消除內部的 '..'
target = (self.repo_root / relative_path).resolve()
# 核心防禦:確認目標路徑是否仍在 repo_root 的子樹下
if not target.is_relative_to(self.repo_root):
raise RepositoryBoundaryError(
f"安全防護觸發:路徑 '{relative_path}' 超出專案邊界!"
)
return target
app/tools.py工具層是未來提供給 LLM 進行 Tool / Function Calling 的入口。在實作具體查詢前,先將型別與規格(Signature)定義乾淨:
"""app/tools.py - 供 CLI 與 Agent 調用的唯讀工具集規格"""
from typing import List, Dict, Any
from app.codebase import CodebaseAnalyzer
class CodebaseTools:
def __init__(self, analyzer: CodebaseAnalyzer):
self.analyzer = analyzer
def read_evidence(self, relative_path: str, start_line: int, end_line: int) -> str:
"""安全讀取指定檔案的特定區間程式碼作為證據"""
safe_path = self.analyzer.resolve_safe_path(relative_path)
if not safe_path.exists():
raise FileNotFoundError(f"找不到檔案: {relative_path}")
lines = safe_path.read_text(encoding="utf-8").splitlines()
selected = lines[max(0, start_line - 1):end_line]
return "\n".join(selected)
def search_symbol(self, name: str) -> List[Dict[str, Any]]:
"""搜尋符號(下一階段串接 SQLite 實作)"""
return []
app/cli.py提供一個能直接在終端機操作的最小 CLI 骨架:
"""app/cli.py - 終端命令列介面"""
import argparse
import sys
from app.codebase import CodebaseAnalyzer
def main():
parser = argparse.ArgumentParser(description="Codebase Intelligence CLI")
parser.add_argument("--repo", default=".", help="目標 repository 路徑")
subparsers = parser.add_subparsers(dest="command")
# ping / check 指令
subparsers.add_parser("check", help="檢查目標 repository 有效性")
args = parser.parse_args()
if args.command == "check":
try:
analyzer = CodebaseAnalyzer(args.repo)
print(f"成功連結目標 Repository: {analyzer.repo_root}")
except Exception as e:
print(f"錯誤: {e}", file=sys.stderr)
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()
tests/unit/test_boundary.py在接上 LLM 之前,我們必須親手驗證 Day 3 驗收清單中的第 10 題與第 11 題(安全邊界):
"""tests/unit/test_boundary.py - 驗證路徑邊界防禦機制"""
import pytest
from app.codebase import CodebaseAnalyzer, RepositoryBoundaryError
from app.tools import CodebaseTools
def test_prevent_path_traversal(tmp_path):
"""測試越界存取(如 ../secret.txt)時必須強制拋出例外"""
repo = tmp_path / "repo"
repo.mkdir()
analyzer = CodebaseAnalyzer(str(repo))
# 嘗試跳出根目錄
with pytest.raises(RepositoryBoundaryError):
analyzer.resolve_safe_path("../secret.txt")
# 嘗試多層跳出
with pytest.raises(RepositoryBoundaryError):
analyzer.resolve_safe_path("sub/../../outside.txt")
def test_allow_valid_path(tmp_path):
"""測試合法目錄與檔案能夠被正常解析"""
repo = tmp_path / "repo"
repo.mkdir()
target_file = repo / "src" / "main.py"
target_file.parent.mkdir(parents=True)
target_file.write_text("print('hello')", encoding="utf-8")
analyzer = CodebaseAnalyzer(str(repo))
resolved = analyzer.resolve_safe_path("src/main.py")
assert resolved == target_file
def test_read_evidence_slice(tmp_path):
"""測試精準讀取指定行號程式碼"""
repo = tmp_path / "repo"
repo.mkdir()
test_file = repo / "demo.py"
test_file.write_text("line1\nline2\nline3\nline4\n", encoding="utf-8")
analyzer = CodebaseAnalyzer(str(repo))
tools = CodebaseTools(analyzer)
evidence = tools.read_evidence("demo.py", start_line=2, end_line=3)
assert evidence == "line2\nline3"
使用 uv 建立虛擬環境並執行測試:
uv run pytest tests/unit -v
今天我們完成了三件關鍵底層工作:
地基打好、測試通過。
明天在 Day 6 中,我們將在 CodebaseAnalyzer 的基礎上,實作檔案掃描過濾機制與 AST 結構化萃取,正式開始抓出程式碼裡的 Symbol!