diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md new file mode 100644 index 0000000..d3183ab --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md @@ -0,0 +1,24 @@ +# Submission rights declaration + +Project: `Code Review Agent` +Submission slug: `kestarsheng-code-review-agent` +Submitter: `刘宇珂 (kestarsheng)` +Date: `2026-09-10` + +The submitter confirms that they own, or have sufficient authorization for, the source code, dependencies, service, data, branding, and other materials submitted in this pull request. + +Subject to the official program terms, the submitter authorizes X-Agent to retain, reproduce, audit, test, archive, and publish the submitted program artifact for judging, fraud prevention, dispute handling, ecosystem submission, and post-award accountability. Closing the pull request, deleting a fork, or deleting an external repository does not revoke the official archive rights attached to an accepted and rewarded entry. + +Third-party components and their licenses: +- `fastapi` — MIT License +- `uvicorn` — BSD-3-Clause License +- `openai` (Python SDK) — MIT License +- `httpx` — BSD-3-Clause License +- `pydantic` / `pydantic-settings` — MIT License +- `python-dotenv` — BSD-3-Clause License +- `fastmcp` — MIT License +- `pytest` — MIT License + +Exceptions or restrictions: None. All source code in `source/` is original work of the submitter. The service calls DeepSeek's hosted LLM API at runtime; no DeepSeek software is included in the submission. + +This template is an operational declaration, not a substitute for event terms reviewed by qualified counsel. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md new file mode 100644 index 0000000..9788dc3 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -0,0 +1,59 @@ +# Code Review Agent + +## Capability + +- **One-line description:** Triple-engine code review: rule-based static analysis + AST structural analysis + LLM semantic review with cross-validation, returning a structured quality report so AI-generated code can be checked before merge. +- **Who it helps:** Developers using AI coding tools (Claude Code, Codex, Cursor) and any AI Agent that needs a code-quality gate. +- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars), a unified diff, multiple files (structured list), or a GitHub PR/commit URL, plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), five-dimension scores (correctness, security, performance, maintainability, best_practice), issues (with source attribution: rule/ast/llm/confirmed, fix_code), strengths and improvements. The rule engine covers 40 built-in rules across 9 languages (Python, JavaScript, TypeScript, Java, Go, Rust, C/C++, Shell, PHP). AST analyzer catches structural issues (undefined vars, unused imports, duplicate defs). Also provides 10 MCP tools, 12 REST endpoints, SARIF 2.1.0 export, code quality metrics, and a CLI for git-diff review. Does not execute, compile, or persist submitted code. + +## Live API + +- **API base URL:** https://code-review-agent-ashy-six.vercel.app/v1 +- **Health-check URL:** https://code-review-agent-ashy-six.vercel.app/health +- **Authentication:** none +- **Rate limits / known limits:** Single request limited by LLM provider timeout (120 s). Max code size 60 000 chars. Free-tier hosting may cold-start. +- **API contract:** OpenAPI at `/docs`; request `POST /v1/review` body `{"code": string, "language"?: string, "context"?: string}`, response `{"ok": true, "language": string, "model": string, "report": ReviewReport}`. Also supports `POST /v1/review_diff` (unified diff), `POST /v1/review_files` (multi-file batch), `POST /v1/review_pr` (GitHub PR URL), `POST /v1/suggest_fix` (auto-fix), `POST /v1/metrics` (code quality metrics), `POST /v1/sarif` (SARIF 2.1.0 export). MCP endpoint at `/mcp` with 10 tools. + +## Source and reproducibility + +- **Source repository:** https://github.com/kestarsheng/code-review-agent +- **Review commit:** `2355ff3` +- **Source submitted in this PR:** `source/` +- **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` +- **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` +- **Deploy:** `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent`, Render Blueprint from `render.yaml`, or Vercel (current production deployment). +- **Version binding:** `GET /health` returns `{"status":"ok","commit":""}`; `GET /.well-known/xagent-verification.json` returns `{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":""}`. The commit is injected via the `COMMIT` environment variable at deploy time. + +The API must expose: + +```json +// GET /health +{"status":"ok","commit":"2355ff3"} +``` + +```json +// GET /.well-known/xagent-verification.json +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"2355ff3"} +``` + +## Verification + +The reproducible call instructions and redacted example responses are in `verification/README.md`. + +- **Health-check result:** `{"status":"ok","commit":"2355ff3..."}` +- **Capability call:** `POST /v1/review` with `{"code":"def f(x): return x/0","language":"python"}` +- **Expected error behavior:** Empty body → 422; oversized code → 413; LLM failure → 502 `{"ok":false,"error":"..."}`. + +## Security and data handling + +- **Data collected:** Submitted code snippet, language hint, optional context. No authentication, no user identifiers. +- **Purpose and retention:** Code is sent to the configured LLM provider (DeepSeek) for review only. The service does not persist submitted code to any database or log. +- **Third parties / outbound network calls:** DeepSeek API (OpenAI-compatible protocol) for LLM inference. +- **Secrets:** No secrets are committed. `LLM_API_KEY` is set as a deployment environment variable and never appears in source. +- **Known risks / restrictions:** Vercel serverless functions have a 10 s default timeout; long code reviews may approach this limit. The LLM may occasionally produce imperfect JSON; the parser tolerates fenced/embedded JSON. + +## Support + +- **Team / builder:** kestarsheng (刘宇珂) +- **Contact:** 2410251355@henu.edu.cn +- **License / rights:** UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. Submitter owns all source and authorizes review and post-award retention. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example new file mode 100644 index 0000000..f9b74b3 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example @@ -0,0 +1,10 @@ +# LLM (OpenAI-compatible protocol) +LLM_BASE_URL=https://api.deepseek.com/v1 +LLM_API_KEY=sk-xxxxxxxxxxxxxxxx +LLM_MODEL=deepseek-chat +LLM_TIMEOUT_SECONDS=120 + +# Deployment identification +COMMIT=dev +HOST=0.0.0.0 +PORT=8000 \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml new file mode 100644 index 0000000..e06663b --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml @@ -0,0 +1,76 @@ +name: Code Review Agent + +on: + pull_request: + branches: [main, master] + +permissions: + contents: read + security-events: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get PR diff + run: | + git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > pr.diff + echo "DIFF_LINES=$(wc -l < pr.diff)" >> $GITHUB_ENV + + - name: Skip if empty + if: env.DIFF_LINES == '0' + run: echo "No changes to review"; exit 0 + + - name: Review with Code Review Agent + run: | + RESPONSE=$(curl -s -X POST \ + https://code-review-agent-ashy-six.vercel.app/v1/review_diff \ + -H "Content-Type: application/json" \ + -d "$(jq -Rs '{diff: .}' < pr.diff)") + + echo "$RESPONSE" > review_result.json + + SCORE=$(echo "$RESPONSE" | jq -r '.report.score // 0') + ISSUES=$(echo "$RESPONSE" | jq -r '.report.issues | length') + CRITICAL=$(echo "$RESPONSE" | jq -r '[.report.issues[] | select(.severity == "critical")] | length') + + echo "::notice::Score: $SCORE/100 | Issues: $ISSUES | Critical: $CRITICAL" + + if [ "$CRITICAL" -gt "0" ]; then + echo "::error::Found $CRITICAL critical issues. Review failed." + exit 1 + fi + + - name: Export SARIF + if: always() + run: | + python3 -c " + import json, sys + data = json.load(open('review_result.json')) + report = data.get('report', {}) + sev_map = {'critical': 'error', 'major': 'error', 'minor': 'warning', 'info': 'note'} + results = [] + for i in report.get('issues', []): + results.append({ + 'ruleId': str(i.get('rule_id') or 'llm'), + 'level': sev_map.get(i.get('severity', 'info'), 'note'), + 'message': {'text': str(i.get('title', '')) + ' — ' + str(i.get('description', ''))}, + 'locations': [{'physicalLocation': { + 'artifactLocation': {'uri': str(i.get('file', 'diff'))}, + 'region': {'startLine': int(i.get('line', 1))}, + }}], + }) + sarif = {'\$schema': 'https://json.schemastore.org/sarif-2.1.0.json', 'version': '2.1.0', 'runs': [{'tool': {'driver': {'name': 'Code Review Agent', 'version': '1.0'}}, 'results': results}]} + json.dump(sarif, open('results.sarif', 'w'), indent=2) + " + + - name: Upload SARIF to GitHub Code Scanning + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: code-review-agent \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore new file mode 100644 index 0000000..e2d9c48 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ + +# Environment & secrets +.env +.env.* +!.env.example + +# IDE / OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# Build / logs +dist/ +build/ +*.log +.vercel diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile new file mode 100644 index 0000000..53edb26 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app +COPY web ./web + +ENV HOST=0.0.0.0 +ENV PORT=7860 + +EXPOSE 7860 + +CMD ["python", "-m", "app.main"] \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md new file mode 100644 index 0000000..8cb8bc8 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md @@ -0,0 +1,403 @@ +# Code Review Agent + +**A code review agent for AI-generated code.** When Claude Code / Codex / Cursor writes code, who checks it before merge? This agent does — triple-engine review (rule engine + AST structural analysis + LLM semantic review) with cross-validation, catching the patterns AI coding tools most commonly get wrong: hallucinated imports, `eval()` injections, shell=True, swallowed exceptions, and more. Returns structured reports with per-dimension scores, deterministic metrics, SARIF export, and directly applicable fix code. Provides REST API and 9 MCP tools. + +> [中文](README_ZH.md) | English + +> Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. +> +> Live demo: https://code-review-agent-ashy-six.vercel.app + +## Why: AI-generated code needs a different kind of review + +AI coding tools (Claude Code, Codex, Cursor, GitHub Copilot) are fast — but they repeat the same mistakes: + +| AI pattern | What happens | Rule that catches it | +| --- | --- | --- | +| Hallucinated imports | `from django.core import some_nonexistent_module` — AI guesses API names | `AI-H001`–`AI-H006` | +| `eval()` / `exec()` for parsing | AI uses `eval(user_input)` instead of `ast.literal_eval()` | `PY-S001` | +| `shell=True` command execution | AI builds shell strings instead of arg lists | `PY-S003` | +| Swallowed exceptions | `except: pass` — AI adds bare catches to "be safe" | `PY-B001` | +| `forEach` + `await` | AI writes `arr.forEach(async (x) => await fetch(x))` — doesn't await | `AI-H004` | +| Hardcoded secrets | AI inlines API keys instead of using env vars | `PY-S004` / `JS-S004` | + +This agent's rule engine includes **6 dedicated AI-pattern rules** (`AI-H001`–`AI-H006`) that target these hallucination patterns. The triple-engine design means: rule engine catches deterministic patterns (ms-level, free), AST analyzer catches structural errors (undefined vars, duplicate defs), and LLM confirms/denies rule hits to reduce false positives — the cross-validation that a single-engine tool can't do. + +## Triple-Engine Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Input: Code / Diff / Multi-file / PR URL │ +└───────────────┬─────────────────────────────────────────┘ + ▼ +┌──────────────────────────┐ ┌─────────────────────────────┐ +│ ① Rule Engine (regex) │ │ ② AST Analysis (Python) │ +│ · 40 cross-language rules │ │ · Syntax errors (exact) │ +│ · Python/JS/TS/Java/Go/ │ │ · Undefined variables │ +│ Rust/C/C++/Shell │ │ · Unused imports │ +│ · Security/Perf/AI/style │ │ · Duplicate definitions │ +│ · Zero-cost, ms-level │ │ · Empty stub functions │ +└──────────┬───────────────┘ └──────────┬──────────────────┘ + ▔▔▔▔▔▔▔▔┬────────────────────▘ + ▼ +┌──────────────────────────┐ +│ ③ LLM Semantic Analysis │ +│ · Receives rule + AST │ +│ pre-scan results │ +│ · Confirms/denies hits │ +│ · Semantic issues │ +│ · Scores & fix_code │ +└──────────┬───────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ④ Cross-Validation Merge (merge_findings) │ +│ · rule / ast / llm / confirmed (both agree → +0.3 conf) │ +└───────────────────────────────┬───────────────────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ⑤ Output: 5-dimension scores + metrics + SARIF + fixes │ +│ · correctness/security/performance/maintainability/best │ +│ · quality metrics (cyclomatic complexity, function length) │ +│ · SARIF 2.1.0 export (VS Code / GitHub Code Scanning) │ +│ · each issue includes fix_code (copy-paste ready) │ +└───────────────────────────────────────────────────────────┘ +``` + +## Features + +- **Triple-engine review** — Regex rule engine (40 rules, 9 languages, 6 AI-pattern rules) + AST-level static analysis (Python syntax/undefined vars/unused imports/duplicate defs) + LLM semantic review with cross-validation +- **5-dimension scoring** — Correctness / Security / Performance / Maintainability / Best Practice, each 0–100, weighted composite score +- **Deterministic quality metrics** — Cyclomatic complexity (McCabe), function length distribution, comment ratio, long lines — zero LLM cost, instant +- **SARIF 2.1.0 export** — Standards-compliant output for VS Code (Sarif Viewer) and GitHub Code Scanning, CI-ready +- **GitHub PR/commit URL review** — Paste a PR or commit URL, auto-fetch diff and review +- **Directly applicable fix code** — Rule engine auto-generates `fix_code` for 8 key rule types, LLM covers complex scenarios +- **Four review modes** — Single file code, Unified Diff, Multi-file batch, GitHub PR URL +- **CLI one-click review** — `python cli.py` reads git diff directly, no pasting needed +- **MCP toolset** — 10 tools: review / diff review / multi-file / PR review / security scan / metrics / SARIF export / rule explanation / fix generation / rule listing +- **Interactive workbench** — Live demo with Metrics, SARIF, Rules, and PR URL tabs (free & instant, no LLM needed) + +## CLI One-Click Review (Recommended) + +```bash +python cli.py # Review uncommitted changes (git diff) +python cli.py --staged # Review staged changes (git diff --cached) +python cli.py --commit HEAD~1 # Review the last commit +python cli.py src/utils.py # Review a single file +python cli.py --remote # Use remote Vercel deployment (no local server needed) +python cli.py --format json # Output JSON (machine-readable, for pipes/CI) +python cli.py --sarif out.sarif # Export SARIF (GitHub Code Scanning format) +``` + +Exit codes: `0` no serious issues | `2` critical/major found (CI gate) | `1` runtime error + +Auto-reads git diff → calls API → outputs structured report with severity icons, dimension scores, and fix code. + +## CI/CD Integration + +### GitHub Actions (PR Auto-Review) + +Includes `.github/workflows/code-review.yml`, auto-triggers on PR to main: + +1. Gets PR diff → calls Code Review Agent API +2. Fails Action if critical issues found (blocks merge) +3. Exports SARIF and uploads to GitHub Code Scanning (issues annotated on PR diff lines) + +### pre-commit hook + +```bash +# .git/hooks/pre-commit +python cli.py --staged --remote || exit 1 # Blocks commit if critical/major found +``` + +### SARIF + GitHub Code Scanning + +```bash +python cli.py --sarif results.sarif --remote +# Then upload in GitHub Action with github/codeql-action/upload-sarif@v3 +``` + +## API Overview + +| Method | Endpoint | Description | +| --- | --- | --- | +| `POST` | `/v1/review` | Review source code, return structured report | +| `POST` | `/v1/review_diff` | Review Unified Diff (PR changes) | +| `POST` | `/v1/review_files` | Multi-file batch review (cross-file architecture analysis) | +| `POST` | `/v1/review_pr` | Review GitHub PR/commit URL (auto-fetch diff) | +| `POST` | `/v1/suggest_fix` | Generate complete fixed version for problematic code | +| `POST` | `/v1/metrics` | Deterministic code quality metrics (no LLM) | +| `POST` | `/v1/sarif` | SARIF 2.1.0 export (VS Code / GitHub Code Scanning) | +| `GET` | `/v1/rules` | List all rule engine rules | +| `GET` | `/v1/rules/{rule_id}` | View single rule details and fix guidance | +| `GET` | `/health` | Health check, returns deployment commit | +| `GET` | `/.well-known/xagent-verification.json` | Deployment proof (slug + commit) | +| `GET` | `/` | Interactive demo page | + +## Quick start (local) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # Fill in LLM_API_KEY +uvicorn app.main:app --reload +``` + +Open http://127.0.0.1:8000 for the demo page, or http://127.0.0.1:8000/docs for Swagger. + +### Example: Review Code + +```bash +curl -X POST http://127.0.0.1:8000/v1/review \ + -H "Content-Type: application/json" \ + -d '{"code": "result = eval(user_input)", "language": "python"}' +``` + +Response (abridged): + +```json +{ + "report": { + "score": 68, + "grade": "C", + "dimension_scores": { + "correctness": 88, "security": 35, + "performance": 90, "maintainability": 80, "best_practice": 75 + }, + "issues": [ + { + "severity": "critical", + "category": "security", + "line": 1, + "title": "Using eval() to execute arbitrary code", + "description": "eval() executes arbitrary strings as code, posing a severe injection risk.", + "suggestion": "Use ast.literal_eval() or a dedicated parser.", + "fix_code": "result = ast.literal_eval(user_input)", + "source": "confirmed", + "rule_id": "PY-S001", + "confidence": 1.0 + } + ], + "engine_info": { + "rule_count": 0, "llm_count": 0, "confirmed_count": 1, + "total_rules_run": 3, "engines": ["rule", "llm"] + } + } +} +``` + +### Example: Review a Diff + +```bash +curl -X POST http://127.0.0.1:8000/v1/review_diff \ + -H "Content-Type: application/json" \ + -d '{"diff": "--- a/x.py\n+++ b/x.py\n@@ -1,3 +1,4 @@\n def f():\n- return 1\n+ return eval(data)", "language": "python"}' +``` + +Response includes `files_changed` / `added_lines` / `removed_lines` change metadata with the full report. + +### Example: Multi-file Review + +```json +{ + "context": "User service module", + "files": [ + {"filename": "utils.py", "content": "import os\napi_key = os.environ['KEY']", "language": "python"}, + {"filename": "main.py", "content": "from utils import *\nresult = eval(req.body)", "language": "python"} + ] +} +``` + +Returns per-file `file_reports` (rule scan) and one `overall_report` (LLM cross-file architecture review). + +## MCP Usage + +### Local stdio (Claude Code / Codex / Cursor) + +```bash +python -m app.mcp_server # stdio transport +``` + +Register in client config: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "python", + "args": ["-m", "app.mcp_server"] + } + } +} +``` + +### Remote streamable HTTP (same deployment, no local Python needed) + +After deployment, access `https:///mcp`, configure in MCP client: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "npx", + "args": ["-y", "@anthropic-ai/mcp-client", "https:///mcp"] + } + } +} +``` + +> The remote MCP endpoint and REST API share the same server. After deployment, `/mcp` provides streamable HTTP protocol, `/v1/*` provides REST. + +### Usage Guide (for Agents) + +1. **Free quick scan first**: Use `detect_security` / `list_rules` / `explain_issue` (no LLM call, ms-level response) +2. **Deep review**: Use `review_code` / `review_diff` / `review_files`, default `detail="brief"` (saves context, returns title-level issues only) +3. **Full report when needed**: `detail="full"` returns complete description / suggestion / fix_code for each issue +4. **Fix**: Use `suggest_fix` to get directly replaceable `fixed_code` + +### MCP Tools + +| Tool | Parameters | LLM | Description | +| --- | --- | --- | --- | +| `review_code` | `code, language?, context?, detail?` | ✅ | Review source code (`detail: "brief"\|"full"`) | +| `review_diff` | `diff, language?, context?, detail?` | ✅ | Review Unified Diff | +| `review_files` | `files: [{filename, content, language?}], context?, detail?` | ✅ | Multi-file batch review (structured params, not JSON string) | +| `review_pr` | `url, language?, context?, detail?` | ✅ | Review GitHub PR/commit URL (auto-fetch diff) | +| `detect_security` | `code, language?` | ❌ | Rule engine security scan only, instant response | +| `analyze_metrics` | `code, language?` | ❌ | Deterministic quality metrics (complexity, function length) | +| `explain_issue` | `rule_id` | ❌ | Explain a rule (definition/severity/fix guidance) | +| `suggest_fix` | `code, language?, context?` | ✅ | Return fixed code (fixed_code + change explanation) | +| `export_sarif` | `code, language?, uri?` | ❌ | SARIF 2.1.0 export (VS Code / GitHub Code Scanning) | +| `list_rules` | — | ❌ | List all rules | + +> `review_files` `files` parameter is a **structured array**, each element `{filename, content, language?}`. Agents don't need to manually compose JSON strings. + +### Real MCP tool calls (captured output) + +The following are **real tool outputs** from the deployed agent — non-LLM tools called locally, LLM tools called against the live endpoint. + +**Agent → `detect_security`** (instant, no LLM): + +``` +Agent: detect_security(code="import os\napi_key='sk-1234567890abcdef'\nresult = eval(user_input)\nos.system('rm -rf /tmp/x')\ndata = pickle.loads(raw_data)", language="python") + +Tool → {"ok": true, "total_findings": 3, "findings": [ + {"rule_id": "PY-S001", "severity": "critical", "line": 3, + "title": "使用 eval() 执行任意代码", + "suggestion": "避免使用 eval()。如需解析表达式,使用 ast.literal_eval() 或专用解析器。", + "confidence": 0.95}, + {"rule_id": "PY-S004", "severity": "major", "line": 2, + "title": "硬编码密钥/密码", + "suggestion": "使用环境变量或密钥管理服务:api_key = os.environ['API_KEY']", + "confidence": 0.8}, + {"rule_id": "PY-S005", "severity": "major", "line": 5, + "title": "使用 pickle 反序列化不可信数据", + "suggestion": "使用 JSON 等安全格式序列化数据", + "confidence": 0.9} +]} +``` + +**Agent → `analyze_metrics`** (instant, no LLM): + +``` +Agent: analyze_metrics(code="def process_data(items):\n result = []\n for i in range(len(items)):\n for j in range(len(items)):\n ...", language="python") + +Tool → {"ok": true, "metrics": { + "complexity": {"average": 4.0, "max": 4, + "most_complex": [{"name": "process_data", "line": 1, "complexity": 4}]}, + "functions": {"count": 1, "average_length": 7.0, "max_length": 7}, + "lines": {"total": 7, "code": 7, "comment": 0, "blank": 0} +}} +``` + +**Agent → `list_rules`** (instant, no LLM): + +``` +Agent: list_rules() + +Tool → {"total": 40, "rules": [ + {"id": "PY-S001", "severity": "critical", "category": "security", "title": "使用 eval() 执行任意代码"}, + {"id": "PY-S002", "severity": "critical", "category": "security", "title": "使用 exec() 执行任意代码"}, + {"id": "PY-S003", "severity": "critical", "category": "security", "title": "命令注入风险"}, + {"id": "PY-S004", "severity": "major", "category": "security", "title": "硬编码密钥/密码"}, + {"id": "PY-S005", "severity": "major", "category": "security", "title": "使用 pickle 反序列化不可信数据"}, + ... (35 more) +]} +``` + +**Agent → `review_pr`** (LLM, live endpoint `https://code-review-agent-ashy-six.vercel.app/v1/review_pr`): + +``` +Agent: review_pr(url="https://github.com/kestarsheng/code-review-agent/commit/952fa21", language="python") + +Tool → {"ok": true, "files_changed": 5, "added_lines": 240, "model": "deepseek-chat", + "report": { + "score": 46, "grade": "D", + "dimension_scores": {"correctness": 36, "security": 9, "performance": 85, + "maintainability": 88, "best_practice": 54}, + "issues": [ + {"severity": "critical", "source": "rule", "rule_id": "PY-AST-S001", + "line": 104, "title": "Python 语法错误,代码无法解析"}, + {"severity": "critical", "source": "rule", "rule_id": "PY-S001", + "line": 229, "title": "使用 eval() 执行任意代码", + "fix_code": "result = ast.literal_eval(x)"}, + {"severity": "major", "source": "llm", + "line": 78, "title": "fetch_diff 跟随重定向且未校验最终主机,存在 SSRF 风险"}, + ... (9 more) + ], + "engine_info": {"rule_count": 8, "llm_count": 4, "confirmed_count": 0, + "engines": ["rule", "ast", "llm"]} + } +} +``` + +> The `review_pr` call demonstrates the full pipeline: GitHub URL → diff fetch → triple-engine review → structured report with cross-engine attribution (`source: "rule"` vs `source: "llm"`) and auto-generated `fix_code`. + +## Rule Engine + +Built-in **40 cross-language rules** covering Python / JavaScript / TypeScript / Java / Go / Rust / C/C++ / Shell / PHP: + +| Category | Count | Examples | +| --- | --- | --- | +| Security | 15 | `eval`/`exec`, SQL injection, command injection, hardcoded secrets, `pickle.loads`, `innerHTML` XSS | +| Performance | 6 | Nested loops O(n²), dict iteration without `.items()`, pre-generating large lists | +| **AI Pattern** | **6** | **Hallucinated imports** (`AI-H001`–`AI-H003`), `forEach`+`await` (`AI-H004`), catch swallowing (`AI-H005`), nonexistent method calls (`AI-H006`) | +| Maintainability / Best Practice | 13 | TODO/FIXME, bare `except`, missing type annotations | + +8 key rule types have **auto fix code generation** (`eval`→`ast.literal_eval`, `innerHTML`→`textContent`, hardcoded secret→`os.environ`, etc.). + +## Configuration (Environment Variables) + +| Var | Default | Description | +| --- | --- | --- | +| `LLM_BASE_URL` | `https://api.deepseek.com/v1` | OpenAI-compatible base URL | +| `LLM_API_KEY` | — | API key (required) | +| `LLM_MODEL` | `deepseek-chat` | Model name | +| `LLM_TIMEOUT_SECONDS` | `120` | LLM request timeout | +| `MAX_CODE_CHARS` | `60000` | Max characters per review | +| `COMMIT` | `dev` | Deployment commit, returned by /health and verification file | + +## Deployment + +- **Vercel** (current): `vercel.json` configured for Serverless service; push after setting env vars in Vercel project +- **Docker**: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` +- **Render**: Use `render.yaml`, push repo and set env vars + +Post-deploy verification: + +```bash +curl https:///health +curl https:///.well-known/xagent-verification.json +``` + +## Testing + +```bash +python -m pytest tests/ -v +``` + +93 unit tests covering rule engine (40 rules), AST analysis, diff parsing, 5-dimension scoring, fix code generation, multi-file review, PR URL review, metrics, SARIF export, and full triple-engine flow. + +## License + +UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_ZH.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_ZH.md new file mode 100644 index 0000000..7f6d810 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_ZH.md @@ -0,0 +1,322 @@ +# Code Review Agent + + +**给 AI 生成的代码把关。** 当 Claude Code / Codex / Cursor 写完代码,谁在 merge 前检查?本 Agent 做这件事——三引擎评审(规则引擎 + AST 结构分析 + LLM 语义评审)+ 交叉验证,专治 AI 编程工具最常犯的错误:幻觉导入、`eval()` 注入、`shell=True`、吞异常等。输出带分维度评分、确定性度量、SARIF 导出和可直接应用修复代码的结构化报告。提供 REST API 与 10 个 MCP 工具。 + +> [English](README.md) | 中文 + +> Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. +> +> 在线演示:https://code-review-agent-ashy-six.vercel.app + +## 为什么:AI 生成的代码需要不同的评审方式 + +AI 编程工具(Claude Code、Codex、Cursor、GitHub Copilot)很快——但它们重复犯同样的错误: + +| AI 模式 | 发生了什么 | 捕获规则 | +| --- | --- | --- | +| 幻觉导入 | `from django.core import some_nonexistent_module` — AI 猜 API 名 | `AI-H001`–`AI-H006` | +| `eval()` / `exec()` 解析 | AI 用 `eval(user_input)` 而非 `ast.literal_eval()` | `PY-S001` | +| `shell=True` 命令执行 | AI 拼字符串而非参数列表 | `PY-S003` | +| 吞异常 | `except: pass` — AI 加裸 catch "以防万一" | `PY-B001` | +| `forEach` + `await` | AI 写 `arr.forEach(async (x) => await fetch(x))` — 不会 await | `AI-H004` | +| 硬编码密钥 | AI 内联 API key 而非用环境变量 | `PY-S004` / `JS-S004` | + +本 Agent 的规则引擎包含 **6 条专用 AI 模式规则**(`AI-H001`–`AI-H006`),精准打击这些幻觉模式。三引擎设计意味着:规则引擎捕获确定性模式(毫秒级、免费),AST 分析器捕获结构错误(未定义变量、重复定义),LLM 确认/否定规则命中以降低误报——这是单引擎工具做不到的交叉验证。 + +## 三引擎架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 输入:代码 / Diff / 多文件 / PR URL │ +└───────────────┬─────────────────────────────────────────┘ + ▼ +┌──────────────────────────┐ ┌─────────────────────────────┐ +│ ① 规则引擎(regex) │ │ ② AST 分析(Python) │ +│ · 40 条跨语言规则 │ │ · 语法错误(精确) │ +│ · Python/JS/TS/Java/Go/ │ │ · 未定义变量 │ +│ Rust/C/C++/Shell/PHP │ │ · 未使用导入 │ +│ · 安全/性能/AI幻觉/风格 │ │ · 重复定义 │ +│ · 零成本、毫秒级 │ │ · 空桩函数 │ +└──────────┬───────────────┘ └──────────┬──────────────────┘ + ▔▔▔▔▔▔▔┬────────────────────▘ + ▼ +┌──────────────────────────┐ +│ ③ LLM 语义分析 │ +│ · 接收规则 + AST 预检结果 │ +│ · 确认/否定命中 │ +│ · 语义级问题 │ +│ · 评分 & fix_code │ +└──────────┬───────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ④ 交叉验证合并(merge_findings) │ +│ · rule / ast / llm / confirmed(双引擎一致 → 置信 +0.3) │ +└───────────────────────────────┬───────────────────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ⑤ 输出:五维度评分 + 度量 + SARIF + 修复代码 │ +│ · correctness/security/performance/maintainability/best │ +│ · 代码质量度量(圈复杂度、函数长度) │ +│ · SARIF 2.1.0 导出(VS Code / GitHub Code Scanning) │ +│ · 每个 issue 附带 fix_code(可直接复制替换) │ +└───────────────────────────────────────────────────────────┘ +``` + +## 功能特性 + +- **三引擎评审** — 规则引擎(40 条规则,9 语言,6 条 AI 幻觉规则)+ AST 结构分析(Python 语法/未定义变量/未使用导入/重复定义)+ LLM 语义评审 + 交叉验证 +- **五维度评分** — 正确性 / 安全性 / 性能 / 可维护性 / 最佳实践各一个 0–100 分,加权得综合分 +- **确定性质量度量** — 圈复杂度(McCabe)、函数长度分布、注释率、长行——零 LLM 成本,即时 +- **SARIF 2.1.0 导出** — 兼容 VS Code(Sarif Viewer)和 GitHub Code Scanning,CI-ready +- **GitHub PR/commit URL 评审** — 粘贴 PR 或 commit URL,自动拉取 diff 并评审 +- **可直接应用的修复代码** — 规则引擎为 8 类关键规则自动生成 `fix_code`,LLM 覆盖更复杂的修复 +- **四种评审模式** — 单文件代码、Unified Diff、多文件批量、GitHub PR URL +- **CLI 一键评审** — `python cli.py` 直接读 git diff 评审,无需粘贴代码 +- **MCP 工具集** — 10 个工具:评审 / Diff 评审 / 多文件 / PR 评审 / 安全扫描 / 度量 / SARIF 导出 / 规则解释 / 修复生成 / 规则列表 +- **交互式演示页** — 度量、SARIF、规则、PR URL 标签页(免费即时,无需 LLM) + +## CLI 一键评审(推荐) + +```bash +python cli.py # 评审工作区未提交改动 (git diff) +python cli.py --staged # 评审已暂存改动 (git diff --cached) +python cli.py --commit HEAD~1 # 评审最近一次提交 +python cli.py src/utils.py # 评审单个文件 +python cli.py --remote # 用远程 Vercel 部署(无需启动本地服务) +python cli.py --format json # 输出 JSON(机器可读,用于管道/CI) +python cli.py --sarif out.sarif # 导出 SARIF(GitHub Code Scanning 格式) +``` + +退出码:`0` 无严重问题 | `2` 存在 critical/major(可做 CI 门禁)| `1` 运行错误 + +自动读取 git diff → 调 API → 输出带严重度图标、维度评分、修复代码的结构化报告。 + +## CI/CD 集成 + +### GitHub Actions(PR 自动评审) + +项目自带 `.github/workflows/code-review.yml`,PR 到 main 时自动触发: + +1. 获取 PR diff → 调用 Code Review Agent API +2. 有 critical issue 时 Action 失败(阻断 merge) +3. 导出 SARIF 上传到 GitHub Code Scanning(issue 直接标注在 PR diff 行上) + +### pre-commit hook + +```bash +# .git/hooks/pre-commit +python cli.py --staged --remote || exit 1 # 有 critical/major 则阻止提交 +``` + +### SARIF + GitHub Code Scanning + +```bash +python cli.py --sarif results.sarif --remote +# 然后在 GitHub Action 中用 github/codeql-action/upload-sarif@v3 上传 +``` + +## API 一览 + +| Method | Endpoint | 说明 | +| --- | --- | --- | +| `POST` | `/v1/review` | 评审源代码,返回结构化报告 | +| `POST` | `/v1/review_diff` | 评审 Unified Diff(PR 变更) | +| `POST` | `/v1/review_files` | 多文件批量评审(跨文件架构分析) | +| `POST` | `/v1/review_pr` | 评审 GitHub PR/commit URL(自动拉取 diff) | +| `POST` | `/v1/suggest_fix` | 为问题代码生成完整修复版本 | +| `POST` | `/v1/metrics` | 确定性代码质量度量(无 LLM) | +| `POST` | `/v1/sarif` | SARIF 2.1.0 导出(VS Code / GitHub Code Scanning) | +| `GET` | `/v1/rules` | 列出全部规则引擎规则 | +| `GET` | `/v1/rules/{rule_id}` | 查看单条规则详情与修复指引 | +| `GET` | `/health` | 健康检查,返回部署 Commit | +| `GET` | `/.well-known/xagent-verification.json` | 部署证明(slug + commit) | +| `GET` | `/` | 在线演示页 | + +## Quick start (local) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # 填入 LLM_API_KEY +uvicorn app.main:app --reload +``` + +打开 http://127.0.0.1:8000 使用演示页,或 http://127.0.0.1:8000/docs 查看 Swagger。 + +### 示例:评审一段代码 + +```bash +curl -X POST http://127.0.0.1:8000/v1/review \ + -H "Content-Type: application/json" \ + -d '{"code": "result = eval(user_input)", "language": "python"}' +``` + +响应(节选): + +```json +{ + "report": { + "score": 68, + "grade": "C", + "dimension_scores": { + "correctness": 88, "security": 35, + "performance": 90, "maintainability": 80, "best_practice": 75 + }, + "issues": [ + { + "severity": "critical", + "category": "security", + "line": 1, + "title": "使用 eval() 执行任意代码", + "description": "eval() 会执行任意字符串作为代码,是严重的注入风险点。", + "suggestion": "使用 ast.literal_eval() 或专用解析器。", + "fix_code": "result = ast.literal_eval(user_input)", + "source": "confirmed", + "rule_id": "PY-S001", + "confidence": 1.0 + } + ], + "engine_info": { + "rule_count": 0, "llm_count": 0, "confirmed_count": 1, + "total_rules_run": 3, "engines": ["rule", "llm"] + } + } +} +``` + +### 示例:评审一个 Diff + +```bash +curl -X POST http://127.0.0.1:8000/v1/review_diff \ + -H "Content-Type: application/json" \ + -d '{"diff": "--- a/x.py\n+++ b/x.py\n@@ -1,3 +1,4 @@\n def f():\n- return 1\n+ return eval(data)", "language": "python"}' +``` + +响应会包含 `files_changed` / `added_lines` / `removed_lines` 变更元数据与完整报告。 + +### 示例:多文件评审 + +```json +{ + "context": "用户服务模块", + "files": [ + {"filename": "utils.py", "content": "import os\napi_key = os.environ['KEY']", "language": "python"}, + {"filename": "main.py", "content": "from utils import *\nresult = eval(req.body)", "language": "python"} + ] +} +``` + +返回每个文件的 `file_reports`(规则扫描)与一个 `overall_report`(LLM 跨文件架构评审)。 + +## MCP usage + +### 本地 stdio(Claude Code / Codex / Cursor) + +```bash +python -m app.mcp_server # stdio transport +``` + +注册到客户端配置: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "python", + "args": ["-m", "app.mcp_server"] + } + } +} +``` + +### 远程 streamable HTTP(同一部署,无需本地 Python) + +部署后访问 `https:///mcp`,在 MCP 客户端中配置: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "npx", + "args": ["-y", "@anthropic-ai/mcp-client", "https:///mcp"] + } + } +} +``` + +> 远程 MCP 端点与 REST API 共用同一个服务器,部署后 `/mcp` 提供 streamable HTTP 协议,`/v1/*` 提供 REST。 + +### 使用引导(给 Agent) + +1. **先免费快筛**:用 `detect_security` / `list_rules` / `explain_issue`(无 LLM 调用,毫秒级返回) +2. **深度评审**:用 `review_code` / `review_diff` / `review_files`,默认 `detail="brief"`(节省上下文,仅返回标题级 issue) +3. **需要完整报告时**:`detail="full"` 返回每个 issue 的完整 description / suggestion / fix_code +4. **修复**:用 `suggest_fix` 获取可直接替换的 `fixed_code` + +### MCP 工具 + +| 工具 | 参数 | LLM | 说明 | +| --- | --- | --- | --- | +| `review_code` | `code, language?, context?, detail?` | ✅ | 评审源代码(`detail: "brief"\|"full"`) | +| `review_diff` | `diff, language?, context?, detail?` | ✅ | 评审 Unified Diff | +| `review_files` | `files: [{filename, content, language?}], context?, detail?` | ✅ | 多文件批量评审(结构化参数,非 JSON 字符串) | +| `review_pr` | `url, language?, context?, detail?` | ✅ | 评审 GitHub PR/commit URL(自动拉取 diff) | +| `detect_security` | `code, language?` | ❌ | 仅规则引擎安全扫描,即时返回 | +| `analyze_metrics` | `code, language?` | ❌ | 确定性质量度量(圈复杂度、函数长度) | +| `explain_issue` | `rule_id` | ❌ | 解释某条规则(定义/严重级别/修复指引) | +| `suggest_fix` | `code, language?, context?` | ✅ | 返回修复后的完整代码(fixed_code + 变更说明) | +| `export_sarif` | `code, language?, uri?` | ❌ | SARIF 2.1.0 导出(VS Code / GitHub Code Scanning) | +| `list_rules` | — | ❌ | 列出全部规则 | + +> `review_files` 的 `files` 参数是**结构化数组**,每个元素 `{filename, content, language?}`,Agent 无需手工拼 JSON 字符串。 + +## 规则引擎 + +内置 **40 条跨语言规则**,覆盖 Python / JavaScript / TypeScript / Java / Go / Rust / C/C++ / Shell / PHP: + +| 类别 | 数量 | 示例 | +| --- | --- | --- | +| Security | 15 | `eval`/`exec`、SQL 注入、命令注入、硬编码密钥、`pickle.loads`、`innerHTML` XSS | +| Performance | 6 | 嵌套循环 O(n²)、字典遍历未用 `.items()`、预生成大列表 | +| **AI Pattern** | **6** | **幻觉导入**(`AI-H001`–`AI-H003`)、`forEach`+`await`(`AI-H004`)、吞异常(`AI-H005`)、不存在的方法调用(`AI-H006`) | +| Maintainability / Best Practice | 13 | TODO/FIXME、裸 `except`、缺类型注解 | + +8 类关键规则带 **自动修复代码生成**(`eval`→`ast.literal_eval`、`innerHTML`→`textContent`、硬编码密钥→`os.environ` 等)。 + +## 配置(环境变量) + +| Var | Default | Description | +| --- | --- | --- | +| `LLM_BASE_URL` | `https://api.deepseek.com/v1` | OpenAI-compatible base URL | +| `LLM_API_KEY` | — | API key(必填) | +| `LLM_MODEL` | `deepseek-chat` | 模型名 | +| `LLM_TIMEOUT_SECONDS` | `120` | LLM 请求超时 | +| `MAX_CODE_CHARS` | `60000` | 单次评审最大字符数 | +| `COMMIT` | `dev` | 部署 Commit,/health 与验证文件返回 | + +## 部署 + +- **Vercel**(当前):`vercel.json` 已配置 Serverless 服务;在 Vercel 项目设置环境变量后推送即可 +- **Docker**: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` +- **Render**: 使用 `render.yaml`,推送仓库并设置环境变量 + +部署后验证: + +```bash +curl https:///health +curl https:///.well-known/xagent-verification.json +``` + +## 测试 + +```bash +python -m pytest tests/ -v +``` + +93 个单元测试,覆盖规则引擎(40 条规则)、AST 分析、Diff 解析、五维度评分、修复代码生成、多文件评审、PR URL 评审、度量、SARIF 导出与完整三引擎流程。 + +## License + +UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py new file mode 100644 index 0000000..732c1c3 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py @@ -0,0 +1 @@ +from app.main import app \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py new file mode 100644 index 0000000..690abf7 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Code Review Agent application package.""" \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/ast_analyzer.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/ast_analyzer.py new file mode 100644 index 0000000..cdee899 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/ast_analyzer.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +"""AST-level static analysis engine (Python). + +Unlike the regex rule engine, this parses source with the standard-library +`ast` module and reasons about the actual syntax tree. It detects issues the +regex engine cannot see reliably: + +- PY-AST-S001 SyntaxError: the code does not even parse +- PY-AST-I001 unused import +- PY-AST-U001 reference to a variable that is not defined in scope +- PY-AST-D001 duplicate function/class definition (later wins, silent bug) +- PY-AST-M001 function body that only contains `pass` (unimplemented stub) + +All checks are deterministic, offline, and cost zero LLM tokens. +""" +from __future__ import annotations + +import ast +import builtins + +from .rules_engine import Finding + +_BUILTIN_NAMES = set(dir(builtins)) +_SKIP_NAMES = { + "__name__", "__file__", "__doc__", "__package__", "__loader__", + "__spec__", "__builtins__", "__main__", "self", "cls", +} + + +def analyze_python(code: str) -> list[Finding]: + """Run AST-level analysis on Python source, returning deterministic findings.""" + try: + tree = ast.parse(code) + except (SyntaxError, ValueError) as exc: + lineno = getattr(exc, "lineno", None) or 1 + msg = getattr(exc, "msg", "unknown syntax error") or "unknown syntax error" + return [ + Finding( + rule_id="PY-AST-S001", + severity="critical", + category="correctness", + line=lineno, + title="Python 语法错误,代码无法解析", + description=f"第 {lineno} 行存在语法错误: {msg}。代码无法被执行,所有下游分析均已停止。", + suggestion="修复语法后才能继续评审。检查括号/引号/缩进是否配对,并用编辑器语法高亮定位错误行。", + confidence=1.0, + source="ast", + ) + ] + + findings: list[Finding] = [] + findings.extend(_find_unused_imports(tree, code)) + findings.extend(_find_undefined_names(tree)) + findings.extend(_find_duplicate_definitions(tree)) + findings.extend(_find_empty_functions(tree)) + return findings + + +def _name_targets(node: ast.AST) -> set[str]: + """Collect names assigned by a target node (handles Tuple/List unpacking).""" + if isinstance(node, ast.Name): + return {node.id} + if isinstance(node, (ast.Tuple, ast.List)): + names: set[str] = set() + for elt in node.elts: + names |= _name_targets(elt) + return names + if isinstance(node, ast.Starred): + return _name_targets(node.value) + return set() + + +def _find_unused_imports(tree: ast.AST, code: str) -> list[Finding]: + """Flag imports whose names never appear outside their own import statement.""" + findings: list[Finding] = [] + lines = code.splitlines() + + for node in ast.walk(tree): + if not isinstance(node, (ast.Import, ast.ImportFrom)): + continue + for alias in node.names: + name = alias.asname or alias.name.split(".")[0] + if name == "*": + continue + within = 0 + for lineno in range(node.lineno, (node.end_lineno or node.lineno) + 1): + if lineno <= len(lines): + within += lines[lineno - 1].count(name) + total = code.count(name) + if total <= within: + findings.append(Finding( + rule_id="PY-AST-I001", + severity="minor", + category="maintainability", + line=node.lineno, + title=f"未使用的 import: {alias.name}", + description=f"import '{alias.name}' 在代码中从未被使用,属于冗余代码,会增加理解成本。", + suggestion="删除该 import 语句。如果它只是为副作用引入的(如注册插件),显式用下划线标注或以注释说明。", + confidence=0.85, + source="ast", + )) + return findings + + +def _collect_defined_names(tree: ast.AST) -> set[str]: + """Collect every name that is definitely defined somewhere in the module.""" + defined: set[str] = set(_BUILTIN_NAMES) | _SKIP_NAMES + + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + args = node.args + for a in list(args.posonlyargs) + list(args.args) + list(args.kwonlyargs): + defined.add(a.arg) + if args.vararg: + defined.add(args.vararg.arg) + if args.kwarg: + defined.add(args.kwarg.arg) + elif isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + if isinstance(node, ast.Assign): + targets = node.targets + else: + targets = [node.target] + for t in targets: + defined |= _name_targets(t) + elif isinstance(node, (ast.For, ast.AsyncFor)): + defined |= _name_targets(node.target) + elif isinstance(node, (ast.With, ast.AsyncWith)): + for item in node.items: + if item.optional_vars is not None: + defined |= _name_targets(item.optional_vars) + elif isinstance(node, ast.ExceptHandler): + if node.name: + defined.add(node.name) + elif isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)): + for gen in node.generators: + defined |= _name_targets(gen.target) + return defined + + +def _find_undefined_names(tree: ast.AST) -> list[Finding]: + """Flag Load-context names inside functions that are never defined. + + Only function bodies are inspected: at module top level we cannot know + whether a name comes from an external package, so we stay conservative + and keep confidence low to avoid false positives. + """ + defined = _collect_defined_names(tree) + findings: list[Finding] = [] + seen: set[tuple[int, str]] = set() + + def visit(node: ast.AST, in_function: bool) -> None: + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load): + if in_function and node.id not in defined: + key = (node.lineno, node.id) + if key not in seen: + seen.add(key) + findings.append(Finding( + rule_id="PY-AST-U001", + severity="major", + category="correctness", + line=node.lineno, + title=f"可能引用了未定义的变量: {node.id}", + description=( + f"变量 '{node.id}' 在当前作用域中未找到赋值、参数或 import 来源," + f"运行时可能抛出 NameError。" + ), + suggestion=( + "确认该变量已赋值、已作为参数传入,或补充 import。" + "若依赖外部全局变量,请显式传入或用注释说明。" + ), + confidence=0.65, + source="ast", + )) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + for child in ast.iter_child_nodes(node): + visit(child, True) + return + for child in ast.iter_child_nodes(node): + visit(child, in_function) + + for stmt in tree.body: + visit(stmt, False) + return findings + + +def _find_duplicate_definitions(tree: ast.AST) -> list[Finding]: + """Flag two same-named function/class definitions in the same scope.""" + findings: list[Finding] = [] + + def scan_body(body: list[ast.stmt]) -> None: + seen: dict[str, int] = {} + for stmt in body: + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if stmt.name in seen: + findings.append(Finding( + rule_id="PY-AST-D001", + severity="major", + category="correctness", + line=stmt.lineno, + title=f"重复定义: {stmt.name}", + description=( + f"'{stmt.name}' 已在第 {seen[stmt.name]} 行定义过,当前定义会静默覆盖前者," + f"这在条件分支或后续调用时会产生难以排查的 bug。" + ), + suggestion="合并重复定义或重命名,确保同作用域内名称唯一。", + confidence=0.9, + source="ast", + )) + else: + seen[stmt.name] = stmt.lineno + + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): + scan_body(stmt.body) + elif isinstance(stmt, (ast.If,)): + scan_body(stmt.body) + scan_body(stmt.orelse) + elif isinstance(stmt, (ast.For, ast.AsyncFor, ast.While)): + scan_body(stmt.body) + scan_body(stmt.orelse) + elif isinstance(stmt, (ast.With, ast.AsyncWith)): + scan_body(stmt.body) + elif isinstance(stmt, ast.Try): + scan_body(stmt.body) + for handler in stmt.handlers: + scan_body(handler.body) + scan_body(stmt.orelse) + scan_body(stmt.finalbody) + + scan_body(tree.body) + return findings + + +def _find_empty_functions(tree: ast.AST) -> list[Finding]: + """Flag functions whose body is only `pass` (unimplemented stubs).""" + findings: list[Finding] = [] + + def _is_empty(body: list[ast.stmt]) -> bool: + non_doc = [ + n for n in body + if not (isinstance(n, ast.Expr) and isinstance(n.value, ast.Constant) + and isinstance(n.value.value, str)) + ] + return non_doc and all(isinstance(n, ast.Pass) for n in non_doc) + + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _is_empty(node.body): + findings.append(Finding( + rule_id="PY-AST-M001", + severity="info", + category="maintainability", + line=node.lineno, + title=f"函数体仅为 pass 占位: {node.name}", + description=f"'{node.name}' 尚未实现,仅以 pass 占位,调用时会得到 None 而非预期结果。", + suggestion="实现函数逻辑,或改为抛出 NotImplementedError 以显式标记未完成。", + confidence=0.75, + source="ast", + )) + return findings \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py new file mode 100644 index 0000000..94aaf1b --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Application configuration loaded from environment variables.""" +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + +PROJECT_SLUG = "kestarsheng-code-review-agent" + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", extra="ignore" + ) + + # LLM (OpenAI-compatible protocol: DeepSeek / Qwen / etc.) + llm_base_url: str = "https://api.deepseek.com/v1" + llm_api_key: str = "" + llm_model: str = "deepseek-chat" + llm_timeout_seconds: float = 120.0 + + # Deployment identification + commit: str = "dev" + host: str = "0.0.0.0" + port: int = 8000 + + # Safety + max_code_chars: int = 60_000 + max_response_chars: int = 20_000 + + +@lru_cache +def get_settings() -> Settings: + return Settings() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py new file mode 100644 index 0000000..346eb0d --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +"""Unified diff parser for diff-level code review. + +Parses standard unified diff format (git diff, GitHub PR diff) and +extracts changed lines with context for targeted review. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass + + +@dataclass +class DiffHunk: + """A single hunk within a diff.""" + old_start: int + old_count: int + new_start: int + new_count: int + file_path: str + lines: list[tuple[str, str, int]] # (sign, content, new_line_number) + + +@dataclass +class ParsedDiff: + """Result of parsing a unified diff.""" + hunks: list[DiffHunk] + files_changed: list[str] + added_lines: int + removed_lines: int + reconstructed_code: str # changed lines joined for review + + +_HUNK_HEADER = re.compile( + r"^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@" +) +_FILE_HEADER = re.compile(r"^\+\+\+\s+(?:b/)?(.+)$") + + +def parse_diff(diff_text: str) -> ParsedDiff: + """Parse a unified diff string into structured hunks.""" + hunks: list[DiffHunk] = [] + files_changed: set[str] = set() + added = 0 + removed = 0 + + lines = diff_text.splitlines() + i = 0 + current_file = "" + reconstructed_parts: list[str] = [] + + while i < len(lines): + line = lines[i] + + file_match = _FILE_HEADER.match(line) + if file_match: + current_file = file_match.group(1) + files_changed.add(current_file) + i += 1 + continue + + hunk_match = _HUNK_HEADER.match(line) + if hunk_match: + old_start = int(hunk_match.group(1)) + old_count = int(hunk_match.group(2) or 1) + new_start = int(hunk_match.group(3)) + new_count = int(hunk_match.group(4) or 1) + + hunk_lines: list[tuple[str, str, int]] = [] + new_line_num = new_start + i += 1 + + while i < len(lines): + hl = lines[i] + if hl.startswith("@@") or hl.startswith("+++") or hl.startswith("---"): + break + + if hl.startswith("+"): + hunk_lines.append(("+", hl[1:], new_line_num)) + reconstructed_parts.append(hl[1:]) + new_line_num += 1 + added += 1 + elif hl.startswith("-"): + hunk_lines.append(("-", hl[1:], 0)) + removed += 1 + elif hl.startswith(" "): + hunk_lines.append((" ", hl[1:], new_line_num)) + new_line_num += 1 + elif hl == "": + pass + i += 1 + + hunks.append(DiffHunk( + old_start=old_start, old_count=old_count, + new_start=new_start, new_count=new_count, + file_path=current_file, + lines=hunk_lines, + )) + continue + + i += 1 + + reconstructed_code = "\n".join(reconstructed_parts) + return ParsedDiff( + hunks=hunks, + files_changed=sorted(files_changed), + added_lines=added, + removed_lines=removed, + reconstructed_code=reconstructed_code, + ) + + +def diff_summary(parsed: ParsedDiff) -> str: + """Generate a human-readable summary of the diff.""" + parts = [ + f"变更文件: {', '.join(parsed.files_changed) or '未知'}", + f"新增 {parsed.added_lines} 行, 删除 {parsed.removed_lines} 行", + f"共 {len(parsed.hunks)} 个 hunk", + ] + return " | ".join(parts) \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/github_fetch.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/github_fetch.py new file mode 100644 index 0000000..81dcb48 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/github_fetch.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""Fetch unified diffs from GitHub PR / commit URLs. + +Supports: +- https://github.com/{owner}/{repo}/pull/{number} +- https://github.com/{owner}/{repo}/pull/{number}.diff +- https://github.com/{owner}/{repo}/commit/{sha} +- https://github.com/{owner}/{repo}/commit/{sha}.diff +- https://patch-diff.githubusercontent.com/raw/{owner}/{repo}/pull/{number}.diff + +Public repos need no token. Private repos read GITHUB_TOKEN from the env. +Uses only the standard library — no extra dependency. +""" +from __future__ import annotations + +import os +import re +import urllib.error +import urllib.request + +_PR_RE = re.compile( + r"^https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)(?:\.diff)?/?$" +) +_COMMIT_RE = re.compile( + r"^https?://github\.com/([^/]+)/([^/]+)/commit/([0-9a-f]+)(?:\.diff)?/?$" +) +_PATCH_DIFF_RE = re.compile( + r"^https?://patch-diff\.githubusercontent\.com/raw/([^/]+)/([^/]+)/pull/(\d+)\.diff$" +) + +_FETCH_TIMEOUT = 12 +_MAX_DIFF_BYTES = 200_000 + + +class FetchError(Exception): + pass + + +def normalize_to_diff_url(url: str) -> tuple[str, str]: + """Convert a GitHub URL to its .diff URL and return (diff_url, description).""" + url = url.strip() + + m = _PATCH_DIFF_RE.match(url) + if m: + return url, f"PR {m.group(1)}/{m.group(2)}#{m.group(3)}" + + m = _PR_RE.match(url) + if m: + owner, repo, num = m.group(1), m.group(2), m.group(3) + diff_url = f"https://github.com/{owner}/{repo}/pull/{num}.diff" + return diff_url, f"PR {owner}/{repo}#{num}" + + m = _COMMIT_RE.match(url) + if m: + owner, repo, sha = m.group(1), m.group(2), m.group(3) + diff_url = f"https://github.com/{owner}/{repo}/commit/{sha}.diff" + return diff_url, f"commit {owner}/{repo}@{sha[:7]}" + + raise FetchError( + "Unsupported URL. Use a GitHub PR or commit URL, e.g. " + "https://github.com/owner/repo/pull/123 or " + "https://github.com/owner/repo/commit/" + ) + + +def fetch_diff(url: str, token: str | None = None) -> tuple[str, str]: + """Fetch a unified diff from a GitHub PR/commit URL. + + Returns (diff_text, source_description). Raises FetchError on failure. + """ + diff_url, description = normalize_to_diff_url(url) + token = token or os.environ.get("GITHUB_TOKEN", "") + + req = urllib.request.Request(diff_url) + req.add_header("User-Agent", "code-review-agent") + req.add_header("Accept", "text/plain, application/x-patch, */*") + if token: + req.add_header("Authorization", f"token {token}") + + try: + with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT) as resp: + raw = resp.read(_MAX_DIFF_BYTES + 1) + except urllib.error.HTTPError as exc: + if exc.code == 404: + raise FetchError(f"未找到资源(404):{diff_url}") from exc + if exc.code in (401, 403): + raise FetchError( + f"无访问权限({exc.code})。若是私有仓库,请设置 GITHUB_TOKEN 环境变量。" + ) from exc + raise FetchError(f"GitHub 返回 HTTP {exc.code}") from exc + except urllib.error.URLError as exc: + raise FetchError(f"网络请求失败:{exc.reason}") from exc + + if len(raw) > _MAX_DIFF_BYTES: + raise FetchError( + f"diff 过大(>{_MAX_DIFF_BYTES // 1024} KB),请使用更小的 PR 或 commit。" + ) + + diff_text = raw.decode("utf-8", errors="replace") + if not diff_text.strip(): + raise FetchError("拉取到的 diff 为空,可能该 PR/commit 无代码变更。") + return diff_text, description \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py new file mode 100644 index 0000000..154546b --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +"""FastAPI application entry point. + +Provides: +- GET /v1 API base URL endpoint directory +- POST /v1/review dual-engine review of source code +- POST /v1/review_diff dual-engine review of a unified diff +- POST /v1/review_files multi-file batch review +- POST /v1/suggest_fix generate corrected code for known issues +- GET /v1/rules list all built-in rule engine rules +- GET /v1/rules/{rule_id} explain one rule in detail +- GET /health health check returning the deployed commit +- GET /.well-known/xagent-verification.json deployment proof +- GET / minimal web demo page +- MCP /mcp remote streamable-HTTP MCP endpoint (same server) +""" +import logging +from pathlib import Path + +from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse + +from .config import PROJECT_SLUG, get_settings +from .github_fetch import FetchError, fetch_diff +from .mcp_server import mcp +from .metrics import compute_metrics +from .sarif import sarif_from_code +from .reviewer import ( + ReviewError, + explain_issue, + review_code, + review_diff, + review_files, + suggest_fix_for_code, +) +from .rules_engine import RULES, run_rules +from .schemas import ( + DiffReviewRequest, + DiffReviewResponse, + FilesReviewRequest, + FilesReviewResponse, + HealthResponse, + PullRequestReviewRequest, + ReviewRequest, + ReviewResponse, + VerificationResponse, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +settings = get_settings() + +# Remote MCP endpoint (streamable HTTP transport). Created before app so its +# lifespan can be wired into the FastAPI app; mounted after all REST routes. +mcp_app = None +try: + mcp_app = mcp.http_app(path="/mcp") + logger.info("MCP streamable HTTP endpoint ready at /mcp") +except Exception: # noqa: BLE001 + logger.exception("Failed to build MCP HTTP endpoint") + +app = FastAPI( + title="Code Review Agent", + description="Dual-engine AI code quality review: rule-based static analysis " + "+ LLM semantic review with cross-validation.", + version="2.1.0", + lifespan=mcp_app.lifespan if mcp_app is not None else None, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + + +@app.get("/health", response_model=HealthResponse, tags=["meta"]) +def health() -> HealthResponse: + return HealthResponse(status="ok", commit=settings.commit) + + +@app.get( + "/.well-known/xagent-verification.json", + response_model=VerificationResponse, + tags=["meta"], +) +def verification() -> VerificationResponse: + return VerificationResponse( + schemaVersion=1, slug=PROJECT_SLUG, commit=settings.commit + ) + + +@app.get("/v1", tags=["meta"]) +def api_index() -> dict: + """API base URL: return the endpoint directory so /v1 is never a 404.""" + return { + "service": "Code Review Agent", + "version": "2.1.0", + "endpoints": { + "POST /v1/review": "review source code (dual-engine)", + "POST /v1/review_diff": "review a unified diff", + "POST /v1/review_pr": "review a GitHub PR/commit by URL", + "POST /v1/review_files": "multi-file batch review", + "POST /v1/suggest_fix": "generate corrected code", + "POST /v1/metrics": "deterministic quality metrics (no LLM)", + "POST /v1/sarif": "export findings as SARIF 2.1.0 (CI-ready)", + "GET /v1/rules": "list built-in rule engine rules", + "GET /v1/rules/{rule_id}": "explain one rule", + "GET /health": "health check (deployed commit)", + "GET /.well-known/xagent-verification.json": "deployment proof", + "GET /mcp": "remote MCP endpoint", + }, + } + + +@app.post("/v1/review", response_model=ReviewResponse, tags=["review"]) +async def review(req: ReviewRequest) -> ReviewResponse: + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + report = review_code(code=req.code, language=req.language, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return ReviewResponse(language=req.language, model=settings.llm_model, report=report) + + +@app.post("/v1/review_diff", response_model=DiffReviewResponse, tags=["review"]) +async def review_diff_endpoint(req: DiffReviewRequest) -> DiffReviewResponse: + if len(req.diff) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"diff 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + result = review_diff(diff=req.diff, language=req.language, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + diff_meta = result.get("diff_meta", {}) + report = {k: v for k, v in result.items() if k != "diff_meta"} + return DiffReviewResponse( + files_changed=diff_meta.get("files_changed", []), + added_lines=diff_meta.get("added_lines", 0), + removed_lines=diff_meta.get("removed_lines", 0), + model=settings.llm_model, + report=report, + ) + + +@app.post("/v1/review_pr", response_model=DiffReviewResponse, tags=["review"]) +async def review_pr_endpoint(req: PullRequestReviewRequest) -> DiffReviewResponse: + """Fetch a GitHub PR/commit diff by URL and run dual-engine review.""" + try: + diff_text, source = fetch_diff(req.url) + except FetchError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if len(diff_text) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"diff 过长(限制 {settings.max_code_chars} 字符)", + ) + context = req.context or source + try: + result = review_diff(diff=diff_text, language=req.language, context=context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + diff_meta = result.get("diff_meta", {}) + report = {k: v for k, v in result.items() if k != "diff_meta"} + return DiffReviewResponse( + files_changed=diff_meta.get("files_changed", []), + added_lines=diff_meta.get("added_lines", 0), + removed_lines=diff_meta.get("removed_lines", 0), + model=settings.llm_model, + report=report, + ) + + +@app.post("/v1/review_files", response_model=FilesReviewResponse, tags=["review"]) +async def review_files_endpoint(req: FilesReviewRequest) -> FilesReviewResponse: + total_chars = sum(len(f.content) for f in req.files) + if total_chars > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"文件内容总过长(限制 {settings.max_code_chars} 字符)", + ) + files = [ + {"filename": f.filename, "content": f.content, "language": f.language} + for f in req.files + ] + try: + result = review_files(files=files, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + return FilesReviewResponse( + model=settings.llm_model, + total_files=len(files), + file_reports=result["file_reports"], + overall_report=result["overall_report"], + ) + + +@app.post("/v1/metrics", tags=["review"]) +async def metrics_endpoint(req: ReviewRequest) -> dict: + """Compute deterministic quality metrics instantly, no LLM call. + + Returns lines / function length / cyclomatic complexity / comment ratio. + """ + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + return { + "ok": True, + "language": req.language, + "metrics": compute_metrics(req.code, req.language), + } + + +@app.post("/v1/sarif", tags=["review"]) +async def sarif_endpoint(req: ReviewRequest) -> dict: + """Export deterministic findings as SARIF 2.1.0 — no LLM, CI-ready. + + The output is consumable by VS Code (Sarif Viewer), GitHub Code Scanning + and any SARIF-aware tool. Runs rules + AST analysis only. + """ + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + uri = req.context or "snippet.py" + return sarif_from_code(req.code, req.language, uri=uri) + + +@app.get("/v1/rules", tags=["review"]) +def list_rules() -> dict: + """List all built-in rule engine rules.""" + return { + "total": len(RULES), + "rules": [ + { + "id": r.id, + "language": r.language, + "severity": r.severity, + "category": r.category, + "confidence": r.confidence, + "title": r.title, + } + for r in RULES + ], + } + + +@app.get("/v1/rules/{rule_id}", tags=["review"]) +def get_rule(rule_id: str) -> dict: + """Explain a single rule-engine rule in detail.""" + result = explain_issue(rule_id) + if not result.get("ok"): + raise HTTPException(status_code=404, detail=result.get("error")) + return result + + +@app.post("/v1/suggest_fix", tags=["review"]) +async def suggest_fix(req: ReviewRequest) -> dict: + """Generate a full corrected version of code with known issues.""" + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + result = suggest_fix_for_code( + code=req.code, language=req.language, context=req.context + ) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return {"ok": True, "model": settings.llm_model, "result": result} + + +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +def index() -> HTMLResponse: + html = Path(__file__).resolve().parent.parent / "web" / "index.html" + if html.exists(): + return HTMLResponse(html.read_text(encoding="utf-8")) + return HTMLResponse("

