chore(lint): Expand golangci-lint configuration

- Switch from minimal to standard linter set - Enable 40+ linters across 4 tiers - Configure gocritic, govet, revive, gosec - Exclude exported comments & test linters - Restructure AGENTS.md guidelines - Add errgroup reference to AGENTS.md

cognitive committed Mar 3, 2026 at 13:00 UTC 1cd0d0c271a98f0cc61ae7718e7fdfd8b8ef1903
2 files changed +236 -168
.golangci.yml
+114 -3
@@ -14,18 +14,58 @@ formatters:
14 - gosuda.org
15
16 linters:
17 - default: none
17 + default: standard
18
19 enable:
20 - # CI blockers: correctness + security
20 + # --- Tier 1: Bugs & Correctness ---
21 - govet
22 - errcheck
23 - staticcheck
24 - unused
25 + - gosec
26 - errorlint
27 - copyloopvar
28 + - nilerr
29 + - bodyclose
30 + - sqlclosecheck
31 + - rowserrcheck
32 + - durationcheck
33 + - makezero
34 - noctx
35
36 + # --- Tier 2: Code Quality & Style ---
37 + - gocritic
38 + - revive
39 + - unconvert
40 + - unparam
41 + - wastedassign
42 + - misspell
43 + - whitespace
44 + - godot
45 + - goconst
46 + - dupword
47 + - usestdlibvars
48 + - testifylint
49 + - testableexamples
50 + - tparallel
51 + - usetesting
52 +
53 + # --- Tier 3: Concurrency & Safety ---
54 + - gochecknoglobals
55 + - gochecknoinits
56 + - containedctx
57 +
58 + # --- Tier 4: Performance & Modernization ---
59 + - prealloc
60 + - intrange
61 + - modernize
62 + - fatcontext
63 + - perfsprint
64 + - reassign
65 + - spancheck
66 + - mirror
67 + - recvcheck
68 +
69 exclusions:
70 rules:
71 - linters:
@@ -33,8 +73,19 @@ linters:
73 source: "^\\s*defer\\s+"
74 - path: "_test\\.go"
75 linters:
76 + - bodyclose
77 - errcheck
78 + - gosec
79 - noctx
80 + - wrapcheck
81 + - goconst
82 + - funlen
83 + - dupl
84 + - gochecknoglobals
85 + - text: "should have a package comment"
86 + linters: [revive]
87 + - text: "exported \\S+ \\S+ should have comment"
88 + linters: [revive]
89
90 settings:
91 errcheck:
@@ -72,8 +123,68 @@ linters:
123 - atomic.LoadInt64
124 - rand.Read
125
126 + gocritic:
127 + enabled-tags:
128 + - diagnostic
129 + - style
130 + - performance
131 + - experimental
132 + - opinionated
133 + disabled-checks:
134 + - hugeParam
135 + - rangeValCopy
136 +
137 govet:
76 - enable-all: false
138 + enable-all: true
139 + disable:
140 + - fieldalignment
141 + settings:
142 + shadow:
143 + strict: true
144 +
145 + revive:
146 + rules:
147 + - name: blank-imports
148 + - name: context-as-argument
149 + - name: context-keys-type
150 + - name: dot-imports
151 + - name: error-return
152 + - name: error-strings
153 + - name: error-naming
154 + - name: exported
155 + disabled: true
156 + - name: if-return
157 + - name: increment-decrement
158 + - name: var-naming
159 + - name: var-declaration
160 + - name: range
161 + - name: receiver-naming
162 + - name: time-naming
163 + - name: unexported-return
164 + - name: indent-error-flow
165 + - name: errorf
166 + - name: empty-block
167 + - name: superfluous-else
168 + - name: unused-parameter
169 + - name: unreachable-code
170 + - name: redefines-builtin-id
171 +
172 + gosec:
173 + excludes:
174 + - G104
175 + - G304
176 +
177 + perfsprint:
178 + strconcat: true
179 +
180 + fatcontext:
181 + check-struct-pointers: true
182 +
183 + spancheck:
184 + checks:
185 + - end
186 + - record-error
187 + - set-status
188
189 issues:
190 max-issues-per-linter: 0
AGENTS.md
+122 -165
@@ -1,174 +1,131 @@
1 # AGENTS.md
2
3 -Repo-specific guidance for automated agents working on Portal.
3 +## Formatting & Style
4
5 -## Quick Commands
6 -
7 -Build:
8 -- `make build` (all artifacts)
9 -- `make build-server` (relay server binary)
10 -- `make build-frontend` (React admin UI)
11 -- `make build-tunnel` (portal-tunnel binaries)
5 +**Mandatory** before every commit: `gofmt -w . && goimports -w .`
6
13 -Run:
14 -- `make run` (run `./bin/relay-server`)
15 -- `docker compose up` (full stack, relay at :4017, admin at `/admin`)
7 +Import ordering: **stdlib → external → internal** (blank-line separated). Local prefix: `github.com/gosuda`.
8
17 -Lint/Format/Test:
18 -- `make fmt` (gofmt + goimports)
19 -- `make vet` (go vet)
20 -- `make lint` (golangci-lint)
21 -- `make test` (go test -v -race -coverprofile=coverage.out ./...)
22 -- `make vuln` (govulncheck)
23 -- `make tidy` (go mod tidy + go mod verify)
9 +**Naming:** packages lowercase single-word (`httpwrap`) · interfaces as behavior verbs (`Reader`, `Handler`) · errors `Err` prefix sentinels (`ErrNotFound`), `Error` suffix types · context always first param `func Do(ctx context.Context, ...)`
10
25 -Single test:
26 -- `go test -v -run TestName ./path/to/pkg`
11 +**CGo:** always disabled — `CGO_ENABLED=0`. Pure Go only. No C dependencies.
12
28 -Frontend dev:
29 -- `cd cmd/relay-server/frontend && npm run dev`
30 -- `cd cmd/relay-server/frontend && npm run build`
31 -- `cd cmd/relay-server/frontend && npm run lint`
13 +---
14
33 -## Architecture (Big Picture)
34 -
35 -Portal is a relay network that connects Apps (service publishers) and Clients (service consumers) through a central relay server without decrypting payloads.
15 +## Error Handling
16
37 -Core components:
38 -- Relay server: `cmd/relay-server` (HTTP API/admin + SNI router)
39 -- Relay core logic: `portal/` (lease manager, reverse connection hub, forwarding)
40 -- SNI router package: `portal/sni/`
41 -- SDK for Apps: `sdk/`
42 -- Tunnel client: `cmd/portal-tunnel/` (exposes local services)
43 -- Admin frontend: `cmd/relay-server/frontend/` (built into `cmd/relay-server/dist/app`)
44 -
45 -## Connection Flow (High Level)
46 -
47 -1. App/Tunnel registers a Lease with relay via `/sdk/register` (name, metadata, TLS mode, reverse token).
48 -2. Tunnel maintains reverse WebSocket workers to relay via `/sdk/connect`.
49 -3. Client traffic enters relay:
50 - - TLS traffic on SNI port is routed by SNI.
51 - - Non-TLS traffic can use HTTP proxy mode.
52 -4. Relay acquires a reverse tunnel connection and forwards bytes end-to-end.
53 -
54 -## Key Terms
55 -
56 -- Portal / Relay: central mediator; never decrypts payloads.
57 -- App: service publisher using SDK or tunnel to register Leases.
58 -- Client: consumer connecting via relay.
59 -- Lease: advertising unit; one Lease maps to one public endpoint.
60 -
61 -## Where to Look
62 -
63 -- `cmd/relay-server/` (entrypoint, HTTP APIs, SNI callback wiring)
64 -- `portal/reverse_hub.go` (reverse WebSocket connection pool)
65 -- `portal/sni/` (SNI parser/router)
66 -- `sdk/` (App integration)
67 -- `cmd/portal-tunnel/` (tunnel client)
68 -- `docs/architecture.md` and `docs/glossary.md`
69 -
70 -## Domain Configuration
71 -
72 -Portal uses environment variables for domain and TLS configuration:
73 -
74 -### Core Environment Variables
75 -
76 -| Variable | Description |
77 -|----------|-------------|
78 -| `PORTAL_URL` | Base URL (e.g., `https://portal.example.com`) |
79 -| `BOOTSTRAP_URIS` | Relay API URLs (defaults to `PORTAL_URL`) |
80 -| `SNI_PORT` | SNI router port (default `443`) |
81 -| `ADMIN_SECRET_KEY` | Admin auth key (auto-generated if unset) |
82 -| `KEYLESS_DIR` | Relay keyless materials directory (default `/etc/portal/keyless`) |
83 -| `CLOUDFLARE_TOKEN` | Cloudflare DNS token for ACME DNS-01 auto-issuance when key file is missing |
84 -
85 -### Tunnel Environment Variables
86 -
87 -| Variable | Description |
88 -|----------|-------------|
89 -| `RELAYS` | Relay API URLs for tunnel client (comma-separated) |
90 -| `TLS` | Enable TLS keyless mode (`1`/`true`) |
91 -| `TLS_BASE_DOMAIN` | Base domain used for keyless certificate hostname validation (keyless mode) |
92 -
93 -### Domain Derivation
94 -
95 -- Service URL: `{name}.{base_domain}` (e.g., `myapp.example.com`)
96 -- Base domain extracted from `PORTAL_URL` via `extractBaseDomain()` in `cmd/relay-server/utils.go`
97 -- SNI routes registered in `portal/sni/router.go`
98 -
99 -### TLS
100 -
101 -1. **`TLS` disabled**: HTTP proxy mode for development.
102 -2. **`TLS` enabled**: Tunnel uses SDK keyless TLS mode.
103 - - Keyless signer endpoint defaults to relay URL.
104 - - Certificate chain/root trust are auto-discovered by SDK from signer endpoint when not explicitly provided.
105 - - Auto-discovery requires an HTTPS signer endpoint.
106 - - Relay signer key comes from `KEYLESS_DIR/privatekey.pem`; when missing and `CLOUDFLARE_TOKEN` is set, relay auto-issues via ACME DNS-01.
107 -
108 -See `docs/deployment.md` for full deployment documentation.
109 -
110 -## Repo Basics
111 -
112 -- Module: `gosuda.org/portal`
113 -- Go version: 1.26.0 (from `go.mod`)
114 -
115 -## Gosuda Go Standards
116 -
117 -Formatting & style:
118 -- Run formatting before commits (see Quick Commands).
119 -- Import order: stdlib -> external -> internal (blank-line separated).
120 -- Naming: packages lowercase single-word; interfaces as behavior verbs; errors use `Err` prefix for sentinels and `Error` suffix for types.
121 -- Do not add meaningless string normalization or utility wrapper functions unless they provide clear, demonstrated value.
122 -- Do not keep backward compatibility when changing code unless explicitly requested by the user.
123 -- Context first parameter for public I/O: `func Do(ctx context.Context, ...)`.
124 -- CGo disabled: `CGO_ENABLED=0`.
125 -
126 -Static analysis & linters:
127 -- Use `go vet`, `golangci-lint`, `go test -race`, and `govulncheck` (see Quick Commands).
128 -- Linter tiers: correctness, quality, concurrency safety, and performance/modernization (configured in `.golangci.yml`).
129 -
130 -Error handling:
131 -- Wrap with `%w` and include call-site context.
132 -- Sentinel errors per package; use `errors.Is`/`errors.As`.
133 -- Use `errors.Join` for multi-error.
134 -- Never ignore errors unless explicitly excluded by errcheck.
135 -
136 -Iterators (Go 1.23+):
137 -- Signatures: `func(yield func() bool)`, `func(yield func(V) bool)`, `func(yield func(K, V) bool)`.
138 -- Always check yield return; prefer stdlib helpers like `slices.Collect` and `maps.Keys`.
139 -
140 -Context & concurrency:
141 -- Prefer `errgroup.Group` for parallel work, `SetLimit` for bounds.
142 -- No goroutines without clear exit; creator owns lifecycle.
143 -- Directional channels in signatures; only sender closes.
144 -- Avoid `time.After` in loops; use `context.WithTimeout` or `time.Ticker`.
145 -
146 -Testing:
147 -- Do not run tests on every execution. Run tests only when explicitly requested, before handoff, or when a change is high-risk.
148 -- Use race detector in normal test runs.
149 -- Use `t.Context()` in tests where applicable.
150 -- Benchmarks should use `for b.Loop() {}`.
151 -
152 -Security:
153 -- Use `govulncheck` and `go mod verify` during release workflows.
154 -- Avoid `math/rand` for security-sensitive operations.
155 -
156 -Performance:
157 -- Avoid `reflect` on hot paths; prefer generics or type switches.
158 -- Use `sync.Pool` for hot paths only.
159 -
160 -Module hygiene:
161 -- Always commit `go.mod` and `go.sum`; never commit `go.work`.
162 -- Pin toolchain version to match `go.mod`
163 -
164 -CI/CD:
165 -- CI runs a single `verify` job: vet + lint + test + vuln (`.github/workflows/ci.yml`).
166 -- CD builds/pushes Docker images on `main` and `v*` tags, and deploys on `main` pushes (`.github/workflows/cd.yml`).
167 -
168 -Verbalized sampling:
169 -- For non-trivial changes: sample multiple intents, explore edge cases, assess coupling, tidy first, and surface tradeoffs.
170 -
171 -Refactoring discipline:
172 -- Do not stack repeated "minimal patches" that leave logic fragmented across files.
173 -- For domain/URL parsing and normalization, keep a single source of truth and make all callers use it.
174 -- If a flow is being refactored (e.g., SDK client/listener TLS domain handling), complete consolidation in the same change instead of leaving temporary split logic.
17 +1. **Wrap with `%w`** — always add call-site context: `return fmt.Errorf("repo.Find: %w", err)`
18 +2. **Sentinel errors** per package: `var ErrNotFound = errors.New("user: not found")`
19 +3. **Multi-error** — use `errors.Join(err1, err2)` or `fmt.Errorf("op: %w and %w", e1, e2)`
20 +4. **Never ignore errors** — `_ = fn()` only for `errcheck.exclude-functions`
21 +5. **Fail fast** — return immediately; no state accumulation after failure
22 +6. **Check with `errors.Is`/`errors.As`** — never string-match `err.Error()`
23 +
24 +---
25 +
26 +## Iterators (Go 1.23+)
27 +
28 +Signatures: `func(yield func() bool)` · `func(yield func(V) bool)` · `func(yield func(K, V) bool)`
29 +
30 +**Rules:** always check yield return (panics on break if ignored) · avoid defer/recover in iterator bodies · use stdlib (`slices.All`, `slices.Backward`, `slices.Collect`, `maps.Keys`, `maps.Values`) · range over integers: `for i := range n {}`
31 +
32 +---
33 +
34 +## Context & Concurrency
35 +
36 +Every public I/O function **must** take `context.Context` first.
37 +
38 +| Pattern | Primitive |
39 +|---------|-----------|
40 +| Parallel work with errors | `errgroup.Group` (preferred over `WaitGroup`) |
41 +| Bounded concurrency | `errgroup.SetLimit` or buffered channel semaphore |
42 +| Fan-out/fan-in | Unbuffered chan + N producers + 1 consumer; `select` to merge |
43 +| Pipeline stages | `chan T` between stages, sender closes to signal done |
44 +| Cancellation/timeout | `context.WithCancel` / `context.WithTimeout` |
45 +| Concurrent read/write | `sync.RWMutex` (encapsulate behind methods) |
46 +| Lock-free counters | `atomic.Int64` / `atomic.Uint64` |
47 +| One-time init | `sync.Once` / `sync.OnceValue` / `sync.OnceFunc` |
48 +| Object reuse | `sync.Pool` (hot paths only, no lifetime guarantees) |
49 +
50 +**Goroutine rules:** creator owns lifecycle (start, stop, errors, panic recovery) · no bare `go func()` · every goroutine needs a clear exit (context, done channel, bounded work) · leaks are bugs — verify with `goleak` or `runtime.NumGoroutine()`
51 +
52 +**Channel rules:** use directional types (`chan<-`/`<-chan`) in signatures · only sender closes · nil channel blocks forever (use to disable `select` cases) · unbuffered = synchronization, buffered = decoupling/backpressure · `for v := range ch` until closed · `select` with `default` only for non-blocking try-send/try-receive
53 +
54 +**Select patterns:** timeout via `context.WithTimeout` (not `time.After` in loops — leaks timers) · always check `ctx.Done()` · fan-in merges with multi-case `select` · rate-limit with `time.Ticker` not `time.Sleep`
55 +
56 +```go
57 +g, ctx := errgroup.WithContext(ctx)
58 +g.SetLimit(maxWorkers)
59 +for _, item := range items {
60 + g.Go(func() error { return process(ctx, item) })
61 +}
62 +if err := g.Wait(); err != nil { return fmt.Errorf("processAll: %w", err) }
63 +```
64 +
65 +**Anti-patterns:** ❌ shared memory without sync · ❌ `sync.Mutex` in public APIs · ❌ goroutine without context · ❌ closing channel from receiver · ❌ sending on closed channel · ❌ `time.Sleep` for synchronization · ❌ unbounded goroutine spawn
66 +
67 +---
68 +
69 +## Testing
70 +
71 +```bash
72 +go test -v -race -coverprofile=coverage.out ./...
73 +```
74 +
75 +- **Benchmarks (Go 1.24+):** `for b.Loop() {}` — prevents compiler opts, excludes setup from timing
76 +- **Test contexts (Go 1.24+):** `ctx := t.Context()` — auto-canceled when test ends
77 +- **Table-driven tests** as default · **race detection** (`-race`) mandatory in CI
78 +- **Fuzz testing:** `go test -fuzz=. -fuzztime=30s` — fast, deterministic targets
79 +- **testify** for assertions when stdlib `testing` is verbose
80 +
81 +---
82 +
83 +## Security
84 +
85 +- **Vulnerability scanning:** `govulncheck ./...` — CI and pre-release
86 +- **Module integrity:** `go mod verify` — validates checksums against go.sum
87 +- **Supply chain:** always commit `go.sum` · audit with `go mod graph` · pin toolchain
88 +- **SBOM:** `syft packages . -o cyclonedx-json > sbom.json` on release
89 +- **Crypto:** FIPS 140-3, post-quantum X25519MLKEM768, `crypto/rand.Text()` for secure tokens
90 +
91 +---
92 +
93 +## Performance
94 +
95 +- **Object reuse:** `sync.Pool` hot paths · `weak.Make` for cache-friendly patterns
96 +- **Benchmarking:** `go test -bench=. -benchmem` · `-cpuprofile`/`-memprofile`
97 +- **Avoid `reflect`:** ~30x slower than static code, defeats compile-time checks and linters · prefer generics (4–18x faster), type switches, interfaces, or `go generate` codegen for hot paths
98 +- **Escape analysis:** `go build -gcflags='-m'` to verify heap allocations
99 +
100 +---
101 +
102 +## Module Hygiene
103 +
104 +- **Always commit** `go.mod` and `go.sum` · **never commit** `go.work`
105 +- **Pin toolchain:** `toolchain go1.25.0` in go.mod
106 +- **Tool directive (Go 1.24+):** `tool golang.org/x/tools/cmd/stringer` in go.mod
107 +- **Pre-release:** `go mod tidy && go mod verify && govulncheck ./...`
108 +- **Sandboxed I/O (Go 1.24+):** `os.Root` for directory-scoped file operations
109 +
110 +---
111 +
112 +**Pre-commit:** `make all` or `gofmt -w . && goimports -w . && go vet ./... && golangci-lint run --fix && go test -race ./... && govulncheck ./...`
113 +
114 +---
115 +
116 +## Verbalized Sampling
117 +
118 +Before trival or non-trivial changes, AI agents **must**:
119 +
120 +1. **Sample 3–5 intent hypotheses** — rank by likelihood, note one weakness each
121 +2. **Explore edge cases** — at least 3 standard, 5 for architectural changes
122 +3. **Assess coupling** — structural (imports), temporal (co-changing files), semantic (shared concepts)
123 +4. **Tidy first** — high coupling → extract/split/rename before changing; low → change directly
124 +5. **Surface decisions** — ask the human when trade-offs exist; do exactly what is asked, no more
125 +
126 +## Project-specific rules [**ENFORCED**]
127 +
128 +- Do not keep backward compatibility unless explicitly requested.
129 +- Do not add meaningless wrapper functions unless they provide demonstrated value.
130 +- Do not stack minimal patches that fragment logic — complete consolidation in one change.
131 +- Do not run tests on every execution — only when requested, before handoff, or for high-risk changes.