Files
steward/AGENTS.md
T
abrown38andClaude Opus 5.5 3c0808591f Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
Subnets served per interface, pools, reservations, options at three
levels, leases in SQLite saved before the ACK, ping and decline conflict
checks, validated changes with history and undo, users and tokens, an
audit log, a Kea importer, a sandboxed systemd unit and Debian packages.
Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:14:07 +01:00

104 lines
4.6 KiB
Markdown

# Working on Steward
Notes for coding agents (and people) changing this repository. Read this
first, then [docs/design.md](docs/design.md) (how it fits together and
what 0.1 leaves out) and [docs/development.md](docs/development.md)
(layout, tests, releasing).
Steward is a DHCPv4 server: `stewardd` answers DHCP and serves a REST API
and a web UI; `steward` is the CLI; SQLite holds everything. It is the
companion of Jarl (the BIND manager, `../jarl`) and copies its ways:
the same auth, socket and web UI helpers, and the same release process.
When in doubt, do it the way Jarl does. Go module `steward`,
`CGO_ENABLED=0`, Linux only for stewardd.
## Ground rules
* **Configuration changes go through `engine.Apply`** (or `Revert`,
`Replace`). It edits a copy of the state, diffs it, validates the whole
result, saves the objects and a history entry in one transaction, then
hands the new state to the DHCP server. Never write subnets,
reservations or settings to the store from a handler.
* **Leases are not configuration.** `dhcpd` owns them: kept in memory, and
written to the store (`PutLease`) before the ACK is sent. They are not
in the history.
* **Subnets are per interface.** A request is only answered from the
subnets of the interface it came in on. Keep that true in every code
path.
* **Every user-visible change updates `CHANGELOG.md`** under
`## [Unreleased]`, and the matching `docs/` page in the same change.
New validation codes get a row in `docs/validation.md`; new options go
in `internal/dhcp4/catalog.go`; then run `python3 scripts/optionsdoc.py`
to regenerate `docs/options.md`.
* **Errors** say what is wrong and how to fix it, naming the object.
* **Schema changes** append a migration to `internal/store/store.go`;
never edit a shipped one.
* `make test` runs `go vet` and a `gofmt -l` check. `goimports` isn't
installed, so fix imports by hand.
## Testing
```sh
make test # unit tests: the protocol through dhcpd.Server.Handle with fakes, API via httptest
make e2e # real udhcpc and dhcpcd clients against stewardd in Docker (needs Docker)
```
* **Docker** on this machine needs `DOCKER_HOST=unix:///var/run/docker.sock`.
The e2e test prints `RESULT: N passed, 0 failed`; run it after any change
to `dhcpd`, `engine`, `server` or the CLI.
* **Port 67 needs `CAP_NET_BIND_SERVICE`.** A stewardd run by hand shows
its interfaces as "not serving: permission denied", but the API and web
UI work, which is fine for UI work. Use a scratch config with
`listen: ["127.0.0.1:28067"]` and `socket: /run/user/$UID/...`; socket
paths are limited to 107 bytes.
* **The systemd sandbox is only tested by a package test**: run `make
deb`, install it in a `debian:trixie` container running systemd
(`--privileged --cgroupns=host /sbin/init`) on a Docker network, and run
a client container on that network.
* **The web UI** has no JS tests. Bundle `assets/app.js` with esbuild to
catch syntax and import errors, and look at it in headless Chrome.
## Web UI rules
As in Jarl:
* Build DOM only with `h()` from `ui.js`, never `innerHTML`.
* The CSP is `self` only: no inline styles or scripts. Use a class, or set
`el.style.prop`.
* Rebuild stewardd after editing, since the files are embedded.
* Function over polish. Leave pick-list checkboxes unticked by default.
## DHCP facts worth knowing
* Replies to clients without an address are broadcast on the interface
(255.255.255.255:68 through the interface's socket). That works with
every client and needs no raw socket. Renewals (`ciaddr` set) are
unicast.
* `SO_BINDTODEVICE` needs no capability since Linux 5.7. The ping check
uses an unprivileged ICMP socket, allowed by
`net.ipv4.ping_group_range`.
* Renewal REQUESTs often have no host name: keep the lease's previous one.
* dhcpcd sends a DUID-based client identifier, busybox udhcpc none.
Leases match by client ID when both sides have one, else by MAC address.
* A container's interface names follow the order the networks were
attached in (eth0 first).
## Git and releases
The maintainer's rules:
* **Commit only when asked. Never push.** The maintainer pushes, to Gitea
at gitea.adbraun.lol.
* **Branches** are `dev-X.Y.Z`, never named like a tag.
* **A release** is a commit "Release X": it sets `VERSION`, updates the
README status line, and (for a final release) moves `[Unreleased]` to
`## [X] - YYYY-MM-DD`. The annotated tag is `vX` with message
"Steward X".
## Shell hygiene
* **Never kill processes by pattern** (`pkill -f`). The pattern matches
the shell running it. Find the PID with `ps` and kill that.
* **Run long-lived test servers in the background mode of your tool**, not
with `&` in a shell that exits.