Problem
Nine providers re-implement conversation persistence:
Each provider has its own Entries cap, safeSlug/sanitizeFilename, historyPath, Load/Save. The five fields that actually differ (entry struct, status struct, slug key) are easy to parameterise. Three providers got the tight model; six did not.
Where
All nine files above.
Fix
Create internal/convhistory:
type History[E ConvEntry] struct {
file string // e.g. "sentry-<slug>.json"
maxEntries int
entries []E
mu sync.RWMutex
}
func New[E ConvEntry](provider, slug string, maxEntries int) *History[E]
func (h *History[E]) Add(e E)
func (h *History[E]) Recent(n int) []E
func (h *History[E]) Load() error
func (h *History[E]) Save() error // 0o600
func SafeSlug(s string) string // whitelist [A-Za-z0-9_-]
Each provider becomes ~30 lines: register the entry type, expose Add/Save/Recent.
Closes #22 and #23 by construction.
Acceptance criteria
- All nine providers use the shared package
- All saved files are
0o600
- All slugs go through
SafeSlug — adversarial inputs (../../etc/passwd) return default or sanitized
- Generic tests cover the path-traversal cases once, not nine times
Problem
Nine providers re-implement conversation persistence:
internal/sentry/conversation.go(good — 0o600 + safeSlug whitelist)internal/linear/conversation.go(good)internal/notion/conversation.go(good)internal/cloudflare/conversation.go(BAD — 0o644 + sanitizeFilename traversal — see Conversation history written 0o644 — leaks Q&A across 6 providers #22, Path-traversal in k8s/iam/cloudflare sanitizeFilename — replace with safeSlug whitelist #23)internal/k8s/conversation.go(BAD)internal/iam/conversation.go(BAD)internal/flyio/conversation.go(BAD)internal/railway/conversation.go(BAD)internal/vercel/conversation.go(BAD)Each provider has its own
Entriescap,safeSlug/sanitizeFilename,historyPath,Load/Save. The five fields that actually differ (entry struct, status struct, slug key) are easy to parameterise. Three providers got the tight model; six did not.Where
All nine files above.
Fix
Create
internal/convhistory:Each provider becomes ~30 lines: register the entry type, expose Add/Save/Recent.
Closes #22 and #23 by construction.
Acceptance criteria
0o600SafeSlug— adversarial inputs (../../etc/passwd) returndefaultor sanitized