|
| 1 | +# Working in codefly-dev/service-python-fastapi |
| 2 | + |
| 3 | +This repo is one codefly agent binary: the **FastAPI specialization** of the |
| 4 | +generic Python agent. It composes `github.com/codefly-dev/service-python`'s |
| 5 | +`pkg/*` types and adds what FastAPI needs — uvicorn supervision and hot reload, |
| 6 | +OpenAPI-derived REST endpoints, an opt-in `grpc.aio` listener, the Dockerfile |
| 7 | +and kustomize recipes, and the scaffold a new FastAPI service is created from. |
| 8 | +Dependency management is `uv`; poetry is not supported. |
| 9 | + |
| 10 | +It does **not** own: |
| 11 | + |
| 12 | +- **Generic Python behaviour** — `uv`, AST/ruff/pytest tooling, the Python |
| 13 | + `Code` and `Tooling` surfaces. Those come from `service-python` and are |
| 14 | + wired through unchanged in `main.go`. A defect there is fixed there. |
| 15 | +- **The resource, network and configuration model** — ports, endpoints, |
| 16 | + environment injection, readiness, the agent gRPC contracts. That is |
| 17 | + `codefly-dev/core`. |
| 18 | +- **The `codefly` CLI** (`codefly-dev/cli`, private — do not import it, do not |
| 19 | + make CI depend on it) and user workspaces. |
| 20 | + |
| 21 | +When the change you need belongs to one of those, it gets made there. |
| 22 | + |
| 23 | +## How to behave |
| 24 | + |
| 25 | +Fleet standard — [handbook#68](https://github.com/obin-ai/handbook/issues/68). |
| 26 | +These bite here because this agent sits between core's resolution machinery and |
| 27 | +a user's Python process: nearly every defect is observed as "the service does |
| 28 | +not come up", and the local substitute for whatever did not resolve is always |
| 29 | +within reach. |
| 30 | + |
| 31 | +- **A gap in the tooling is a bug in the tooling — never a reason to reach |
| 32 | + around it.** When something needs a step `codefly`, `uv` or a companion image |
| 33 | + does not perform, the answer is a capability fixed in whichever tool owns it, |
| 34 | + and named in the PR. Not a hand-assembled substitute — not as a "workaround", |
| 35 | + not "just this once", not "until the capability lands". |
| 36 | +- **Never hack. Provide the best fix, even when it spans repos.** The fix |
| 37 | + living in `core` or `service-python` is not a reason to work around it here. |
| 38 | + Open the PR there and consume the reviewed result. When it genuinely cannot |
| 39 | + be fixed now, the deliverable is a precise issue against that owner plus an |
| 40 | + explicitly labelled stopgap — never an unlabelled one. |
| 41 | +- **Classify every change that makes something work**, in the PR body: a *fix* |
| 42 | + at the place that owns the behaviour, or a *hack*. A hack does not become a |
| 43 | + fix by working, by being small, by being local, or by the real fix belonging |
| 44 | + elsewhere. |
| 45 | +- **Never hardcode what the system resolves.** Ports, endpoint addresses, |
| 46 | + injected environment and credentials are resolved *for* this agent and then |
| 47 | + published by it: `Init` takes the port out of the network mappings, and the |
| 48 | + environment the Python process receives is assembled by core's |
| 49 | + `EnvironmentVariableManager` from endpoints and workspace configurations. |
| 50 | + Typing one of those values in encodes something true only on one machine for |
| 51 | + ten minutes — and it fails *quietly*: a runtime missing a credential can skip |
| 52 | + registration silently, so the service boots, serves, and is simply absent. |
| 53 | +- **Diagnose, do not pattern-match.** "It started working when I set X" is not |
| 54 | + a diagnosis — set X back and confirm it breaks. Do not trust an error message |
| 55 | + before checking its claim. `TestCreateToRunDocker` failing with `too many |
| 56 | + tries` is the standing example: it reports a readiness timeout and usually |
| 57 | + means the shared Docker daemon is contended, not that the diff broke startup. |
| 58 | +- **Say what you did not verify.** Unverified is not the same as working. The |
| 59 | + Docker-backed and image-scanning tests are the slow, real ones; if you did not |
| 60 | + run them, the PR says so. |
| 61 | + |
| 62 | +## Build and test |
| 63 | + |
| 64 | +Derived from `.github/workflows/ci.yml`, which delegates to core's shared |
| 65 | +`go-service-ci.yml` and adds a `race` job of its own. Go comes from `go.mod` |
| 66 | +(1.27.0). All of it runs locally: |
| 67 | + |
| 68 | +```bash |
| 69 | +go build ./... # CI: build job |
| 70 | +go vet ./... # CI: build job |
| 71 | +go mod tidy -diff # CI: build job |
| 72 | +go test ./... # CI: test job (runs it with -v) |
| 73 | +go test -race ./... # CI: race job |
| 74 | +``` |
| 75 | + |
| 76 | +The `race` job exists because the runner lifecycle (Init / Start / Stop / |
| 77 | +Destroy) is concurrent gRPC handlers over shared state: a lost lock is a data |
| 78 | +race, not a failed assertion, so the plain `go test` above passes on code with |
| 79 | +a mutex removed. Touch `runtime.go`'s locking and `-race` is the run that |
| 80 | +matters. The lock order is documented on the struct fields — follow it. |
| 81 | + |
| 82 | +**Nothing is mocked.** The suite drives a real uvicorn through `uv`, real |
| 83 | +containers through the Docker daemon, and a real `syft` scan of an image built |
| 84 | +from the recipe this agent emits. Tests skip rather than fake when a tool is |
| 85 | +absent, so a green run on a bare machine has proved much less than it looks: |
| 86 | + |
| 87 | +| Missing | Silently skips | |
| 88 | +| --- | --- | |
| 89 | +| `uv` | the real uvicorn lifecycle | |
| 90 | +| `docker` | destroy-under-docker | |
| 91 | +| `syft` (+ `docker`, `uv`) | the image SBOM inventory | |
| 92 | + |
| 93 | +CI installs `uv` and pins `syft` v1.48.0 in `setup-run` for exactly that |
| 94 | +reason. Get them locally too, or say which halves you did not exercise. |
| 95 | + |
| 96 | +The suite is slow because it is real — CI finishes inside `go test`'s default |
| 97 | +10-minute timeout, but a contended local Docker daemon pushes the container |
| 98 | +cases past it. A local `panic: test timed out` in the lifecycle matrix is that, |
| 99 | +not a finding; re-run the suite with `-timeout` raised before concluding |
| 100 | +anything from it. |
| 101 | + |
| 102 | +## Where things live |
| 103 | + |
| 104 | +| Path | Owns | |
| 105 | +| --- | --- | |
| 106 | +| `main.go` | settings, agent advertisement, composition of the generic Python layer | |
| 107 | +| `runtime.go` | Init/Start/Stop/Destroy, uvicorn supervision, hot reload, the lock order | |
| 108 | +| `builder.go` | `Create` scaffolding, OpenAPI endpoint loading, Docker + kustomize recipes | |
| 109 | +| `probes.go`, `probe.py` | endpoint health translated to Kubernetes probes / a local probe | |
| 110 | +| `templates/` | **what ships** — embedded via `go:embed`, rendered on `Create` | |
| 111 | +| `base/` | a checked-in sample service, embedded nowhere | |
| 112 | + |
| 113 | +`templates/` and `base/` look interchangeable and are not. Only `templates/` is |
| 114 | +embedded in the binary, so **editing `base/` changes nothing a user receives**; |
| 115 | +it has already drifted from `templates/factory/`. Dependabot watches |
| 116 | +`/base/code` for pip updates and `/templates/builder` for the base image, so a |
| 117 | +dependency bump landing in `base/code` is not a bump of the scaffold — carry it |
| 118 | +into `templates/factory/code/pyproject.toml.tmpl` yourself. |
| 119 | + |
| 120 | +## Rules that bite |
| 121 | + |
| 122 | +- **Endpoint contracts are read from fixed locations in the *generated |
| 123 | + service*** (not paths in this repo): `openapi/api.swagger.json` for REST and |
| 124 | + `proto/api.proto` for gRPC, the latter being core's `standards.ProtoPath`. |
| 125 | + Both Builder and Runtime re-derive endpoints from exactly there, and so does |
| 126 | + any dependent service. The swagger file does not exist at `Create` time — the |
| 127 | + service generates it at runtime from its own `src/openapi.py` — so absent is |
| 128 | + a normal state there, not an error to code around. |
| 129 | +- **Readiness is the endpoint's declared health check, never an open port.** |
| 130 | + Probe intents (readiness / startup / liveness) stay separate through |
| 131 | + `resources.PlanEndpointProbes`; collapsing them silently drops a predicate. |
| 132 | +- **The runtime image is pinned.** `docker-image` rejects a bare name and |
| 133 | + `:latest`. The default `codeflydev/python` companion is built by core and |
| 134 | + repinned on release — prefer it. |
| 135 | +- **A user's `flake.nix` is never overwritten.** The embedded flake is written |
| 136 | + only when the source dir has none. |
| 137 | +- **Release needs cgo.** The binary links core's tree-sitter grammars, so |
| 138 | + releases go through the `goreleaser-cross` image; `conformance_test.go` is |
| 139 | + what keeps `.goreleaser.yaml` and the release workflow honest about it. |
| 140 | + |
| 141 | +## Procedures |
| 142 | + |
| 143 | +Step-by-step procedures live in `.claude/skills/`, loaded on demand rather than |
| 144 | +carried here: |
| 145 | + |
| 146 | +- `lifecycle-test-triage` — a lifecycle test is red or hanging, and you need to |
| 147 | + tell a real regression from daemon contention, a silent skip, or the timeout. |
| 148 | + |
| 149 | +## Workflow |
| 150 | + |
| 151 | +- Branch and PR; never commit to `main`. Conventional Commits for the title. |
| 152 | +- Bumping a dependency: `go get` does not tidy. Run `go mod tidy` afterwards, |
| 153 | + or `ci / build` goes red on a change that passed every other local check. |
| 154 | +- `agent.codefly.yaml` carries the agent version; releases are tag-driven |
| 155 | + (`.github/workflows/releaser.yml`), never hand-built. |
| 156 | +- Keep this file under ~150 lines (hard cap 200). Push depth into a nested |
| 157 | + `AGENTS.md` beside what it describes, or into `.claude/skills/`. |
| 158 | +- There is no `CLAUDE.md`. If one is added, it is a one-line `@AGENTS.md` |
| 159 | + pointer — one canonical source, never a second copy to drift. |
| 160 | +- Treat this file as code: the PR that changes a process updates it. |
0 commit comments