Skip to content

Commit 10e985b

Browse files
committed
code-review-skill
1 parent 73655ab commit 10e985b

67 files changed

Lines changed: 9293 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Required model configuration
2+
TRPC_AGENT_API_KEY=your-api-key
3+
TRPC_AGENT_BASE_URL=https://your-model-service.example/v1
4+
TRPC_AGENT_MODEL_NAME=your-model-name
5+
# Optional comma-separated provider host allowlist.
6+
TRPC_AGENT_ALLOWED_MODEL_HOSTS=your-model-service.example
7+
8+
# Optional; the image is built from sandbox/Dockerfile by default
9+
CODE_REVIEW_SANDBOX_BACKEND=docker
10+
CODE_REVIEW_DOCKER_IMAGE=skills-code-review-agent:latest
11+
CODE_REVIEW_DOCKER_MEMORY_BYTES=536870912
12+
CODE_REVIEW_DOCKER_NANO_CPUS=1000000000
13+
CODE_REVIEW_DOCKER_PIDS_LIMIT=256
14+
CODE_REVIEW_DOCKER_TMPFS_BYTES=268435456
15+
16+
# Persistence backend selection
17+
CODE_REVIEW_STORAGE_BACKEND=sqlite
18+
CODE_REVIEW_SQLITE_PATH=storage/reviews.sqlite3
19+
# Optional compatible SQLite schema override
20+
CODE_REVIEW_SQLITE_SCHEMA_PATH=storage/schema.sql
21+
22+
# Sandbox policy ceilings; values may be tightened but not raised above defaults
23+
CODE_REVIEW_MAX_TIMEOUT_SECONDS=120
24+
CODE_REVIEW_MAX_OUTPUT_BYTES=15360
25+
CODE_REVIEW_MAX_SANDBOX_RUNS=12
26+
CODE_REVIEW_TOTAL_TIMEOUT_SECONDS=110
27+
CODE_REVIEW_MAX_TOOL_CALLS=30
28+
# Unit tests execute untrusted repository code, so this is opt-in.
29+
CODE_REVIEW_ALLOW_REPOSITORY_EXECUTION=false
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## .env
2+
Plan.md
3+
.env
4+
5+
## runtime output
6+
__pycache__/
7+
*.py[cod]
8+
storage/reviews.sqlite3
9+
storage/reviews.sqlite3-*
10+
reports/output/
11+
.pytest_cache/
12+
.ruff_cache/
13+
.mypy_cache/
14+
15+
## AGENTS.md
16+
AGENTS.md
17+
18+
## uv
19+
.venv/
20+
.python-version
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# 基于 Skill 的代码审查 Agent
2+
3+
本示例提供自动代码审查 Agent 的最小框架:Workflow 只负责输入、调用、校验、落库和报告等确定性步骤;Agent 负责判断是否需要 Skill 和沙箱检查。`code-review` Skill 提供规则和脚本,结果通过可替换存储层持久化,并生成 JSON 与 Markdown 报告。
4+
5+
## 目录结构
6+
7+
```text
8+
skills_code_review_agent/
9+
├── run_agent.py # 主要入口
10+
├── workflow.py # 审查流程编排
11+
├── docs/design.md # 方案设计说明
12+
├── agent/
13+
│ ├── agent.py # LlmAgent 构建
14+
│ ├── config.py # 模型配置
15+
│ ├── fake.py # 确定性 fake model
16+
│ ├── normalization.py # 去重、降噪和脱敏
17+
│ ├── prompts.py # 审查 Prompt
18+
│ └── tools.py # SkillToolSet 与沙箱连接
19+
├── inputs/ # diff、file list、worktree、fixture 输入
20+
├── filters/ # 命令策略和 SDK Tool Filter
21+
├── skills/code-review/
22+
│ ├── SKILL.md # Skill 入口
23+
│ ├── agents/openai.yaml # Skill UI 元数据
24+
│ ├── references/RULES.md # 审查规则
25+
│ └── scripts/ # 输入解析、受控读取及分类审查脚本
26+
├── sandbox/
27+
│ ├── base.py # 可替换沙箱接口
28+
│ ├── factory.py # 环境变量驱动的实现选择
29+
│ ├── docker.py # Docker 实现
30+
│ ├── lazy.py # 按工具调用惰性创建 runtime
31+
│ ├── fake.py # 不执行代码的测试模拟器
32+
│ ├── .dockerignore # 最小化镜像构建上下文
33+
│ └── Dockerfile # 最小审查镜像
34+
├── storage/
35+
│ ├── base.py # BaseReviewStore 抽象基类
36+
│ ├── factory.py # 环境变量驱动的实现选择
37+
│ ├── schema.sql # 显式 SQLite schema
38+
│ └── sqlite.py # SQLite 实现
39+
├── reports/
40+
│ ├── models.py # 结构化审查模型
41+
│ └── writers.py # JSON/Markdown 输出
42+
├── tests/fixtures/ # 8 条要求样本及超时补充样本
43+
├── tests/run_tests.py # 非 Docker 验收测试入口
44+
├── tests/run_docker_tests.py # tRPC Container runtime 集成测试
45+
├── tests/evaluate_fixtures.py # 公开 fixture 指标评测
46+
└── examples/review_report.* # 示例报告
47+
```
48+
49+
## 运行要求
50+
51+
- Python 3.10+
52+
- 已按仓库根目录说明安装 `trpc-agent-python` 及其现有依赖
53+
- fake/dry-run 不需要 Docker 或模型 API Key
54+
- 真实模式需要 Docker daemon,以及模型环境变量
55+
56+
远程模型地址必须使用 HTTPS;仅 `localhost``127.0.0.1``::1`
57+
允许使用 HTTP,便于连接本地开发模型服务。
58+
生产环境建议设置 `TRPC_AGENT_ALLOWED_MODEL_HOSTS`,限制可接收 API Key
59+
和审查证据的模型服务域名。
60+
61+
本示例不额外依赖 `.env` 解析库。入口只读取示例目录下权限为 `0600`
62+
键名前缀为 `TRPC_AGENT_``CODE_REVIEW_` 的普通文件;同名进程变量优先:
63+
64+
```bash
65+
cp examples/skills_code_review_agent/.env.example \
66+
examples/skills_code_review_agent/.env
67+
chmod 600 examples/skills_code_review_agent/.env
68+
```
69+
70+
## 输入与运行方式
71+
72+
所有命令从仓库根目录执行。默认审查 Git 工作区变更:
73+
74+
```bash
75+
uv run --project examples/skills_code_review_agent --with-editable . \
76+
python examples/skills_code_review_agent/run_agent.py \
77+
--repo-path /path/to/repository
78+
```
79+
80+
其他输入:
81+
82+
```bash
83+
# unified diff / PR patch
84+
uv run --project examples/skills_code_review_agent --with-editable . \
85+
python examples/skills_code_review_agent/run_agent.py --diff-file change.patch
86+
87+
# 文件路径列表;真实模式同时提供列表所属仓库
88+
uv run --project examples/skills_code_review_agent --with-editable . \
89+
python examples/skills_code_review_agent/run_agent.py \
90+
--repo-path /path/to/repository --file-list /path/to/repository/files.txt
91+
92+
# 内置 fixture,无模型、无 Docker
93+
uv run --project examples/skills_code_review_agent --with-editable . \
94+
python examples/skills_code_review_agent/run_agent.py \
95+
--fixture security --fake-model
96+
```
97+
98+
`--dry-run` 同样走确定性 fake 链路,仍执行解析、Filter、sandbox 模拟、落库和报告生成,但不执行任何宿主或容器命令。省略输入时从当前工作目录向上查找最近的 Git worktree,并仅审查其变更;全仓库审查必须显式添加 `--full`
99+
100+
unified diff 解析结果保留每个 hunk 的 added、removed、unchanged context
101+
行、old/new 双侧行号和候选变更行号,生命周期规则可利用未修改上下文降噪。
102+
103+
## 输出和持久化
104+
105+
默认输出位置:
106+
107+
- SQLite:`storage/reviews.sqlite3`
108+
- JSON:`reports/output/<task-id>/review_report.json`
109+
- Markdown:`reports/output/<task-id>/review_report.md`
110+
111+
持久化默认由以下环境变量选择:
112+
113+
```bash
114+
CODE_REVIEW_STORAGE_BACKEND=sqlite
115+
CODE_REVIEW_SQLITE_PATH=storage/reviews.sqlite3
116+
CODE_REVIEW_SQLITE_SCHEMA_PATH=storage/schema.sql
117+
```
118+
119+
当前只实现 `sqlite`;新增后端应继承 `BaseReviewStore` 并在 `storage/factory.py` 注册。`--database` 的优先级高于 `CODE_REVIEW_SQLITE_PATH`
120+
`CODE_REVIEW_SQLITE_SCHEMA_PATH` 可选择兼容的 SQLite 初始化 schema;替换文件必须保留存储实现使用的表和字段契约,并且只能使用 `storage/` 下的普通文件。schema 有大小限制,初始化时禁止 attach、trigger、view、虚拟表、删除对象和业务数据写入。
121+
122+
SQLite 分表保存 `review_tasks``review_inputs``sandbox_runs``filter_decisions``findings``monitoring_summaries``review_reports``get_task_details(task_id)` 可查询完整审计记录。任务在 Agent 启动前以 `running` 状态落库,异常终止会更新为 `failed`。SQLite 启用 WAL、等待锁和 digest/profile 索引。对于内容不可变的 diff/fixture,缓存必须同时匹配输入摘要、规则、Skill、模式、模型和审查范围;是否复用仍由 Agent 决定。
123+
124+
沙箱实现由 `CODE_REVIEW_SANDBOX_BACKEND=docker` 选择,当前仅提供 Docker;新增实现需满足 `SandboxProvider` 并在 `sandbox/factory.py` 注册。可通过 `--output-dir``--docker-image` 覆盖输出目录和镜像。Docker runtime 按 Agent 的 workspace 工具调用惰性创建;代码只读挂载,diff/fixture 仅挂载任务级副本。容器禁网、非 root、删除 capabilities、启用 `no-new-privileges` 和只读根文件系统,并限制 CPU、内存、PID 与 tmpfs。模型服务仍由宿主进程调用,因此应使用符合代码数据策略的模型服务。
125+
126+
## 安全和治理
127+
128+
- 真实执行只通过加固 Docker;fake sandbox 不执行代码。报告目录和 SQLite 使用仅当前用户可读写权限。
129+
- diff 任务副本使用仅当前用户可读权限;SQLite 在首次连接前以 `0600` 安全创建,并拒绝符号链接路径。
130+
- unified diff 和 Git staged/unstaged diff 均通过聚合脚本调用安全、异步、资源、数据库、测试和敏感信息六个独立规则;结果按最多 24 条记录分页,避免 SDK 的 16KB inline 上限截断 JSON。
131+
- 文件列表和受控文件读取同样分页;路径长度、数量、敏感文件和符号链接在容器内再次校验。Git 工作区的直接读取还会重新验证路径属于 changed 或 full scope,避免模型读取未选择文件。
132+
- `skill_run` 必须先完成 `skill_load`。前置 Filter 同时检查输入模式、命令、脚本、Git 参数、路径、网络、环境变量和预算;`deny``needs_human_review` 不进入沙箱。`compileall` 只做有界语法编译;`unittest``pytest` 会执行不受信任的仓库代码,默认进入人工复核。仅在确认仓库与挂载内容可信后,才可设置 `CODE_REVIEW_ALLOW_REPOSITORY_EXECUTION=true` 显式放行。
133+
- 单次 Skill run 默认 30 秒;整次 review 默认 110 秒、30 次工具调用和 12 次 sandbox run。所有限制均可通过 `.env` 中的 `CODE_REVIEW_*` 字段收紧。
134+
- Workflow 可信地记录每个脚本的 cursor;缺少必需的 staged、unstaged、文件枚举或受控读取证据,或者任一 `next_cursor` 未读完时,报告会强制加入人工复核项。
135+
- 代码、注释和工具输出均按不可信数据处理;Filter 阻止外部 diff helper、敏感路径和跨输入模式读取。
136+
- 输入预览、finding、Filter、sandbox 输出、数据库和报告写入前执行敏感信息脱敏。
137+
- 容器进程由容器内 `timeout` 终止;stdout/stderr 在返回模型前按 `CODE_REVIEW_MAX_OUTPUT_BYTES` 硬限制并脱敏。受 Skill 工具 16 KiB inline 契约约束,Docker 传输的两路输出合计还会取配置值与 15 KiB 的较小者,避免 Docker Desktop 在 64 KiB socket 边界产生长时间等待。
138+
- findings 按 `(file, line, category)` 去重,置信度低于 `0.70` 自动进入 warnings。
139+
140+
## 测试
141+
142+
按要求使用 uv 启动,不运行 Docker:
143+
144+
```bash
145+
uv run --project examples/skills_code_review_agent --with-editable . \
146+
python examples/skills_code_review_agent/tests/run_tests.py
147+
```
148+
149+
公开 fixture 指标评测:
150+
151+
```bash
152+
uv run --project examples/skills_code_review_agent --with-editable . \
153+
python examples/skills_code_review_agent/tests/evaluate_fixtures.py
154+
```
155+
156+
指标输出包含高风险检出率、clean diff 误报率、敏感信息检出率,
157+
并确认 8 个必需 fixture 均生成 JSON 和 Markdown 报告。
158+
159+
不调用模型、但实际启动 Docker runtime 的集成测试:
160+
161+
```bash
162+
uv run --project examples/skills_code_review_agent --with-editable . \
163+
python examples/skills_code_review_agent/tests/run_docker_tests.py
164+
```
165+
166+
测试覆盖:无问题、安全问题、异步任务泄漏、资源生命周期、数据库连接生命周期、测试缺失、重复 finding、sandbox 失败/超时、敏感信息脱敏、六类独立规则、配置工厂、分页、挂载最小化和报告注入。Docker 集成脚本验证 Skill 加载、规则执行、Filter、只读输入、禁网、真实超时、分页以及容器资源安全配置;模型效果仍取决于所配置模型,隐藏样本指标不能由公开 fixture 证明。
167+
168+
使用 `.env` 中的真实模型做完整联调:
169+
170+
```bash
171+
uv run --project examples/skills_code_review_agent --with-editable . \
172+
python examples/skills_code_review_agent/run_agent.py --fixture security
173+
```
174+
175+
详细取舍见 [docs/design.md](./docs/design.md)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Code review agent construction."""
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Build the reasoning agent used by the review workflow."""
2+
3+
from pathlib import Path
4+
5+
from trpc_agent_sdk.agents import LlmAgent
6+
from trpc_agent_sdk.models import OpenAIModel
7+
8+
from reports.models import ReviewAnalysis
9+
from filters.policy import ReviewPolicyContext
10+
from sandbox.base import SandboxProvider
11+
12+
from .config import ModelConfig
13+
from .prompts import INSTRUCTION
14+
from .tools import create_skill_tools
15+
16+
OUTPUT_KEY = "review_analysis"
17+
18+
19+
def create_review_agent(
20+
model_config: ModelConfig,
21+
sandbox: SandboxProvider,
22+
repository_path: Path,
23+
skills_path: Path,
24+
policy_context: ReviewPolicyContext,
25+
) -> LlmAgent:
26+
"""Create an LLM agent with Docker-backed Skill tools."""
27+
toolset, skill_repository, _runtime = create_skill_tools(
28+
sandbox,
29+
repository_path,
30+
skills_path,
31+
policy_context,
32+
)
33+
model = OpenAIModel(
34+
model_name=model_config.model_name,
35+
api_key=model_config.api_key,
36+
base_url=model_config.base_url,
37+
)
38+
return LlmAgent(
39+
name="code_review_agent",
40+
description="Reviews code by selecting and running sandboxed Agent Skills.",
41+
model=model,
42+
instruction=INSTRUCTION,
43+
tools=[toolset],
44+
skill_repository=skill_repository,
45+
output_schema=ReviewAnalysis,
46+
output_key=OUTPUT_KEY,
47+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Model configuration for the review agent."""
2+
3+
import os
4+
import math
5+
from dataclasses import dataclass
6+
from urllib.parse import urlsplit
7+
8+
9+
@dataclass(frozen=True)
10+
class ModelConfig:
11+
"""Configuration required by the OpenAI-compatible model client."""
12+
13+
api_key: str
14+
base_url: str
15+
model_name: str
16+
17+
@classmethod
18+
def from_env(cls) -> "ModelConfig":
19+
"""Load and validate model settings from environment variables."""
20+
values = {
21+
"api_key": os.getenv("TRPC_AGENT_API_KEY", "").strip(),
22+
"base_url": os.getenv("TRPC_AGENT_BASE_URL", "").strip(),
23+
"model_name": os.getenv("TRPC_AGENT_MODEL_NAME", "").strip(),
24+
}
25+
missing = [name for name, value in values.items() if not value]
26+
if missing:
27+
env_names = {
28+
"api_key": "TRPC_AGENT_API_KEY",
29+
"base_url": "TRPC_AGENT_BASE_URL",
30+
"model_name": "TRPC_AGENT_MODEL_NAME",
31+
}
32+
required = ", ".join(env_names[name] for name in missing)
33+
raise ValueError(f"Missing required environment variables: {required}")
34+
parsed_url = urlsplit(values["base_url"])
35+
loopback_hosts = {"localhost", "127.0.0.1", "::1"}
36+
if (
37+
parsed_url.scheme not in {"http", "https"}
38+
or not parsed_url.hostname
39+
or parsed_url.username
40+
or parsed_url.password
41+
or parsed_url.query
42+
or parsed_url.fragment
43+
or any(character.isspace() for character in values["base_url"])
44+
):
45+
raise ValueError(
46+
"TRPC_AGENT_BASE_URL must be an HTTP(S) URL without credentials, "
47+
"query parameters, or fragments"
48+
)
49+
if parsed_url.scheme != "https" and parsed_url.hostname not in loopback_hosts:
50+
raise ValueError(
51+
"TRPC_AGENT_BASE_URL must use HTTPS unless it targets a loopback host"
52+
)
53+
allowed_hosts = {
54+
host.strip().lower()
55+
for host in os.getenv("TRPC_AGENT_ALLOWED_MODEL_HOSTS", "").split(",")
56+
if host.strip()
57+
}
58+
if allowed_hosts and parsed_url.hostname.lower() not in allowed_hosts:
59+
raise ValueError(
60+
"TRPC_AGENT_BASE_URL host is not in TRPC_AGENT_ALLOWED_MODEL_HOSTS"
61+
)
62+
return cls(**values)
63+
64+
65+
@dataclass(frozen=True)
66+
class ReviewLimits:
67+
"""Whole-review budgets applied in addition to per-command limits."""
68+
69+
timeout_seconds: float = 110.0
70+
max_tool_calls: int = 30
71+
72+
@classmethod
73+
def from_env(cls) -> "ReviewLimits":
74+
timeout_seconds = float(os.getenv("CODE_REVIEW_TOTAL_TIMEOUT_SECONDS", "110"))
75+
max_tool_calls = int(os.getenv("CODE_REVIEW_MAX_TOOL_CALLS", "30"))
76+
if (
77+
not math.isfinite(timeout_seconds)
78+
or not 0 < timeout_seconds <= 120
79+
):
80+
raise ValueError(
81+
"CODE_REVIEW_TOTAL_TIMEOUT_SECONDS must be between 0 and 120"
82+
)
83+
if not 0 < max_tool_calls <= 30:
84+
raise ValueError("CODE_REVIEW_MAX_TOOL_CALLS must be between 1 and 30")
85+
return cls(
86+
timeout_seconds=timeout_seconds,
87+
max_tool_calls=max_tool_calls,
88+
)

0 commit comments

Comments
 (0)