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.
Construct security.FirewallRef early in main() and Set it immediately
after security.NewFirewall returns. Wrap every provider that may be
called outside engine.buildRequest():
- primary provider arm (limitedProvider)
- discovered local models (RegisterDiscoveredModels factory)
- CLI agent arms (subprocprov.New)
- background-discovery factory (StartDiscoveryLoop)
- SLM arm + classifier transport
- summarizer (gnomactx.NewSummarizeStrategy)
routerStreamer and hook PromptExecutor inherit redaction automatically
once every router arm is wrapped — they dispatch through router.Stream
→ arm.Provider.Stream.
engine.Config.Provider stays raw because the engine still scans inline
at buildRequest(); per the Wave 1 plan, removing that scan is deferred
one release as belt-and-suspenders.
Integration tests in internal/security/integration_test.go verify the
boundary end-to-end: a router arm wrapped with WrapProvider redacts an
'sk-ant-...' literal before the inner provider sees it, and the
pre-Set / post-Set transition works as documented (pass-through until
the FirewallRef has a Firewall installed).
Introduces internal/security/SafeProvider — a provider.Provider decorator
that scans outgoing messages and the system prompt through the firewall
before delegating to the inner provider. Tool-result redaction stays in
the engine because it needs per-tool context the boundary lacks.
FirewallRef provides a late-binding atomic.Pointer[Firewall] so the
wrapper can be installed before NewFirewall runs in main. A nil or
unset ref makes SafeProvider a pass-through — preserves the current
init order without lock contention or panics.
Wave 1 of the post-audit hardening plan
(docs/superpowers/plans/2026-05-19-security-wave1-safeprovider.md).
Closes the architectural critique that secret scanning only ran inside
engine.buildRequest(), leaving SLM/summarizer/hook/routerStreamer paths
to send raw payloads. This commit only ships the wrapper; W1-2 and W1-3
will wire it through main and the four bypass sites.