SPB Git

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%
3.9 KB · 115 lines markdown
Rendered Raw Blame History
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