Files
nxdns/AGENTS.md
T
mokhtar ce143d1d87
Gates / frontend (push) Successful in 1m43s
Gates / test (push) Successful in 2m14s
Gates / test-aarch64 (push) Successful in 8m3s
Gates / package (push) Successful in 5m42s
Gates / container (push) Successful in 54s
CI / gates (push) Successful in 50m24s
db-mode config changes apply live in-process
settings and upstream writes now follow a prepare, commit, publish, retire
contract: candidates are built and validated before the database transaction,
published as infallible pointer swaps, and old generations retire after their
readers drain. per-query policy values snapshot once per query; upstream pool,
cache, rate limiter, sessions, api limiter, log sink, blocklist scheduler and
the query-log queue each gained one named live operation. restart_required
shrinks from every scalar key to the bind keys and web.enabled; the admin ui
drops its restart notices for everything else. file mode is unchanged.
2026-08-24 00:04:28 +02:00

5.3 KiB

AGENTS.md

Aim

nxdns: a self-hosted DNS sinkhole for a household LAN, written in Zig 0.16.0. Portfolio-grade public repo. PLAN.md is the source of truth for scope and design; specs/ holds per-milestone contracts; specs/research/ holds verified stdlib facts.

Values

We intentionally architect this code to be robust, maintainable, pragmatic — good craftsmanship and good engineering. We explicitly avoid tech debt, code smells, bad architecture decisions, and brittle implementations.

What that means in practice:

  • This is a greenfield project. Breaking changes are allowed. Never keep a bad interface for compatibility; fix it at the root.
  • No versioning of scope. A feature is in scope (build it completely) or out of scope (do not build it). No "v2 later", no stubs left behind.
  • Fix root causes, not symptoms. Do not iterate on workarounds.
  • Scope is small on purpose: household scale, two targets, few dependencies. Do not add generality nobody asked for.
  • Dependencies are liabilities: stdlib first; vendored + pinned C deps (sqlite3, mbedTLS) only where the stdlib has nothing.
  • Verify stdlib claims against ../zig at tag 0.16.0 — pre-0.16 knowledge is stale (std.Io migration). See specs/research/zig-0.16-api-notes.md.
  • Pure core: dns/, filter/, local/, cache/ take bytes and return bytes — no Io, no sockets, no clocks hidden inside.
  • Every failure mode must be visible: no silent drops, no unbounded logs, no swallowed errors. Counters + health surfaces over log spam.
  • Tests are runnable acceptance criteria, not decoration. Required CI stays deterministic — no network-dependent tests in blocking jobs.
  • Comments state constraints the code cannot show. No narration, no commented-out code.
  • Git: GPG-signed commits (git commit -S), simple lowercase messages, no generated-by footers.

Reading zig build test output

A fully passing zig build test still prints a line like failed command: .../test --cache-dir=... --seed=... --listen=-, and still exits 0. That line is a known upstream zig 0.16.0 labelling defect. It does not mean a test failed, and no test binary crashed.

The build runner sets a step's result_failed_command on every spawn (std/Build/Step/Run.zig:1540) and never clears it on success. It then prints a step's diagnostics whenever the step wrote anything to stderr, explicitly "no matter the result" (compiler/build_runner.zig:1381), and that printer emits the failed command: label unconditionally when the field is set (compiler/build_runner.zig:1515). Our suite writes to stderr on every run, because the tests that cover the warning paths log through the real sink. A minimal reproducer with no mbedTLS and no C — one passing test whose body is a std.debug.print — prints the same label and reports "3/3 steps succeeded; 1/1 tests passed"; deleting the print removes the label. No upstream issue matched a search, so the reference is the 0.16.0 source lines above.

Any other failure text is real. Trust the summary line: zig build test exiting non-zero, a N failed count, or a panic backtrace all mean a genuine failure. Do not filter, wrap, or suppress the runner's output to hide the label — that would hide real failures with it.

One trap: running a cached test binary by hand with --listen=- aborts with internal test runner failure: EndOfStream. That is not a teardown bug; the IPC runner is talking to a closed stdin because no build runner is on the other end. Run the binary with no arguments to get the plain stdio report.

Debug-mode miscompile: a bool live across an atomic read-modify-write

In Debug the x86_64 self-hosted backend is the default, and zig 0.16.0's atomic read-modify-write lowering there does not invalidate a bool the register allocator is still tracking in EFLAGS. The bool silently becomes the flags the lock xadd left behind. Release modes go through LLVM and are unaffected, so this can only ever break zig build test, never a shipped binary.

The shape to avoid is a comparison whose result stays live across fetchAdd/fetchSub/@atomicRmw and is then branched on:

const idle = old.refs == 0;              // sete 0x50(%rsp)   -- correct
_ = self.published.fetchAdd(1, .monotonic); // lock xadd %rdi,(%rsi)
                                         // sete 0x51(%rsp)   -- bogus, reads EFLAGS from the xadd
return if (idle) old else null;          // branches on 0x51, not 0x50

That function returns null for every input. fetchAdd is the only trigger: a plain += 1, an atomic load, and an atomic store in the same slot all compile correctly, and inserting any call (including std.debug.print) between the comparison and the branch forces a spill that hides it. Build the same file with -fllvm or -OReleaseSafe to confirm a suspected instance.

Owner.published in src/upstream/owner.zig is a plain u64 under the owner's mutex for this reason. Do not "modernize" it to std.atomic.Value(u64).

Regenerating the contract samples

admin/src/lib/contractSamples.gen.ts is a committed golden of canonicalized API responses, byte-compared against the live server by a -Dintegration test and type-checked by tsc. After a deliberate API contract change, regenerate it with:

zig build test -Dintegration -Dcontract-samples-out="$PWD/admin/src/lib/contractSamples.gen.ts"

then update admin/src/lib/types.ts to match and commit both. Never edit the generated file by hand.