Backend for Gitnet — an autonomous AI software engineering platform. FastAPI + async SQLAlchemy + Postgres, with a GitHub App integration, a sandboxed project workspace, a controlled command execution layer, and a Gemini-powered AI agent that works through the same tools a human user has.
Frontend (Next.js, Vercel) is a separate repository and talks to this API over HTTPS + JWT bearer auth.
app/
core/ settings, db engine, JWT, password hashing, error types, logging
models/ SQLAlchemy models (see schema.sql for the raw DDL)
schemas/ Pydantic request/response models
routers/ FastAPI routers (one per resource area)
services/ auth, GitHub App client, import, log broker, cleanup
workspace/ sandboxed file/dir access, git operations, zip import
commands/ controlled subprocess executor + Gitnet command registry + workflow engine
ai/ Gemini client, tool schemas, tool dispatcher, agent loop, skills/rules
alembic/ DB migrations
tests/ pytest suite (unit + integration)
schema.sql hand-maintained mirror of the SQLAlchemy models, for direct review/apply
Every file and command operation for a project workspace funnels through a small number of choke points, deliberately kept small so they're easy to audit:
app/workspace/paths.py—resolve_workspace_path()is the only sanctioned way to turn a user-supplied relative path into a filesystem path. It rejects absolute paths, null bytes, and anything that resolves (after following symlinks) outside the workspace root. All file read/write/delete/list operations go through this.app/workspace/zip_import.py— validates every entry in an uploaded ZIP before extracting anything: rejects path traversal, symlink entries, oversized files, and suspiciously high compression ratios (zip bombs).app/commands/executor.py— the only place a subprocess is ever spawned. No shell is invoked (asyncio.create_subprocess_exec, notshell=True), the binary must be on an explicit allowlist, a denylist of shell metacharacters is checked, the working directory is pinned to the project workspace, the environment is scrubbed of backend secrets, and every call has a hard timeout.- AI tools (
app/ai/tool_dispatcher.py) call into these exact same primitives — the AI has no separate, less-restricted code path. If a human can't do it through the API, the AI can't do it either.
Requires Python 3.12, Postgres 14+, and pip.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# fill in DATABASE_URL, SESSION_SECRET at minimum to run locally
# apply the schema (either works; Alembic is authoritative for prod)
psql "$DATABASE_URL" -f schema.sql
# or:
alembic upgrade head
uvicorn app.main:app --reloadVisit http://127.0.0.1:8000/health to confirm it's up, /docs for interactive
API docs (FastAPI's built-in Swagger UI).
pip install -r requirements.txt
createdb gitnet_test # or point DATABASE_URL at any throwaway Postgres db
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost/gitnet_test \
ENV=test pytest -qThe suite creates its own tables on first run and truncates between tests — no
manual fixture data needed. Security-critical paths (path traversal, zip-slip,
symlink injection, zip bombs, command injection, cross-user authorization) each
have dedicated tests in tests/unit/ and tests/integration/.
Database: Neon (serverless Postgres) works well for the free tier. Make sure
your DATABASE_URL includes ?ssl=require (or sslmode=require) for Neon.
Backend: Render, as a standard Python web service.
- Build command:
pip install -r requirements.txt - Start command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT - Set all variables from
.env.examplein Render's environment settings. - Run
alembic upgrade headonce (Render's shell, or a one-off deploy hook) before the first request hits a fresh database.
Frontend: Vercel, pointed at ALLOWED_ORIGINS for CORS.
GitHub App: create one at https://github.com/settings/apps/new with:
- Repository permissions: Contents (read/write), Pull requests (read/write), Metadata (read)
- Webhook URL:
https://<your-backend>/github/webhook - Generate a private key from the app settings page and set it as
GITHUB_PRIVATE_KEY(escape newlines as\nif storing as a single env var).
Gemini: get API keys from Google AI Studio. Up to 3 keys can be configured
(GEMINI_API_KEY_1/2/3) — Gitnet rotates to the next key automatically when one
hits a quota/rate error, with a cooldown before retrying an exhausted key. This
is entirely optional; /ai/tasks will return a clear error if no keys are set,
everything else works without it.
| Area | Routes |
|---|---|
| Auth | POST /auth/register, /login, /refresh, /logout, GET /me, password reset, session management |
| Projects | GET/POST /projects, POST /projects/import/github, POST /projects/import/zip, DELETE /projects/{id} |
| Files | GET .../files, /files/read, PUT /files/write, DELETE /files, GET /files/search |
| Git | GET .../git/status, /git/diff, POST /git/commit, /git/push, /git/pull-request |
| Commands | POST .../commands — runs a controlled Gitnet command (status/diff/add/branch/install/build/test/run) |
| Workflows | GET/POST .../workflows, POST .../workflows/{id}/run |
| Logs | GET .../logs/stream — Server-Sent Events, real-time |
| GitHub | installation callback, list installations/repositories, webhook receiver |
| AI | POST /ai/tasks (runs in the background), GET /ai/tasks/{id}, GET /ai/tasks?project_id= |
See direct.md for the exact request/response shapes and the Gitnet command /
workflow-step vocabulary the frontend and AI both target.
- Ephemeral workspaces. Project files live on local disk under
WORKSPACE_ROOTand are cleaned up afterWORKSPACE_IDLE_TTL_MINUTESof inactivity. Re-importing (GitHub re-clone or ZIP re-upload) restores a workspace; project metadata and history survive in Postgres regardless. This keeps the backend stateless-enough to run comfortably on Render's single-instance free/starter tiers without needing a persistent volume or object storage for an MVP. - Single-instance log streaming. The real-time log broker
(
app/services/log_broker.py) is in-process pub/sub, not backed by Redis. Fine for one backend instance; would need a shared channel to scale horizontally. - AI agent step ceiling. Each AI task caps at
MAX_AGENT_STEPS(12) tool calls to bound cost and prevent runaway loops, not because the model can't usefully do more.