spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# Patterns — Validating Input78## Contents9- Pydantic request schema (strict, canonicalizing)10- Zod equivalent11- Structured 422 handler12- Path traversal guard13- Upload validation14- Gotchas1516## Pydantic request schema (strict, canonicalizing)1718```python19from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator20import unicodedata2122class CreateUser(BaseModel):23 model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)2425 name: str = Field(min_length=1, max_length=200)26 email: EmailStr27 age: int = Field(ge=13, le=130)28 role: Literal["member", "editor"] # closed set — allowlist29 tags: list[str] = Field(default_factory=list, max_length=50)3031 @field_validator("name")32 @classmethod33 def canonicalize(cls, v: str) -> str:34 return unicodedata.normalize("NFC", v) # canonicalize BEFORE checks3536 @field_validator("email")37 @classmethod38 def lower_email(cls, v: str) -> str:39 return v.lower()40```4142## Zod equivalent4344```ts45const CreateUser = z.object({46 name: z.string().trim().min(1).max(200),47 email: z.string().email().toLowerCase(),48 age: z.number().int().min(13).max(130),49 role: z.enum(["member", "editor"]),50 tags: z.array(z.string().max(50)).max(50).default([]),51}).strict(); // reject unknown fields52```5354## Structured 422 handler5556```python57# FastAPI: collect ALL field errors in one response.58@app.exception_handler(RequestValidationError)59async def on_validation_error(request, exc):60 return JSONResponse(status_code=422, content={61 "type": "https://example.com/errors/validation",62 "title": "Request validation failed",63 "errors": [64 {"field": ".".join(map(str, e["loc"])), "reason": e["msg"]}65 for e in exc.errors()66 ],67 })68```6970```json71{"title": "Request validation failed",72 "errors": [{"field": "body.age", "reason": "Input should be less than or equal to 130"}]}73```7475## Path traversal guard7677```python78from pathlib import Path7980BASE = Path("/srv/app/uploads").resolve()8182def safe_path(user_supplied: str) -> Path:83 candidate = (BASE / user_supplied).resolve()84 if not candidate.is_relative_to(BASE): # blocks ../.. and absolute paths85 raise ValueError("path escapes base directory")86 return candidate87```8889## Upload validation9091```python92MAX_UPLOAD = 10 * 1024 * 1024 # 10 MB — set per product need, never unlimited9394MAGIC = {b"\x89PNG": "image/png", b"\xff\xd8\xff": "image/jpeg", b"%PDF": "application/pdf"}9596def check_upload(stream, declared_type: str):97 head = stream.read(8); stream.seek(0)98 sniffed = next((t for magic, t in MAGIC.items() if head.startswith(magic)), None)99 if sniffed is None or sniffed != declared_type:100 raise ValueError("content does not match declared type") # never trust filename/Content-Type101```102103Enforce `MAX_UPLOAD` at the web server (nginx `client_max_body_size`) as well —104before the app buffers anything.105106## Gotchas107108- **`extra="ignore"` (the common default)** silently drops attacker fields today and mass-assigns them after the next model refactor; always `forbid`.109- **Unanchored regex** — `re.search("[a-z]+")` passes `"$(rm -rf /)abc"`; anchor `^...$` and prefer `fullmatch`.110- **Unicode homoglyphs after validation** — normalize (NFC/NFKC) first or `café` and `café` (combining accent) count as different users.111- **`int` coercion of booleans** — in Python `True` is an `int`; pydantic v2 `strict=True` or explicit `StrictInt` where it matters.112- **Trusting Content-Length** — read with a hard cap; a lying client otherwise OOMs the worker.113- **Validating after parsing huge JSON** — depth/size bombs hit the parser first; cap body size and nesting at the server layer.114- **First-error-only responses** — clients fix one field per round-trip; return all errors at once (see handler above).115