Code Review Agent

See /docs for API.

") + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"ok": False, "error": exc.detail}, + ) + + +# Mount MCP catch-all AFTER all REST routes so /health, /v1/* etc. win. +if mcp_app is not None: + app.mount("/", mcp_app) + + +def main() -> None: + import uvicorn + + uvicorn.run("app.main:app", host=settings.host, port=settings.port, reload=False) + + +if __name__ == "__main__": + main() diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py new file mode 100644 index 0000000..92f28a7 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +"""FastMCP server exposing code review capabilities as reusable MCP tools. + +Tools: +- review_code: dual-engine review of a source code snippet [LLM] +- review_diff: dual-engine review of a unified diff / PR [LLM] +- review_files: dual-engine review of multiple files [LLM] +- detect_security: fast rule-only security scan (instant, free) +- analyze_metrics: deterministic quality metrics (instant, free) +- list_rules: list all built-in rule engine rules (instant) +- explain_issue: explain a rule in detail (instant, free) +- suggest_fix: generate corrected code for known issues [LLM] + +Usage guidance for agents: +1. Start cheap: use detect_security / analyze_metrics / list_rules / + explain_issue first (no LLM cost, millisecond latency). +2. For a full analysis call review_code / review_diff / review_files with + detail="brief" (default) to save context tokens; use detail="full" when + the user needs every fix suggestion. +3. When the user wants the problem fixed, call suggest_fix with the code. + +Run via stdio (default) for Claude Code / Codex / Cursor, or mount +`mcp` onto a FastAPI app for remote streamable HTTP transport +(see app.main: `app.mount("/mcp", mcp.http_app())`). +""" +import json + +from fastmcp import FastMCP + +from .config import PROJECT_SLUG +from .github_fetch import FetchError, fetch_diff +from .metrics import compute_metrics +from .sarif import sarif_from_code +from .reviewer import ( + ReviewError, + _build_brief_report, + explain_issue, + review_code, + review_diff, + review_files, + suggest_fix_for_code, +) +from .rules_engine import RULES, run_rules + +mcp = FastMCP( + PROJECT_SLUG, + instructions=( + "Dual-engine code review assistant. Rule engine + AST analysis + LLM " + "semantic review with cross-validation. Free instant tools (no LLM " + "call): detect_security, analyze_metrics, list_rules, explain_issue. " + "LLM tools: review_code, review_diff, review_files (use " + "detail='brief' to save context unless the user needs full details), " + "suggest_fix. Workflow: quick scan with detect_security and " + "analyze_metrics first, then deep review, then explain_issue/" + "suggest_fix as needed." + ), +) + + +def _wrap_review(report: dict, detail: str, extra: dict | None = None) -> str: + """Serialize a review report, applying brief/full detail control.""" + out = {"ok": True} + if extra: + out.update(extra) + if detail == "full": + report_with_meta = dict(report) + report_with_meta["detail"] = "full" + out["report"] = report_with_meta + else: + brief = _build_brief_report(report) + brief["detail"] = "brief" + out["report"] = brief + return json.dumps(out, ensure_ascii=False) + + +@mcp.tool() +def review_code_tool( + code: str, + language: str = "", + context: str = "", + detail: str = "brief", +) -> str: + """Review a source code snippet with dual-engine analysis (rules + LLM). + + Use this when you have a piece of code (not a diff) you want reviewed + for security, correctness, performance, maintainability issues. + + Args: + code: source code to review. + language: programming language hint (python, java, js, go, ...). + context: optional description of what the code is supposed to do. + detail: "brief" (default, trimmed issues to save context) or "full" + (complete report with every description/suggestion/fix_code). + + Returns: + JSON string with summary, score, grade, dimension scores, issues + (source-attributed) and engine stats. + """ + try: + report = review_code(code=code, language=language, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + return _wrap_review(report, detail) + + +@mcp.tool() +def review_diff_tool( + diff: str, + language: str = "", + context: str = "", + detail: str = "brief", +) -> str: + """Review a unified diff (git diff / PR change) for change-level risks. + + Use this when the user shares a diff or asks you to check a PR / commit + change, not full source files. + + Args: + diff: unified diff text. + language: programming language hint. + context: optional description of the change purpose. + detail: "brief" (default) or "full". + + Returns: + JSON string with diff metadata (files_changed, added/deleted lines) + and a structured review report. + """ + try: + result = review_diff(diff=diff, language=language, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + diff_meta = result.pop("diff_meta", {}) + return _wrap_review(result, detail, extra={"diff_meta": diff_meta}) + + +@mcp.tool() +def review_pull_request( + url: str, + language: str = "", + context: str = "", + detail: str = "brief", +) -> str: + """Fetch a GitHub PR/commit diff by URL and run dual-engine review. + + Accepts GitHub PR or commit URLs (public repos need no token; private + repos read GITHUB_TOKEN from the environment). Use this when the user + shares a GitHub link and wants it reviewed. + + Args: + url: GitHub PR or commit URL. + language: programming language hint. + context: optional description of the change purpose. + detail: "brief" (default) or "full". + + Returns: + JSON string with diff metadata and a structured review report. + """ + try: + diff_text, source = fetch_diff(url) + except FetchError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + try: + result = review_diff( + diff=diff_text, language=language, context=context or source + ) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + diff_meta = result.pop("diff_meta", {}) + return _wrap_review( + result, detail, extra={"diff_meta": diff_meta, "source": source} + ) + + +@mcp.tool() +def review_files_tool( + files: list[dict], + context: str = "", + detail: str = "brief", +) -> str: + """Review multiple files (entire module/project change) with dual-engine analysis. + + Use this when reviewing a whole set of files at once, e.g. before committing + or when several files changed together. Detects per-file rule hits plus + cross-file architecture problems. + + Args: + files: array of file objects, each {"filename": str, "content": str, + "language": str (optional)}. + context: optional project/task context description. + detail: "brief" (default) or "full". + + Returns: + JSON string with per-file reports and an overall cross-file report. + """ + try: + result = review_files(files=files, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + if detail == "full": + out = result + else: + overall = result.get("overall_report", {}) + out = { + "file_count": len(result.get("file_reports", [])), + "overall_report": _build_brief_report(overall, max_issues=8), + } + out["detail"] = detail + return json.dumps({"ok": True, "result": out}, ensure_ascii=False) + + +@mcp.tool() +def explain_issue(rule_id: str) -> str: + """Explain a rule-engine rule in detail (no LLM call, instant, free). + + Use this to understand why a rule fired or to explain a finding to the + user. Args: rule_id, e.g. PY-S001, JS-S002, AI-H003. + + Returns: + JSON string with rule definition, severity, category and guidance. + """ + result = explain_issue(rule_id) + return json.dumps(result, ensure_ascii=False) + + +@mcp.tool() +def suggest_fix( + code: str, + language: str = "", + context: str = "", +) -> str: + """Generate a full corrected version of code with known issues (LLM). + + Use this when the user wants the code actually fixed, not just reviewed. + Rules engine runs first to surface deterministic findings, then the LLM + produces a complete fixed_code block that can replace the original. + + Args: + code: source code to fix. + language: programming language hint (python, java, js, go, ...). + context: optional description of what the code is supposed to do. + + Returns: + JSON string with fixed_code, explanation, and list of changes. + """ + try: + result = suggest_fix_for_code(code=code, language=language, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + return json.dumps({"ok": True, "result": result}, ensure_ascii=False) + + +@mcp.tool() +def detect_security(code: str, language: str = "") -> str: + """Fast rule-only security scan — no LLM call, returns instantly. + + Use this for a quick cheap security check while coding; call review_code + for deeper semantic analysis. + + Args: + code: source code to scan. + language: programming language hint. + + Returns: + JSON string with detected security issues and their rule IDs. + """ + findings = run_rules(code, language) + security_findings = [ + f for f in findings if f.category in ("security", "ai_pattern") + ] + return json.dumps( + { + "ok": True, + "total_findings": len(security_findings), + "findings": [ + { + "rule_id": f.rule_id, + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + "confidence": f.confidence, + } + for f in security_findings + ], + }, + ensure_ascii=False, + ) + + +@mcp.tool() +def analyze_metrics(code: str, language: str = "") -> str: + """Compute deterministic code quality metrics — instant and free, no LLM. + + Use this for a quantitative health check while coding: lines, function + length distribution, cyclomatic complexity, comment ratio, long lines. + + Args: + code: source code to measure. + language: programming language hint. + + Returns: + JSON string with the metrics object. + """ + return json.dumps( + {"ok": True, "metrics": compute_metrics(code, language)}, + ensure_ascii=False, + ) + + +@mcp.tool() +def export_sarif(code: str, language: str = "", uri: str = "snippet.py") -> str: + """Export deterministic findings as SARIF 2.1.0 — instant, free, CI-ready. + + The output is consumable by VS Code (Sarif Viewer), GitHub Code Scanning + and any SARIF-aware pipeline tool. Runs rules + AST analysis only (no LLM). + + Args: + code: source code to analyze. + language: programming language hint. + uri: artifact URI to attach findings to (e.g. 'src/main.py'). + + Returns: + JSON string of the SARIF 2.1.0 document. + """ + return json.dumps( + sarif_from_code(code, language, uri=uri), + ensure_ascii=False, + ) + + +@mcp.tool() +def list_rules() -> str: + """List all built-in rule engine rules with their metadata (instant, free). + + Returns: + JSON string with all rules (id, language, severity, category, title). + """ + return json.dumps( + { + "total": len(RULES), + "rules": [ + { + "id": r.id, + "language": r.language, + "severity": r.severity, + "category": r.category, + "confidence": r.confidence, + "title": r.title, + } + for r in RULES + ], + }, + ensure_ascii=False, + ) + + +def main() -> None: + mcp.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/metrics.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/metrics.py new file mode 100644 index 0000000..078089f --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/metrics.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +"""Code quality metrics computation (deterministic, offline, zero LLM). + +Produces a quantitative "health check" of a piece of code: size, function +length distribution, cyclomatic complexity, comment ratio, and long lines. + +Python input is measured precisely with the standard-library `ast` module. +Other languages fall back to lightweight line-based heuristics. +""" +from __future__ import annotations + +import ast +import re +from dataclasses import dataclass + +from .rules_engine import detect_language + +_LONG_LINE_THRESHOLD = 120 + + +@dataclass +class FunctionStat: + name: str + line: int + length: int + complexity: int + + +def compute_metrics(code: str, language: str = "") -> dict: + """Compute quality metrics for a code snippet.""" + code = code or "" + detected = detect_language(code, language) + lines_list = code.splitlines() + total = len(lines_list) + + code_lines = 0 + comment_lines = 0 + blank_lines = 0 + long_lines = 0 + + for raw in lines_list: + stripped = raw.strip() + if not stripped: + blank_lines += 1 + elif _is_comment_line(stripped): + comment_lines += 1 + else: + code_lines += 1 + if len(raw) > _LONG_LINE_THRESHOLD: + long_lines += 1 + + if detected == "python": + funcs, has_syntax_error = _analyze_python_functions(code) + else: + funcs, has_syntax_error = _analyze_functions_heuristic(lines_list) + + if funcs: + avg_len = round(sum(f.length for f in funcs) / len(funcs), 1) + avg_cc = round(sum(f.complexity for f in funcs) / len(funcs), 1) + max_len = max(f.length for f in funcs) + max_cc = max(f.complexity for f in funcs) + else: + avg_len = 0.0 + avg_cc = 0.0 + max_len = 0 + max_cc = 0 + + longest = sorted(funcs, key=lambda f: f.length, reverse=True)[:5] + most_complex = sorted(funcs, key=lambda f: f.complexity, reverse=True)[:5] + + denominator = code_lines + comment_lines + comment_ratio = round(comment_lines / denominator, 3) if denominator else 0.0 + + return { + "language": detected or language or "unknown", + "syntax_error": has_syntax_error, + "lines": { + "total": total, + "code": code_lines, + "comment": comment_lines, + "blank": blank_lines, + }, + "comment_ratio": comment_ratio, + "long_lines": long_lines, + "functions": { + "count": len(funcs), + "average_length": avg_len, + "max_length": max_len, + "longest": [ + {"name": f.name, "line": f.line, "length": f.length} + for f in longest + ], + }, + "complexity": { + "average": avg_cc, + "max": max_cc, + "most_complex": [ + {"name": f.name, "line": f.line, "complexity": f.complexity} + for f in most_complex + ], + }, + } + + +def _is_comment_line(stripped: str) -> bool: + if stripped.startswith("#"): + return True + if stripped.startswith("//"): + return True + if stripped.startswith("/*") or stripped.startswith("*"): + return True + if stripped.startswith("--") or stripped.startswith(" + + + +
+
+
+
6 AI-pattern rules · 40 total · 3 engines
+

Who reviews the code
your AI agent writes?

+

Claude Code, Codex, Cursor write code fast — but they hallucinate imports, use eval(), swallow exceptions. This agent catches those patterns: 6 dedicated AI-pattern rules + AST structural analysis + LLM semantic review with cross-validation. Every issue tagged with its source engine.

+
+ + +
+
+
40
Rules
+
9
Languages
+
93
Tests
+
12
Endpoints
+
+
+
+
+
cli.py — commit HEAD~1
+
$ python cli.py --commit HEAD~1 + +Analyzing 2 files · 3 engines... + Review complete Score: 72/C Issues: 4 + + ● CRITICAL SQL Injection + line 2 rule:sql_injection · confirmed + Use parameterized query + + ● MAJOR Hardcoded Secret + line 6 rule:hardcoded_secret · confirmed + Read from env variable + + ● MAJOR eval() Usage + line 7 ast:eval_usage + Replace with json.loads + +─── engines ─────────────────── + rule: 2 ast: 1 llm: 1 confirmed: 2
+
+
+
+
+ + +
+
+
+ how it works +

Three engines. One verdict.

+

A single rule engine finds known patterns. A single LLM hallucinates. AST analysis alone misses semantics. Running all three and cross-validating gives you determinism where it matters and depth where it counts.

+
+
+
+
01
+
+

Rule Engine — 40 patterns, zero API cost

+

Deterministic pattern matching across 9 languages. Security vulnerabilities, performance anti-patterns, AI hallucination signatures. Runs in milliseconds, works offline, never costs a token.

+
# matches in <5ms, no LLM call +sql_injection → CRITICAL line 2 +hardcoded_secret → MAJOR line 6 +eval_usage → CRITICAL line 7
+
+
+
+
02
+
+

AST Analyzer — structural analysis

+

Python ast.parse walks the syntax tree to catch what regex can't: undefined variables, unused imports, duplicate definitions, empty functions, syntax errors. Deterministic, precise, zero false positives on structural issues.

+
# ast_analyzer.py — structural only +undefined_var → MAJOR 'user_id' used before assignment +unused_import → INFO 'os' imported but never used +duplicate_def → MAJOR 'process_user' defined twice
+
+
+
+
03
+
+

LLM Semantic — the deep review

+

Receives rule + AST pre-scan results, then does what deterministic engines can't: logic errors, architecture flaws, business boundary violations. Confirms or denies rule hits, eliminating false positives.

+
// LLM receives pre-scan context, then: +"User input flows into SQL query without + sanitization — confirmed injection risk." +→ issue.source = "confirmed" // both agree +→ issue.confidence = 0.95 // auto-boosted
+
+
+
+
04
+
+

5-Dimension Scoring — quantified quality

+

Correctness, Security, Performance, Maintainability, Best Practice — each 0–100. Composite score weighted: security 30% + correctness 25%. Every issue links back to its source engine and rule ID.

+
security: 45/100 ████████░░░░░░ D +correctness: 72/100 ██████████████ C +performance: 85/100 ████████████████ B +overall: 68/C
+
+
+
+
05
+
+

Fix Code — copy-paste ready

+

Every issue ships with a fix_code snippet. Rule engine auto-generates for 8 key patterns. LLM covers complex semantic fixes. One click applies directly to your input — no manual rewriting.

+
--- line 2 +- query = f"SELECT * FROM users WHERE id = {user_id}" ++ query = "SELECT * FROM users WHERE id = ?" ++ cursor.execute(query, (user_id,))
+
+
+
+
06
+
+

MCP + REST + SARIF — integrate anywhere

+

10 MCP tools for AI agents. 12 REST endpoints for CI/CD. SARIF 2.1.0 export for GitHub Code Scanning and VS Code. PR URL review for GitHub pull requests. All on one server.

+
POST /v1/review → REST (CI/CD) +MCP review_code → Agent tool +MCP analyze_metrics → Agent tool +POST /v1/sarif → GitHub Code Scanning +POST /v1/review_pr → GitHub PR URL
+
+
+
+
+
+ + +
+
+
+ try it +

Paste code. Get a review.

+

Paste code or diff, click review. Or use CLI: python cli.py --commit HEAD~1

+
+
+
+
Code Review
+
Diff Review
+
Multi-File
+
Auto Fix
+
+
+
+ + +
+
+ + +
+ + +
+
+
+
+
Click "Start Review" to see results
+
+ +
+
+ + +
+
+
+ workbench +

Live Workbench — free & instant

+

Deterministic engines run server-side — no LLM, no API key, millisecond latency. Try metrics, SARIF export, rule catalog, or PR URL review.

+
+
+
+
Metrics
+
SARIF Export
+
Rule Catalog
+
PR URL
+
+
+
+
+ + +
+ + +
+ + +
+
+
+
+
Click a tab above and run — results appear here instantly.
+
+
+
+ + +
+
+
+ api +

12 endpoints. 10 MCP tools.

+

REST API and MCP tools coexist on the same server. SARIF export for GitHub Code Scanning. PR URL review for pull requests.

+
+
+
POST/v1/reviewReview source code, return 5-dimension scores + issues + fix code
+
POST/v1/review_diffReview Unified Diff, return change metadata + review report
+
POST/v1/review_filesMulti-file batch review, per-file scan + cross-file analysis
+
POST/v1/review_prReview GitHub PR or commit URL by fetching diff remotely
+
POST/v1/suggest_fixGenerate complete fixed version (fixed_code + change notes)
+
POST/v1/metricsCode quality metrics: complexity, function lengths, comment ratio
+
POST/v1/sarifExport SARIF 2.1.0 for GitHub Code Scanning / VS Code
+
GET/v1/rulesList all 40 rule engine rules
+
GET/v1/rules/{rule_id}View single rule details and fix guidance
+
GET/mcpRemote MCP streamable HTTP endpoint (10 tools)
+
GET/healthHealth check, returns deployment commit
+
GET/.well-known/xagent-verification.jsonDeployment proof — signed commit for hackathon judges
+
+
+
+ + + + +
+ + + + + diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json new file mode 100644 index 0000000..fb2f5e5 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "name": "Code Review Agent", + "slug": "kestarsheng-code-review-agent", + "sourceRepository": "https://github.com/kestarsheng/code-review-agent", + "reviewCommit": "2355ff3637f7c535f71495766ab3b185a20e1932", + "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", + "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", + "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" +} \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md new file mode 100644 index 0000000..c1e2465 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -0,0 +1,264 @@ +# Verification evidence + +## Prerequisites + +- Review commit: `2355ff3` +- API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` +- Authentication: none + +## 1. Health check + +```bash +curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/health +``` + +Expected response: + +```json +{"status":"ok","commit":"2355ff3"} +``` + +## 2. Deployment proof + +```bash +curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json +``` + +Expected response: + +```json +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"2355ff3"} +``` + +## 3. Capability call + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review \ + --header "content-type: application/json" \ + --data '{"code":"def f(x):\n return x / 0","language":"python"}' +``` + +Expected success response (abridged): + +```json +{ + "ok": true, + "language": "python", + "model": "deepseek-chat", + "report": { + "summary": "...", + "score": 30, + "grade": "D", + "issues": [ + { + "severity": "critical", + "category": "correctness", + "line": 2, + "title": "除零错误", + "description": "...", + "suggestion": "..." + } + ], + "strengths": ["..."], + "improvements": ["..."] + } +} +``` + +## 4. Safe error behavior + +Empty body: + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review \ + --header "content-type: application/json" \ + --data '{}' +``` + +Expected: HTTP 422 with `{"ok":false,"error":...}`. + +Oversized code (> 60 000 chars): HTTP 413. +LLM provider failure: HTTP 502 with `{"ok":false,"error":"LLM 调用失败: ..."}`. + +## 5. End-to-end PR review (real LLM call) + +This section demonstrates the full pipeline: paste a GitHub commit URL → fetch diff → triple-engine review → structured report. + +### Request + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review_pr \ + --header "content-type: application/json" \ + --data '{"url":"https://github.com/kestarsheng/code-review-agent/commit/952fa21","language":"python"}' +``` + +**Input:** A real commit from the project's own repository — the commit that added the `github_fetch.py` module (PR/commit URL diff fetching capability). + +### Response (abridged) + +```json +{ + "ok": true, + "files_changed": [ + "app/github_fetch.py", + "app/main.py", + "app/mcp_server.py", + "app/schemas.py", + "tests/test_app.py" + ], + "added_lines": 240, + "removed_lines": 0, + "model": "deepseek-chat", + "report": { + "summary": "本次变更新增了从 GitHub PR/commit URL 拉取 unified diff 的能力…但存在一个 SSRF 风险点:fetch_diff 会跟随重定向且未限制最终主机…", + "score": 46, + "grade": "D", + "dimension_scores": { + "correctness": 36, + "security": 9, + "performance": 85, + "maintainability": 88, + "best_practice": 54 + }, + "issues": [ + { + "severity": "critical", + "category": "correctness", + "line": 104, + "title": "Python 语法错误,代码无法解析", + "source": "rule", + "rule_id": "PY-AST-S001", + "confidence": 1.0 + }, + { + "severity": "critical", + "category": "security", + "line": 229, + "title": "使用 eval() 执行任意代码", + "suggestion": "避免使用 eval()。如需解析表达式,使用 ast.literal_eval()。", + "fix_code": "result = ast.literal_eval(x)", + "source": "rule", + "rule_id": "PY-S001", + "confidence": 0.95 + }, + { + "severity": "major", + "category": "security", + "line": 78, + "title": "fetch_diff 跟随重定向且未校验最终主机,存在 SSRF 风险", + "source": "llm", + "confidence": 0.7 + }, + { + "severity": "minor", + "category": "correctness", + "line": 31, + "title": "PullRequestReviewRequest.url 缺少 max_length 限制", + "source": "llm", + "confidence": 0.7 + } + ], + "engine_info": { + "rule_count": 8, + "ast_count": 0, + "llm_count": 4, + "confirmed_count": 0, + "total_rules_run": 8, + "engines": ["rule", "ast", "llm"] + } + } +} +``` + +### What this proves + +| Evidence | Detail | +| --- | --- | +| **GitHub URL → diff fetch** | `review_pr` endpoint accepted a commit URL, fetched the unified diff from GitHub API | +| **Triple-engine activation** | `rule_count=8` (rule engine fired 8 rules), `llm_count=4` (LLM found 4 issues), `engines=["rule","ast","llm"]` | +| **Cross-engine attribution** | Issues tagged with `source: "rule"` (PY-AST-S001, PY-S001, PY-B002) and `source: "llm"` (SSRF, max_length, newline, test coverage) | +| **Auto fix code** | `eval()` issue includes `fix_code: "result = ast.literal_eval(x)"` — directly replaceable | +| **5-dimension scoring** | correctness=36, security=9, performance=85, maintainability=88, best_practice=54 → composite score 46/D | +| **Change metadata** | `files_changed` (5 files), `added_lines` (240), `removed_lines` (0) extracted from diff | +| **12 issues total** | 2 critical + 1 major + 2 minor + 7 info — full severity spectrum exercised | +## 6. Cross-validation confirmed (rule × LLM agree) + +This section demonstrates the **cross-validation** mechanism: when the rule engine and LLM independently find the same issue on the same line, `merge_findings` tags it `source: "confirmed"` and boosts confidence to 1.0. + +### Request + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review \ + --header "content-type: application/json" \ + --data '{"code":"import os\napi_key = \"sk-1234567890abcdef\"\nresult = eval(user_input)\nprint(result)\nos.system(\"ls \" + filename)","language":"python","context":"AI-generated code"}' +``` + +**Input:** A typical AI-generated snippet — hardcoded secret, `eval()` on untrusted input, `os.system()` with string concatenation. + +### Response (abridged) + +```json +{ + "ok": true, + "model": "deepseek-chat", + "report": { + "score": 14, + "grade": "D", + "dimension_scores": { + "correctness": 2, "security": 0, + "performance": 60, "maintainability": 17, "best_practice": 12 + }, + "issues": [ + { + "severity": "critical", + "category": "security", + "line": 3, + "title": "使用 eval() 执行任意代码", + "suggestion": "避免使用 eval()。如需解析表达式,使用 ast.literal_eval()。", + "fix_code": "import subprocess\nsubprocess.run([\"ls\", filename], check=True, shell=False)", + "source": "confirmed", + "rule_id": "PY-S001", + "confidence": 1.0 + }, + { + "severity": "major", + "category": "security", + "line": 2, + "title": "硬编码密钥/密码", + "source": "rule", + "rule_id": "PY-S004", + "confidence": 0.8 + }, + { + "severity": "major", + "category": "correctness", + "line": 3, + "title": "未定义输入变量导致运行时错误", + "source": "llm", + "confidence": 0.7 + } + ], + "engine_info": { + "rule_count": 1, + "ast_count": 0, + "llm_count": 3, + "confirmed_count": 1, + "total_rules_run": 2, + "engines": ["rule", "ast", "llm"] + } + } +} +``` + +### What this proves + +| Evidence | Detail | +| --- | --- | +| **`confirmed_count = 1`** | The rule engine (`PY-S001`) and LLM **independently** found `eval()` on line 3 → `merge_findings` merged them into a single `source: "confirmed"` issue | +| **Confidence boost** | Rule confidence 0.95 + LLM agreement +0.3 → capped at `confidence: 1.0` | +| **Cross-engine attribution** | Same report has `source: "rule"` (PY-S004 hardcoded secret, rule-only), `source: "llm"` (undefined variable, LLM-only), and `source: "confirmed"` (eval(), both engines) — all three attribution types in one response | +| **Fix code** | The confirmed issue includes `fix_code` with a complete replacement using `subprocess.run()` with `shell=False` | diff --git a/submissions/mcp-hackathon/kestarsheng-contract-guard/source/.env.example b/submissions/mcp-hackathon/kestarsheng-contract-guard/source/.env.example new file mode 100644 index 0000000..942fc64 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-contract-guard/source/.env.example @@ -0,0 +1,12 @@ +# LLM (OpenAI-compatible protocol) — optional advisory layer only. +# The deterministic diff engine runs without any key; the LLM merely adds +# impact assessment and migration guidance when present. +LLM_BASE_URL=https://api.deepseek.com/v1 +LLM_API_KEY=sk-xxxxxxxxxxxxxxxx +LLM_MODEL=deepseek-chat +LLM_TIMEOUT_SECONDS=60 + +# Deployment identification +COMMIT=dev +HOST=0.0.0.0 +PORT=8000 \ No newline at end of file