# 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. ```sh 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 `source`d, 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` (group `incus-admin`). The panel daemon (`vps.service`) runs as the unprivileged `vps` user, which is a member of `incus-admin`; all Incus access is via the REST API over that socket. - `internal/lx` is the ONLY package that talks to Incus. Exec is done over the API websocket transport (`Exec`/`ExecSH`/`RunInitScript`) — there must be no `incus`/`lxc` CLI calls in the panel runtime path. - Storage pool `vpsmgr` (ZFS by default, bridge `incusbr0`). `btrfs` is supported as an alternative backend (on a btrfs host it becomes a native subvolume; elsewhere a loop file) — `VPSMGR_STORAGE=btrfs`. A `dir` backend exists only as an explicit `VPSMGR_STORAGE=dir` opt-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 is `lx.EnsureZFSRemoveSnapshots`, which is a no-op off ZFS. - A remote-qualified fallback image (`images:debian/13`) must go through `lx.EnsureImage` before `lx.Launch` — the API cannot auto-fetch it inside the create call (unlike the old `lxc launch` CLI). ## Privilege model - `vps install` runs as root (setup: user creation, units, kernel modules). - `vps serve` runs as the unprivileged `vps` user. Root commands are pinned in `/etc/sudoers.d/vps` (installed by `ensureSudoers`, validated with `visudo -c`); the panel escalates only those exact commands via `internal/su` (`sudo -n`). Never add a new `exec.Command("...")` that needs root without also adding it to the whitelist. ## Documentation - `docs/README.md` — index - `docs/architecture.md` — system design (storage/network/security/bandwidth) - `docs/configuration.md` — `/etc/vpsmgr/config.yaml` reference - `docs/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.sh` **without** `--purge` must keep `/etc/vpsmgr` (config/db) and `/etc/haproxy` so reinstall adopts them; only `--purge` deletes them. A pre-HAProxy install's `/etc/traefik` is 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-build` must always rebuild (never reuse an installed binary) and warn about the branch. `--update` re-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.sh` and `90-arch-image.sh` are optional and must never be part of `install.sh` (small boxes stay lean). `90-arch-image.sh` is 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 via `systemctl restart vps`). Do not reintroduce `vpsmgr-*.service` names on the host. (The in-container `vpsmgr-ipv6.service` helper is a different thing — it lives inside container images.) ## Conventions - One commit per small bug/feature; short subject + bullet body. - Run `git log --oneline -10` for 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.