# AGENTS.md CLI for Schlundtech DNS. Go, cobra. Read `README.md` first — it is the source of truth for what the tool does. ## Rules - **Docs and code must never contradict.** Every command in `README.md` must exist, with the flags and output described. If you change the CLI, update `README.md` in the same change. Verify with `go run . --help`. - **Go only.** No other language, no script substitutes. - **gopls is the diagnostic source of truth.** Do not guess at type errors. Run `gopls check ` (or `gopls definition` / `gopls references` when you need to navigate). Fix what gopls reports; do not suppress. - **cobra for the CLI.** One command per file under `cmd/`. Every command needs `Short`, `Long`, and a complete `Example` in its help text. No hand-rolled flag parsing. - **Errors go to stderr, data goes to stdout.** Anything pipeable must not be polluted by log output. - **Never log or echo credentials.** ## Layout ``` main.go entrypoint cmd/ root.go, credentials.go, output.go, one file per command internal/api/ the gateway client: XML types, tasks, record logic ``` `internal/api` is the only place that speaks HTTP to the gateway. Commands must not build requests themselves. This is a personal tool — do not add packages beyond this; shared helpers belong in `cmd/`. ## Workflow ```bash go build # writes ./schlundtech-dns — run that, not `go build ./...` go test ./... gopls check ./... # must be clean before you stop ``` `go build ./...` deliberately writes nothing: with more than one package it only compiles as a check. Use bare `go build`, or `go run . ` to skip the build step entirely. ## Credentials Read from `.env` in the working directory, falling back to the process environment. `godotenv` parses the file, but the precedence is ours: **a real environment variable always wins**, so a one-off override never needs a file edit. Never commit `.env`; `.env.example` is the committed template. Never log or echo a credential, and keep a test that proves the password cannot reach an error message. Writes to DNS are public and propagate immediately — every mutating command needs a `--dry-run` flag. ## Generated files `.opencode/skill/`, `.opencode/agent/`, `.opencode/command/` are generated from the `maister` submodule. Never edit them; run `node tools/build-maister-opencode.mjs` and `node tools/verify-maister-opencode.mjs` after `git -C maister pull`. ## References - **The gateway is AutoDNS.** `gateway.schlundtech.de` is Schlundtech's hostname for InterNetX's AutoDNS platform — same host as `gateway.autodns.com` (the IP reverse-resolves to it). Use `gateway.schlundtech.de` as the default endpoint. - **XML is the transport.** Schlundtech's own web portal is also built on the AutoDNS **JSON** API (`cloud.schlundtech.com/services/backend/zone/...` returns the documented `stid`/`status`/`object`/`data` envelope with the same task codes: S0205 zone inquire, S0210 axfr, S0225 zone list). Do not mistake that for a public API: it authenticates by **portal session cookie only** — no BasicAuth — so it is the frontend's private backend, versioned with Schlundtech's release cycle, and unusable without driving a browser login plus 2FA. `api.autodns.com/v1` is the documented JSON API but rejects a Schlundtech login with `EF1322002 "Application could not be found."`: the BasicAuth username must be a registered *application* on InterNetX's side (`x-autodns-agent: PHOENIX` does not change this). Build against the XML gateway, which takes the credentials a Schlundtech customer actually has. - **Context ID is `10`.** Verified against the real account: zone responses carry `"owner":{"context":10,"user":""}`. Context 1 is the demo system, 4 the PersonalAutoDNS live system — so a demo run needs `SCHLUNDTECH_CONTEXT=1`, not 10. - **2FA is a `` in the auth block, XML only.** The Authentication page lists exactly two options for XML: credentials (user/context/password) and 2FA. The token is a six-digit TOTP per RFC 6238, not an opaque string, and it expires roughly every 30 seconds. The JSON API has no token at all — it uses BasicAuth plus an `X-Domainrobot-Context` header, which is a second reason not to build on it here. - **The XML gateway answers HTTP 200 even on failure.** Verified: a request with bad credentials returns `HTTP 200` and this body: `response/result/msg/{code:EF00202, text:"User does not exist or password incorrect.", object:{type:user, value:}}`, with the sibling `status/{code:E00000, type:error}` staying generic. **Never branch on the HTTP status code** — parse the body, treat `result/msg/code` as the real error and `result/status/type` as the success flag. Non-2xx from this host is an nginx 404 (wrong path), never an application error. Whether the 3 req/s limit comes back as an HTTP 429 or as an in-body error is still unverified. - Zone read = task `0205`. Record replace = task `0202001` (`rr_rem` + `rr_add` in one transaction, `system_ns` required). Zone's own NS is the first NS in `nameServers` (a.k.a. `virtualNameServer`). - Record shape is `{name, type, value}` plus `ttl`, where the apex is the empty name. `nameServers` is the NS set. Task `0210` returns a plain zone file. - Working implementations: https://github.com/jurica/ddns-schlundtech (Go), https://github.com/couchtyp/certbot-dns-schlundtech, https://github.com/wosc/schlund-ddns