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

4.6 KiB

Working on Steward

Notes for coding agents (and people) changing this repository. Read this first, then docs/design.md (how it fits together and what 0.1 leaves out) and 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

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.