feat(security): wrap engine.Config.Provider + SetProvider doc (W1 follow-up)

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.
This commit is contained in:
2026-05-19 22:37:24 +02:00
parent c932f1e307
commit eb3f3d7134
3 changed files with 18 additions and 1 deletions
+7
View File
@@ -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
+4 -1
View File
@@ -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,
+7
View File
@@ -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