{"owner":"juanfont","repo":"headscale","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"files":{"AGENTS.md":"# AGENTS.md\n\nBehavioural guidance for AI agents working in this repository. Reference\nmaterial for complex procedures lives next to the code — integration\ntesting is documented in [`cmd/hi/README.md`](cmd/hi/README.md) and\n[`integration/README.md`](integration/README.md). Read those files\nbefore running tests or writing new ones.\n\nHeadscale is an open-source implementation of the Tailscale control server\nwritten in Go. It manages node registration, IP allocation, policy\nenforcement, and DERP routing for self-hosted tailnets.\n\n## Interaction Rules\n\nThese rules govern how you work in this repo. They are listed first\nbecause they shape every other decision.\n\n### Ask with comprehensive multiple-choice options\n\nWhen you need to clarify intent, scope, or approach, use the\n`AskUserQuestion` tool (or a numbered list fallback) and present the user\nwith a comprehensive set of options. Cover the likely branches explicitly\nand include an \"other — please describe\" escape.\n\n- Bad: _\"How should I handle expired nodes?\"_\n- Good: _\"How should expired nodes be handled? (a) Remain visible to peers\n  but marked expired (current behaviour); (b) Hidden from peers entirely;\n  (c) Hidden from peers but visible in admin API; (d) Other.\"_\n\nThis matters more than you think — open-ended questions waste a round\ntrip and often produce a misaligned answer.\n\n### Read the documented procedure before running complex commands\n\nBefore invoking any `hi` command, integration test, generator, or\nmigration tool, read the referenced README in full —\n`cmd/hi/README.md` for running tests, `integration/README.md` for\nwriting them. Never guess flags. If the procedure is not documented\nanywhere, ask the user rather than inventing one.\n\n### Map once, then act\n\nUse `Glob` / `Grep` to understand file structure, then execute. Do not\nre-explore the same area to \"double-check\" once you have a plan. Do not\nre-read files you edited in this session — the harness tracks state for\nyou.\n\n### Fail fast, report up\n\nIf a command fails twice with the same error, stop and report the exact\nerror to the user with context. Do not loop through variants or\n\"try one more thing\". A repeated failure means your model of the problem\nis wrong.\n\n### Confirm scope for multi-file changes\n\nBefore touching more than three files, show the user which files will\nchange and why. Use plan mode (`ExitPlanMode`) for non-trivial work.\n\n### Prefer editing existing files\n\nDo not create new files unless strictly necessary. Do not generate helper\nabstractions, wrapper utilities, or \"just in case\" configuration. Three\nsimilar lines of code is better than a premature abstraction.\n\n## Quick Start\n\n```bash\n# Enter the nix dev shell (Go 1.26.1, buf, golangci-lint, prek)\nnix develop\n\n# Full development workflow: fmt + lint + test + build\nmake dev\n\n# Individual targets\nmake build           # build the headscale binary\nmake test            # go test ./...\nmake fmt             # format Go, docs, proto\nmake lint            # lint Go, proto\nmake generate        # regenerate protobuf code (after changes to proto/)\nmake clean           # remove build artefacts\n\n# Direct go test invocations\ngo test ./...\ngo test -race ./...\n\n# Integration tests — read cmd/hi/README.md first\ngo run ./cmd/hi doctor\ngo run ./cmd/hi run \"TestName\"\n```\n\nGo 1.26.1 minimum (per `go.mod:3`). `nix develop` pins the exact toolchain\nused in CI.\n\n## Pre-Commit with prek\n\n`prek` installs git hooks that run the same checks as CI.\n\n```bash\nnix develop\nprek install            # one-time setup\nprek run                # run hooks on staged files\nprek run --all-files    # run hooks on the full tree\n```\n\nHooks cover: file hygiene (trailing whitespace, line endings, BOM),\nsyntax validation (JSON/YAML/TOML/XML), merge-conflict markers, private\nkey detection, nixpkgs-fmt, prettier, and `golangci-lint` via\n`--new-from-rev=HEAD~1` (see `.pre-commit-config.yaml:59`). A manual\ninvocation with an `upstream/main` remote is equivalent:\n\n```bash\ngolangci-lint run --new-from-rev=upstream/main --timeout=5m --fix\n```\n\n`git commit --no-verify` is acceptable only for WIP commits on feature\nbranches — never on `main`.\n\n## Project Layout\n\n```\nheadscale/\n├── cmd/\n│   ├── headscale/    # Main headscale server binary\n│   └── hi/           # Integration test runner (see cmd/hi/README.md)\n├── hscontrol/        # Core control plane\n├── integration/      # End-to-end Docker-based tests (see integration/README.md)\n├── proto/            # Protocol buffer definitions\n├── gen/              # Generated code (buf output — do not edit)\n├── docs/             # User and ACL reference documentation\n└── packaging/        # Distribution packaging\n```\n\n### `hscontrol/` packages\n\n- `app.go`, `handlers.go`, `grpcv1.go`, `noise.go`, `auth.go`, `oidc.go`,\n  `poll.go`, `metrics.go`, `debug.go`, `tailsql.go`, `platform_config.go`\n  — top-level server files\n- `state/` — central coordinator (`state.go`) and the copy-on-write\n  `NodeStore` (`node_store.go`). All cross-subsystem operations go\n  through `State`.\n- `db/` — GORM layer, migrations, schema. `node.go`, `users.go`,\n  `api_key.go`, `preauth_keys.go`, `ip.go`, `policy.go`.\n- `mapper/` — streaming batcher that distributes MapResponses to\n  clients: `batcher.go`, `node_conn.go`, `builder.go`, `mapper.go`.\n  Performance-critical.\n- `policy/` — `policy/v2/` is **the** policy implementation. The\n  top-level `policy.go` is thin wrappers. There is no v1 directory.\n- `routes/`, `dns/`, `derp/`, `types/`, `util/`, `templates/`, `capver/`\n  — routing, MagicDNS, relay, core types, helpers, client templates,\n  capability versioning.\n- `servertest/` — in-memory test harness for server-level tests that\n  don't need Docker. Prefer this over `integration/` when possible.\n- `assets/` — embedded UI assets.\n\n### `cmd/hi/` files\n\n`main.go`, `run.go`, `doctor.go`, `docker.go`, `cleanup.go`, `stats.go`,\n`README.md`. **Read `cmd/hi/README.md` before running any `hi` command.**\n\n## Architecture Essentials\n\n- **`hscontrol/state/state.go`** is the central coordinator. Cross-cutting\n  operations (node updates, policy evaluation, IP allocation) go through\n  the `State` type, not directly to the database.\n- **`NodeStore`** in `hscontrol/state/node_store.go` is a copy-on-write\n  in-memory cache backed by `atomic.Pointer[Snapshot]`. Every read is a\n  pointer load; writes rebuild a new snapshot and atomically swap. It is\n  the hot path for `MapRequest` processing and peer visibility.\n- **The map-request sync point** is\n  `State.UpdateNodeFromMapRequest()` in\n  `hscontrol/state/state.go:2351`. This is where Hostinfo changes,\n  endpoint updates, and route advertisements land in the NodeStore.\n- **Mapper subsystem** streams MapResponses via `batcher.go` and\n  `node_conn.go`. Changes here affect all connected clients.\n- **Node registration flow**: noise handshake (`noise.go`) → auth\n  (`auth.go`) → state/DB persistence (`state/`, `db/`) → initial map\n  (`mapper/`).\n\n## Database Migration Rules\n\nThese rules are load-bearing — violating them corrupts production\ndatabases. The `migrationsRequiringFKDisabled` map in\n`hscontrol/db/db.go:962` is frozen as of 2025-07-02 (see the comment at\n`db.go:989`). All new migrations must:\n\n1. **Never reorder existing migrations.** Migration order is immutable\n   once committed.\n2. **Only add new migrations to the end** of the migrations array.\n3. **Never disable foreign keys.** No new entries in\n   `migrationsRequiringFKDisabled`.\n4. **Use the migration ID format** `YYYYMMDDHHMM-short-description`\n   (timestamp + descriptive suffix). Example: `202602201200-clear-tagged-node-user-id`.\n5. **Never rename columns** that later migrations reference. Let\n   `AutoMigrate` create a new column if needed.\n\n## Tags-as-Identity\n\nHeadscale enforces **tags XOR user ownership**: every node is either\ntagged (owned by tags) or user-owned (owned by a user namespace), never\nboth. This is a load-bearing architectural rule.\n\n- **Use `node.IsTagged()`** (`hscontrol/types/node.go:221`) to determine\n  ownership, not `node.UserID().Valid()`. A tagged node may still have\n  `UserID` set for \"created by\" tracking — `IsTagged()` is authoritative.\n- `IsUserOwned()` (`node.go:227`) returns `!IsTagged()`.\n- Tagged nodes are presented to Tailscale as the special\n  `TaggedDevices` user (`hscontrol/types/users.go`, ID `2147455555`).\n- `SetTags` validation is enforced by `validateNodeOwnership()` in\n  `hscontrol/state/tags.go`.\n- Examples and edge cases live in `hscontrol/types/node_tags_test.go`\n  and `hscontrol/grpcv1_test.go` (`TestSetTags_*`).\n\n**Don't do this**:\n\n```go\nif node.UserID().Valid() { /* assume user-owned */ }       // WRONG\nif node.UserID().Valid() && !node.IsTagged() { /* ok */ }  // correct\n```\n\n## Policy Engine\n\n`hscontrol/policy/v2/policy.go` is the policy implementation. The\ntop-level `hscontrol/policy/policy.go` contains only wrapper functions\naround v2. There is no v1 directory.\n\nKey concepts an agent will encounter:\n\n- **Autogroups**: `autogroup:self`, `autogroup:member`, `autogroup:internet`\n- **Tag owners**: IP-based authorization for who can claim a tag\n- **Route approvals**: auto-approval of subnet routes by policy\n- **SSH policies**: SSH access control via grants\n- **HuJSON** parsing for policy files\n\nFor usage examples, read `hscontrol/policy/v2/policy_test.go`. For ACL\nreference documentation, see `docs/`.\n\n## Integration Testing\n\n**Before running any `hi` command, read `cmd/hi/README.md` in full.**\nGuessing at `hi` flags leads to broken runs and stale containers.\n\nTest-authoring patterns (`EventuallyWithT`, `IntegrationSkip`, helper\nvariants, scenario setup) are documented in `integration/README.md`.\n\nKey reminders:\n\n- Integration test functions **must** start with `IntegrationSkip(t)`.\n- External calls (`client.Status`, `headscale.ListNodes`, etc.) belong\n  inside `EventuallyWithT`; state-mutating commands (`tailscale set`)\n  must not.\n- Tests generate ~100 MB of logs per run under `control_logs/{runID}/`.\n  Prune old runs if disk is tight.\n- Flakes are almost always code, not infrastructure. Read `hs-*.stderr.log`\n  before blaming Docker.\n\n## Code Conventions\n\n- **Commit messages** follow Go-style `package: imperative description`.\n  Recent examples from `git log`:\n  - `db: scope DestroyUser to only delete the target user's pre-auth keys`\n  - `state: fix policy change race in UpdateNodeFromMapRequest`\n  - `integration: fix ACL tests for address-family-specific resolve`\n\n  Not Conventional Commits. No `feat:`/`chore:`/`docs:` prefixes.\n\n- **Protobuf regeneration**: changes under `proto/` require\n  `make generate` (which runs `buf generate`) and should land in a\n  **separate commit** from the callers that use the regenerated types.\n- **Formatting** is enforced by `golangci-lint` with `golines` (width 88)\n  and `gofumpt`. Run `make fmt` or rely on the pre-commit hook.\n- **Logging** uses `zerolog`. Prefer single-line chains\n  (`log.Info().Str(...).Msg(...)`). For 4+ fields or conditional fields,\n  build incrementally and **reassign** the event variable:\n  `e = e.Str(\"k\", v)`. Forgetting to reassign silently drops the field.\n- **Tests**: prefer `hscontrol/servertest/` for server-level tests that\n  don't need Docker — faster than full integration tests.\n- **View types in read paths**: response serializers must read through\n  `NodeView`/`UserView`/`PreAuthKeyView` accessors. `AsStruct()` clones the\n  whole record on every read — it is only for DB-write/merge clones and mutable\n  working copies, never to build an API response. `grep AsStruct hscontrol/api`\n  must come back empty.\n\n## Gotchas\n\n- **Database**: SQLite for local dev, PostgreSQL for integration-heavy\n  tests (`go run ./cmd/hi run \"...\" --postgres`). Some race conditions\n  only surface on one backend.\n- **NodeStore writes** rebuild a full snapshot. Measure before changing\n  hot-path code.\n- **`.claude/agents/` is deprecated.** Do not create new agent files\n  there. Put behavioural guidance in this file and procedural guidance\n  in the nearest README.\n- **Do not edit `gen/`** — it is regenerated from `proto/` by\n  `make generate`.\n- **Proto changes + code changes should be two commits**, not one.\n"}}