From eb3f3d71343875dab086026c2d2b89ce98a33565 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Tue, 19 May 2026 22:37:24 +0200 Subject: [PATCH] feat(security): wrap engine.Config.Provider + SetProvider doc (W1 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advisor flagged that engine.Config.Provider stayed raw, so the safety property was 'every call goes through buildRequest' instead of the stronger 'every Stream call routes through a SafeProvider.' Wrap it even though buildRequest still scans inline — at worst this costs one extra idempotent scan pass; it removes the 'someone adds a fifth engine Stream site that skips buildRequest' failure mode. Engine.SetProvider gets a doc comment establishing the wrap contract for callers. No active callers today, but documenting it now prevents the future bypass. Confirmed elf engines inherit the wrap automatically: - elf.Manager.Spawn passes arm.Provider (already *SafeProvider after W1-3a) - elf.Manager.SpawnWithProvider has no callers — dead code path Added the Wave 1 plan to TODO.md under active plans. --- TODO.md | 7 +++++++ cmd/gnoma/main.go | 5 ++++- internal/engine/engine.go | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 21f6599..887609c 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,13 @@ Active plans, newest first: +- **[`docs/superpowers/plans/2026-05-19-security-wave1-safeprovider.md`](docs/superpowers/plans/2026-05-19-security-wave1-safeprovider.md)** + — post-audit hardening, Wave 1. Closes the four firewall-bypass + call sites (SLM classifier, summarizer, prompt hook, routerStreamer) + by introducing `security.SafeProvider` at the provider boundary. + **In progress on `feat/security-wave1-safeprovider`** — implementation + complete; ADR and merge pending. Waves 2 (incognito coherence) and + 3 (scanner + path hygiene) are scoped but not yet drafted. - **[`docs/superpowers/plans/2026-05-19-post-slm-unlock.md`](docs/superpowers/plans/2026-05-19-post-slm-unlock.md)** — outstanding work after the SLM unlock session. Phases A (two-stage tool routing), B (CLI agent binary override), C (user profiles), and diff --git a/cmd/gnoma/main.go b/cmd/gnoma/main.go index 4412f4e..f477652 100644 --- a/cmd/gnoma/main.go +++ b/cmd/gnoma/main.go @@ -803,7 +803,10 @@ func main() { // Create engine eng, err := engine.New(engine.Config{ - Provider: prov, + // Wrap even though the engine's own buildRequest scans inline — + // belt-and-suspenders so a future engine path that bypasses + // buildRequest still routes through the firewall. + Provider: security.WrapProvider(prov, fwRef), Router: rtr, Classifier: engineClassifier, Tools: reg, diff --git a/internal/engine/engine.go b/internal/engine/engine.go index eca316c..ed7bef3 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -265,6 +265,13 @@ func (e *Engine) Usage() message.Usage { } // SetProvider swaps the active provider (for dynamic switching). +// +// Callers must pass a provider that has already been wrapped with +// security.WrapProvider — the engine's buildRequest scans inline today, +// but the boundary contract is "every Stream call routes through a +// SafeProvider." Passing a raw provider here would silently open a +// firewall bypass for any engine path that calls Provider.Stream +// without going through buildRequest. func (e *Engine) SetProvider(p provider.Provider) { e.mu.Lock() e.cfg.Provider = p