initial: toy notes service used to demo pragent
A deliberately small Python service — auth helpers, a sqlite-backed store, and two request handlers — plus a .pr-review.json that steers the reviewer toward this repo's house rules. Pull requests against it carry planted defects so the reviewer has something real to find.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"focus": ["security", "sql-injection", "auth", "path-traversal"],
|
||||||
|
"exclude_paths": ["tests/fixtures/**"],
|
||||||
|
"languages": ["python"],
|
||||||
|
"instructions": "House rules: all SQL must be parameterised — flag any query built by string concatenation or f-string as critical. Secrets and tokens must be compared with hmac.compare_digest, never ==. Any filesystem path derived from user input must be resolved and checked to stay inside its intended root. Public handlers in app/api.py must have an explicit authorisation check, not merely an authentication check."
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# pragent-demo
|
||||||
|
|
||||||
|
A tiny, deliberately small service used to demonstrate
|
||||||
|
[**pragent**](https://git.marcospaulo.dev.br/gitea_admin/pragent) — an AI pull-request
|
||||||
|
reviewer for Gitea.
|
||||||
|
|
||||||
|
## How the demo works
|
||||||
|
|
||||||
|
1. Someone opens a pull request.
|
||||||
|
2. A maintainer adds the **`AI-REVIEW`** label.
|
||||||
|
3. A Gitea webhook notifies the pragent service, which checks the repo out at the
|
||||||
|
PR's head commit, reads the changed files *and the code around them*, runs the
|
||||||
|
repo's own linters, and posts a review — a summary plus **inline comments**
|
||||||
|
anchored to the exact lines, each with a suggested fix.
|
||||||
|
4. Adding **`AI-USAGE`** as well appends a token/cost report to the review, so the
|
||||||
|
price of every review is visible rather than mysterious.
|
||||||
|
|
||||||
|
No review fires without the label. Removing it stops re-reviews.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **This repository is a teaching fixture, not example code.**
|
||||||
|
> Pull requests here contain *deliberately planted defects* — SQL injection, unsafe
|
||||||
|
> path handling, weak comparisons — so the reviewer has something real to find.
|
||||||
|
> Do not copy anything from this repo into a real project.
|
||||||
|
|
||||||
|
## Steering the reviewer
|
||||||
|
|
||||||
|
`.pr-review.json` on the **default branch** configures the review for this repo:
|
||||||
|
focus areas, paths to ignore, and house rules. It is read from the base branch on
|
||||||
|
purpose — otherwise a pull request could rewrite the rules it is judged by.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
app/auth.py token + session helpers
|
||||||
|
app/store.py a toy in-memory data store
|
||||||
|
app/api.py the request handlers
|
||||||
|
tests/ what little coverage exists
|
||||||
|
```
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
"""Request handlers. `request` is a dict of {path, query, headers}."""
|
||||||
|
|
||||||
|
from app import auth
|
||||||
|
from app.store import Store
|
||||||
|
|
||||||
|
STORE = Store()
|
||||||
|
|
||||||
|
|
||||||
|
def _current_user(request: dict) -> str | None:
|
||||||
|
token = request.get("headers", {}).get("Authorization", "").removeprefix("Bearer ")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
return auth.user_for_token(token)
|
||||||
|
|
||||||
|
|
||||||
|
def list_notes(request: dict) -> dict:
|
||||||
|
user = _current_user(request)
|
||||||
|
if user is None:
|
||||||
|
return {"status": 401, "body": "unauthenticated"}
|
||||||
|
return {"status": 200, "body": STORE.notes_for(user)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_note(request: dict) -> dict:
|
||||||
|
user = _current_user(request)
|
||||||
|
if user is None:
|
||||||
|
return {"status": 401, "body": "unauthenticated"}
|
||||||
|
note = STORE.get_note(int(request["query"]["id"]))
|
||||||
|
if note is None:
|
||||||
|
return {"status": 404, "body": "not found"}
|
||||||
|
if note[1] != user:
|
||||||
|
return {"status": 403, "body": "forbidden"}
|
||||||
|
return {"status": 200, "body": note}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
"""Token and session helpers."""
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
_SESSIONS: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def new_token() -> str:
|
||||||
|
"""Generate an opaque session token."""
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(supplied: str, expected: str) -> bool:
|
||||||
|
"""Constant-time token comparison."""
|
||||||
|
return hmac.compare_digest(supplied, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def login(user_id: str) -> str:
|
||||||
|
token = new_token()
|
||||||
|
_SESSIONS[token] = user_id
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def user_for_token(token: str) -> str | None:
|
||||||
|
return _SESSIONS.get(token)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""A toy data store. Pretend it is a real database."""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class Store:
|
||||||
|
def __init__(self, path: str = ":memory:"):
|
||||||
|
self.conn = sqlite3.connect(path, check_same_thread=False)
|
||||||
|
self.conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS notes ("
|
||||||
|
" id INTEGER PRIMARY KEY,"
|
||||||
|
" owner TEXT NOT NULL,"
|
||||||
|
" title TEXT NOT NULL,"
|
||||||
|
" body TEXT NOT NULL)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_note(self, owner: str, title: str, body: str) -> int:
|
||||||
|
cur = self.conn.execute(
|
||||||
|
"INSERT INTO notes (owner, title, body) VALUES (?, ?, ?)",
|
||||||
|
(owner, title, body),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
|
||||||
|
def get_note(self, note_id: int) -> tuple | None:
|
||||||
|
cur = self.conn.execute(
|
||||||
|
"SELECT id, owner, title, body FROM notes WHERE id = ?", (note_id,)
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
def notes_for(self, owner: str) -> list[tuple]:
|
||||||
|
cur = self.conn.execute(
|
||||||
|
"SELECT id, owner, title, body FROM notes WHERE owner = ?", (owner,)
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app.store import Store # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_and_get_note():
|
||||||
|
s = Store()
|
||||||
|
nid = s.add_note("alice", "hello", "world")
|
||||||
|
note = s.get_note(nid)
|
||||||
|
assert note[1] == "alice"
|
||||||
|
assert note[2] == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_are_scoped_to_owner():
|
||||||
|
s = Store()
|
||||||
|
s.add_note("alice", "a", "1")
|
||||||
|
s.add_note("bob", "b", "2")
|
||||||
|
assert len(s.notes_for("alice")) == 1
|
||||||
Reference in New Issue
Block a user