148 lines
5.4 KiB
Markdown
148 lines
5.4 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/09-osint-infrastructure/09-CONTEXT.md
|
|
@go.mod
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: LimiterRegistry with per-source rate.Limiter and jitter</name>
|
|
<files>pkg/recon/limiter.go, pkg/recon/limiter_test.go</files>
|
|
<behavior>
|
|
- 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.
|
|
</behavior>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/salva/Documents/apikey && go test ./pkg/recon/ -run 'TestLimiter|TestWait|TestJitter' -count=1</automated>
|
|
</verify>
|
|
<done>All limiter tests pass; per-source isolation verified; jitter bounded; ctx cancellation honored.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `go test ./pkg/recon/ -run Limiter -count=1` passes
|
|
- `go vet ./pkg/recon/...` clean
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- LimiterRegistry exported with For and Wait
|
|
- Each source receives its own *rate.Limiter
|
|
- Stealth jitter range 100ms-1s enforced
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/09-osint-infrastructure/09-02-SUMMARY.md`
|
|
</output>
|
|
</content>
|