--- phase: 09-osint-infrastructure plan: 02 type: execute wave: 1 depends_on: [] files_modified: - pkg/recon/limiter.go - pkg/recon/limiter_test.go autonomous: true requirements: [RECON-INFRA-05] must_haves: truths: - "Each source has its own rate.Limiter — no central limiter" - "limiter.Wait blocks until a token is available, honoring ctx cancellation" - "Jitter delay (100ms-1s) is applied before each request when stealth is enabled" - "LimiterRegistry maps source names to limiters and returns existing limiters on repeat lookup" artifacts: - path: "pkg/recon/limiter.go" provides: "LimiterRegistry with For(name, rate, burst) + Wait with optional jitter" contains: "type LimiterRegistry" - path: "pkg/recon/limiter_test.go" provides: "Tests for per-source isolation, jitter range, ctx cancellation" key_links: - from: "pkg/recon/limiter.go" to: "golang.org/x/time/rate" via: "rate.NewLimiter per source" pattern: "rate\\.NewLimiter" --- Implement per-source rate limiter architecture: each source registers its own rate.Limiter keyed by name, and the engine calls Wait() before each request. Optional jitter (100ms-1s) when stealth mode is enabled. Purpose: Satisfies RECON-INFRA-05 and guarantees the "every source holds its own limiter — no centralized limiter" success criterion from the roadmap. Output: pkg/recon/limiter.go, pkg/recon/limiter_test.go @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/09-osint-infrastructure/09-CONTEXT.md @go.mod Task 1: LimiterRegistry with per-source rate.Limiter and jitter pkg/recon/limiter.go, pkg/recon/limiter_test.go - LimiterRegistry.For(name string, r rate.Limit, burst int) *rate.Limiter returns the existing limiter for name or creates a new one. Subsequent calls with the same name return the SAME pointer (idempotent). - Wait(ctx, name, r, burst, stealth bool) error calls limiter.Wait(ctx), then if stealth==true sleeps a random duration between 100ms and 1s (respecting ctx). - Per-source isolation: two different names produce two distinct *rate.Limiter instances. - Ctx cancellation during Wait returns ctx.Err() promptly. - Tests: - TestLimiterPerSourceIsolation: registry.For("a", 10, 1) != registry.For("b", 10, 1) - TestLimiterIdempotent: registry.For("a", 10, 1) == registry.For("a", 10, 1) (same pointer) - TestWaitRespectsContext: cancelled ctx returns error - TestJitterRange: with stealth=true, Wait duration is >= 100ms. Use a high rate (1000/s, burst 100) so only jitter contributes. Create pkg/recon/limiter.go: ```go package recon import ( "context" "math/rand" "sync" "time" "golang.org/x/time/rate" ) // LimiterRegistry holds one *rate.Limiter per source name. // RECON-INFRA-05: each source owns its own limiter — no centralization. type LimiterRegistry struct { mu sync.Mutex limiters map[string]*rate.Limiter } func NewLimiterRegistry() *LimiterRegistry { return &LimiterRegistry{limiters: make(map[string]*rate.Limiter)} } // For returns the limiter for name, creating it with (r, burst) on first call. // Repeat calls with the same name return the same *rate.Limiter pointer. func (lr *LimiterRegistry) For(name string, r rate.Limit, burst int) *rate.Limiter { lr.mu.Lock() defer lr.mu.Unlock() if l, ok := lr.limiters[name]; ok { return l } l := rate.NewLimiter(r, burst) lr.limiters[name] = l return l } // Wait blocks until the source's token is available. If stealth is true, // an additional random jitter between 100ms and 1s is applied to evade // fingerprint detection (RECON-INFRA-06 partial — fully wired in 09-03). func (lr *LimiterRegistry) Wait(ctx context.Context, name string, r rate.Limit, burst int, stealth bool) error { l := lr.For(name, r, burst) if err := l.Wait(ctx); err != nil { return err } if stealth { jitter := time.Duration(100+rand.Intn(900)) * time.Millisecond select { case <-time.After(jitter): case <-ctx.Done(): return ctx.Err() } } return nil } ``` Create pkg/recon/limiter_test.go with the four tests above. Use testify require. For TestJitterRange, call Wait with rate=1000, burst=100, stealth=true, measure elapsed, assert >= 90ms (10ms slack) and <= 1100ms. cd /home/salva/Documents/apikey && go test ./pkg/recon/ -run 'TestLimiter|TestWait|TestJitter' -count=1 All limiter tests pass; per-source isolation verified; jitter bounded; ctx cancellation honored. - `go test ./pkg/recon/ -run Limiter -count=1` passes - `go vet ./pkg/recon/...` clean - LimiterRegistry exported with For and Wait - Each source receives its own *rate.Limiter - Stealth jitter range 100ms-1s enforced After completion, create `.planning/phases/09-osint-infrastructure/09-02-SUMMARY.md`