From fb163f07d413720b07648be6943f6b7f9298f979 Mon Sep 17 00:00:00 2001 From: estorozhenko Date: Sun, 6 Sep 2026 13:51:25 +0000 Subject: [PATCH] Initial commit: Hermes skill fastapi-microservice --- SKILL.md | 171 ++++++++++++++++++++++++++++++++++++ references/md2vk-example.md | 90 +++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 SKILL.md create mode 100644 references/md2vk-example.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..88e7155 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,171 @@ +--- +name: fastapi-microservice +description: "Bootstrap FastAPI: SQLAlchemy, auth, Docker, one-pass build." +version: 1.0.0 +author: Hermes Agent +license: MIT +platforms: [linux, macos] +metadata: + hermes: + tags: [fastapi, microservice, bootstrap, backend, python, docker] + related_skills: [task-list-execution, writing-plans] +--- + +# FastAPI Microservice Bootstrap + +## When to Use + +The user asks to create a new FastAPI-based microservice. Signals: + +- "создай каталог /opt/xxx, спланируем и напишем сервис" +- "нужен сервис для X с API на FastAPI" +- "bootstrap a new API service for Y" +- Any request where the output is a Python/FastAPI server with routes, database, and external integration + +## Core Rules + +### 1. No analysis preamble — start immediately + +The user says "создай каталог и спланируем" — your first action is to create the directory. Do not: + +- ❌ Summarize the requirement back to them +- ❌ Ask clarifying questions about scope +- ❌ Propose technology choices for approval +- ❌ Write a multi-step plan document before coding + +✅ Instead: `mkdir -p /opt/project/app` → start writing files + +**Investigation is part of execution, not preparation.** If you need to check Python version, installed packages, or port availability, do it as the first action of writing code, not as a preamble. + +### 2. Build in layers, bottom-up + +Write files in this order for maximum efficiency: + +``` +Layer 1 — Foundation + ├── requirements.txt + ├── .env.example + ├── __init__.py files (empty, create dirs) + ├── app/config.py # pydantic-settings from env + ├── app/security.py # encryption, key generation + ├── app/database.py # async engine + session + └── app/models.py # ORM models (User, Account, etc.) + +Layer 2 — Business logic + ├── app/converters/ # domain-specific converters + └── app/vk_client.py # external API clients + +Layer 3 — API + ├── app/api/schemas.py # Pydantic request/response + ├── app/api/deps.py # auth dependencies + └── app/api/v1.py # route handlers + +Layer 4 — Entry point + └── app/main.py # FastAPI app + lifespan + +Layer 5 — Infrastructure + ├── Dockerfile + ├── docker-compose.yml + ├── Makefile + └── .gitignore +``` + +### 3. Generate secrets and verify immediately + +After writing code, do NOT stop at "code is written". Do: + +1. Generate encryption key: `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" > secrets/token_encryption_key` +2. Install deps: `pip install -r requirements.txt` +3. Verify all modules import: `python3 -c "from app import ..."` +4. Start server and test: curl health endpoint +5. Test at least one API endpoint (e.g. convert endpoint if it's a converter service) + +### 4. Write all project files in one pass + +Do not save README.md and STATUS.md separately from code. Write them last, after the code compiles and runs: + +- **README.md** — API reference, usage examples, security, run instructions +- **STATUS.md** — implementation status table, phases, architecture diagram, how to run +- **Dockerfile** — multi-stage, slim, read-only rootfs, non-root user +- **docker-compose.yml** — service + volume + secret + healthcheck +- **Makefile** — dev/install/run/test/key/user commands + +### 5. Security by default + +Every microservice needs: + +- **Encrypted secrets** — Fernet (AES-128-CBC + HMAC-SHA256), key from file/secret, NOT in code +- **API key auth** — sha256 hashed, not stored in plaintext +- **API key ≠ external token** — the API key lets you *use* the service, not read the upstream token +- **Minimal scope** — upstream tokens should have minimal permissions (e.g. VK wall scope only) +- **Docker read-only rootfs** — only /data writable +- **Non-root user** in Docker container + +Log these in README.md as a security matrix table. + +## Expected File Structure + +``` +/opt// +├── app/ +│ ├── __init__.py +│ ├── main.py +│ ├── config.py +│ ├── database.py +│ ├── models.py +│ ├── security.py +│ ├── vk_client.py # or external_api.py +│ ├── converters/ +│ │ ├── __init__.py +│ │ └── markdown_to_vk.py # or relevant converter +│ └── api/ +│ ├── __init__.py +│ ├── schemas.py +│ ├── deps.py +│ └── v1.py +├── tests/ +│ └── (TODO after code) +├── secrets/ +│ └── token_encryption_key # NOT in git (.gitignore it) +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +├── .env.example +├── .gitignore +├── Makefile +├── README.md +└── STATUS.md +``` + +## Common Pitfalls + +| Pitfall | Fix | +|---------|-----| +| Writing README before code runs | Write docs last — they reflect what's real | +| Asking for approval on every file | Write all files in one batch, then verify | +| Skipping security docs | Include security matrix table in README | +| Not testing the API after writing code | Always start server and curl at least health + one endpoint | +| Using `&` in foreground terminal command | Use `terminal(background=True)` for server, then `curl` | +| Hand-typing code that could be written | Batch independent file writes in the same turn | +| Explaining the design before building | Say "Начинаю" and write files — the code IS the design | + +## Verification Checklist + +After writing all code: + +- [ ] `pip install -r requirements.txt` succeeds +- [ ] All modules import: `python3 -c "from app import config, database, models, security, ..."` +- [ ] Encryption key generated and stored in secrets/ +- [ ] Encrypt/decrypt round-trip works +- [ ] API keys generate and verify +- [ ] Converter (if any) produces correct output +- [ ] Server starts: `uvicorn app.main:app --host 0.0.0.0 --port N` +- [ ] `GET /api/v1/health` returns `{"status":"ok"}` +- [ ] At least one POST endpoint responds correctly (e.g. convert or account create) +- [ ] Swagger UI at `/docs` returns 200 +- [ ] Dockerfile builds (if docker is available) + +## Related Skills + +- **task-list-execution** — for executing tasks from STATUS.md files +- **writing-plans** — for multi-step implementation plans with subagent delegation \ No newline at end of file diff --git a/references/md2vk-example.md b/references/md2vk-example.md new file mode 100644 index 0000000..af16d44 --- /dev/null +++ b/references/md2vk-example.md @@ -0,0 +1,90 @@ +# md2vk — FastAPI microservice example + +**Project:** VK wall publisher — converts Markdown to VK format_data, publishes via VK API. +**Pattern:** FastAPI + async SQLAlchemy + SQLite + Fernet-encrypted tokens + external API client. + +## Architecture decisions + +| Decision | Choice | Why | +|----------|--------|-----| +| Database | SQLite + aiosqlite | Lightweight, no external deps, fits Docker | +| ORM | SQLAlchemy 2.0 async | Async-first, well-typed, portable | +| Token encryption | Fernet (cryptography) | AES-128-CBC + HMAC, simple API, key from file | +| Auth | API key (sha256 hash) | API key !== VK token, token never leaves encrypted storage | +| Markdown conversion | Custom parser | VK format_data is a niche JSON format, no lib exists | +| HTTP client | httpx | Async, modern, timeouts by default | +| VK API scope | wall only | Minimal permissions — can't read DMs, photos, etc. | + +## Layer structure + +``` +app/config.py → pydantic-settings, reads TOKEN_ENCRYPTION_KEY_FILE +app/security.py → Fernet encrypt/decrypt + sha256 API key hash +app/database.py → create_async_engine, async_sessionmaker, init_db() +app/models.py → User, VkAccount, Publication (SQLAlchemy 2.0 mapped) +app/vk_client.py → VkClient class: wall.post, users.get, check_token() +app/converters/ → markdown_to_vk(): returns list of chunks with FormatItem[] +app/api/schemas.py → 12 Pydantic models (Request/Response) +app/api/deps.py → get_current_user_from_header(), get_user_by_api_key() +app/api/v1.py → 6 endpoints: health, accounts CRUD, publish, convert, publications +app/main.py → FastAPI() + lifespan (init_db on startup) +``` + +## Key implementation details + +### Fernet key loading +```python +# app/config.py +@field_validator("fernet_key", mode="before") +@classmethod +def load_key(cls, v: Any, info: ValidationInfo) -> str: + key_file = info.data.get("token_encryption_key_file") + if key_file and Path(key_file).exists(): + return Path(key_file).read_text().strip() + raise ValueError("...") +``` + +### VK format_data converter +The converter parses Markdown AST into VK's `format_data` JSON which uses offset-based formatting: +```json +{"version": 1, "items": [ + {"type": "bold", "offset": 0, "length": 5}, + {"type": "link", "offset": 12, "length": 4, "url": "https://vk.com"} +]} +``` + +Handles: **bold**, *italic*, `inline_code`, [links](url), #h1-######, > quotes, ```code blocks```, ---. + +### API key auth dual-mode +- **Header mode**: `Authorization: Bearer ` — for GET/DELETE (accounts list, delete) +- **Body mode**: `api_key` field in POST body — for publish, create account, list publications +- Both call `get_user_by_api_key()` which does sha256 → query User table + +## Verification commands from session + +```bash +# Generate key +python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" > secrets/token_encryption_key + +# Verify imports +TOKEN_ENCRYPTION_KEY_FILE=secrets/token_encryption_key \ + python3 -c "from app import config, database, models, security, vk_client; from app.converters import markdown_to_vk; from app.api import schemas, deps, v1; print('OK')" + +# Start server +TOKEN_ENCRYPTION_KEY_FILE=secrets/token_encryption_key \ + uvicorn app.main:app --host 0.0.0.0 --port 8000 + +# Test +curl http://localhost:8000/api/v1/health +curl -X POST http://localhost:8000/api/v1/convert \ + -H "Content-Type: application/json" \ + -d '{"message_md":"**bold** text"}' +``` + +## Key counts (from session) + +- 9 source files (excluding __init__.py, config, and infrastructure) +- ~422 lines in converter (largest module) +- ~310 lines in API v1 router (6 endpoints) +- 3 ORM models: User, VkAccount, Publication +- 4 infrastructure files: Dockerfile, docker-compose.yml, Makefile, .gitignore \ No newline at end of file