- add the rule to the conventions with the documentation ranges to use (RFC 5737 for IPv4, RFC 3849 for IPv6) and note that fixtures, docs and commit messages are all covered - record why: a real prefix names an operator's live allocation, the block arithmetic is length-driven so a doc-range prefix substitutes unchanged, and scrubbing a leak later means rewriting published history (force-pushed tags, broken forks/clones). Host addresses, credentials and provider/ASN names are called out as the same class of leak Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
5.6 KiB
AGENTS.md
Guide for AI coding agents working in this repo.
Project
vpsmgr — a lightweight LXC hosting panel. Debian 13/Ubuntu host + Incus containers, one Debian 13 container per user, managed via a web panel and a CLI (single Go binary). Hosting-panel concepts, not an ordinary web app: installers, containers, networking, storage.
Commands (always set CGO_ENABLED=0)
The project is pure Go — no cgo, never enable it. net (stdlib) pulls in
runtime/cgo whenever a C toolchain is present, which fails on machines with
broken gcc.
cd src
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go test ./...
CGO_ENABLED=0 go vet ./...
Shell scripts: check syntax with bash -n scripts/*.sh. Note: install.sh
and 00-ip-ask.sh (the install-time network asks — IPv6 prefix + container
subnet octet) are sourced, so they must use return, not exit.
Incus (not LXD)
The container runtime is Incus 7 LTS, installed from the Zabbly package
repo (10-incus.sh) — never snap, never the lxc CLI.
- Daemon socket:
/var/lib/incus/unix.socket(groupincus-admin). The panel daemon (vps.service) runs as the unprivilegedvpsuser, which is a member ofincus-admin; all Incus access is via the REST API over that socket. internal/lxis the ONLY package that talks to Incus. Exec is done over the API websocket transport (Exec/ExecSH/RunInitScript) — there must be noincus/lxcCLI calls in the panel runtime path.- Storage pool
vpsmgr(ZFS by default, bridgeincusbr0).btrfsis supported as an alternative backend (on a btrfs host it becomes a native subvolume; elsewhere a loop file) —VPSMGR_STORAGE=btrfs. Adirbackend exists only as an explicitVPSMGR_STORAGE=diropt-in for test boxes — never a fallback; a failed pool (ZFS/btrfs) must abort, not downgrade. Container management is storage-driver-transparent in the Go panel; the only driver- aware code islx.EnsureZFSRemoveSnapshots, which is a no-op off ZFS. - A remote-qualified fallback image (
images:debian/13) must go throughlx.EnsureImagebeforelx.Launch— the API cannot auto-fetch it inside the create call (unlike the oldlxc launchCLI).
Privilege model
vps installruns as root (setup: user creation, units, kernel modules).vps serveruns as the unprivilegedvpsuser. Root commands are pinned in/etc/sudoers.d/vps(installed byensureSudoers, validated withvisudo -c); the panel escalates only those exact commands viainternal/su(sudo -n). Never add a newexec.Command("...")that needs root without also adding it to the whitelist.
Documentation
docs/README.md— indexdocs/architecture.md— system design (storage/network/security/bandwidth)docs/configuration.md—/etc/vpsmgr/config.yamlreferencedocs/ipv6.md— IPv6 pass-through design (large dev-branch feature)docs/development.md— build/test/release/conventions
Keep the top-level READMEs concise; technical detail belongs in docs/
(English).
Key invariants — do not break
uninstall.shwithout--purgemust keep/etc/vpsmgr(config/db) and/etc/haproxyso reinstall adopts them; only--purgedeletes them. A pre-HAProxy install's/etc/traefikis kept by the same rule (its config file is inert; the installer removes Traefik's binary/unit/account and the per-domain dynamic directory).- IPv6 bridge prefix is clamped to ≥ /64 (
bridgePrefixLen): Incus's dnsmasq only serves /64 networks. Container addresses always live in the first /64 of the configured prefix. - IPv6 prefix length is required in
ipv6_subnet— a bare address is rejected, never silently treated as /64. - Container hostnames are random (
vps-<8hex>) and never equal the username. install.sh --local-buildmust always rebuild (never reuse an installed binary) and warn about the branch.--updatere-downloads the latest prebuilt release over an existing binary and must keep it untouched on download failure (no fallback rebuild).- Image builds (
50-image.sh,60-rhel-image.sh,80-debian-dev-image.sh) must stay slim (apt/dnf/pacman clean) and delete the base image after publishing.60-rhel-image.sh,70-opensuse-image.sh,80-debian-dev-image.shand90-arch-image.share optional and must never be part ofinstall.sh(small boxes stay lean).90-arch-image.shis a rolling rebuild: it deletes the existing image and re-pulls upstream on every run. - Never add cgo or force C compilation.
- The panel service is
vps.service(restart viasystemctl restart vps). Do not reintroducevpsmgr-*.servicenames on the host. (The in-containervpsmgr-ipv6.servicehelper is a different thing — it lives inside container images.)
Conventions
- One commit per small bug/feature; short subject + bullet body.
- Run
git log --oneline -10for the current commit style before writing a message. - Never put a real IP address in the repo. Tests, fixtures, examples, docs
and commit messages use the documentation ranges only: IPv4 RFC 5737
(
192.0.2.0/24,198.51.100.0/24,203.0.113.0/24) and IPv6 RFC 3849 (2001:db8::/32). Test fixtures are where this leaks most often, and a real prefix names an operator's live allocation (their provider block). The block arithmetic is length-driven, so a doc-range prefix substitutes unchanged. Scrubbing a leak afterwards means rewriting published history — force-pushed tags and every fork/clone broken — so it must never get in. Host addresses, credentials and provider/ASN names belong in the same bucket. - The test environment has ~2 GiB RAM; keep CI/local test workloads light.