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.