Files
vikingowl fb42202834 refactor(security): seal SecureProvider via unexported marker method
The router.SecureProvider interface previously required a public
IsSecure() bool method. Any test mock — or future production type —
could satisfy it by returning true, defeating the W1 "only wrapped
providers may flow past the boundary" contract through convention
rather than at the type level.

Replaces IsSecure() bool with an unexported security.Marker interface
that has a single secured() method. Go's method-set semantics key
unexported methods by their defining package, so only types declared in
internal/security can satisfy Marker. *SafeProvider gets the lone
secured() implementation; router.SecureProvider embeds Marker.

The seal forces every test mock that previously implemented IsSecure()
to either (a) be wrapped with security.WrapProvider(mp, nil) at the use
site, or (b) drop the method entirely if the mock never flows through
SecureProvider. 93 use sites across 11 test files were updated via a
per-package secureMock helper. WrapProvider with a nil firewall ref is
a no-op pass-through, so test behavior is unchanged.

Empirically: a type from outside internal/security can declare
`secured()` but the compiler will reject assigning it to
router.SecureProvider because the unexported method belongs to the
other package's namespace. Convention → compile-time guarantee.
2026-05-20 02:04:07 +02:00

82 lines
2.8 KiB
Go

package security
import (
"context"
"somegit.dev/Owlibou/gnoma/internal/provider"
"somegit.dev/Owlibou/gnoma/internal/stream"
)
// SafeProvider wraps a provider.Provider with firewall redaction.
//
// On every Stream call it scans outgoing messages and the system prompt
// through the firewall referenced by fwRef before delegating to the
// inner provider. Name, Models, and DefaultModel pass through unchanged.
//
// Tool-result redaction stays in the engine because it needs per-tool
// context (origin tool name for logging) that isn't available at the
// provider boundary.
//
// SafeProvider is the single non-bypassable boundary between gnoma's
// internals and any LLM endpoint. Every provider.Provider registered
// with the router or handed to a non-engine consumer (SLM classifier,
// summarizer, hook streamer) should be wrapped.
type SafeProvider struct {
inner provider.Provider
fwRef *FirewallRef
}
// WrapProvider returns a SafeProvider that delegates to inner and
// resolves its firewall through ref. A nil ref is permitted and
// disables scanning — callers who never install a firewall get
// pass-through behaviour rather than a panic.
func WrapProvider(inner provider.Provider, ref *FirewallRef) *SafeProvider {
return &SafeProvider{inner: inner, fwRef: ref}
}
// Inner returns the wrapped provider. Useful for tests and for code
// that needs to reach through to provider-specific behaviour.
func (p *SafeProvider) Inner() provider.Provider {
return p.inner
}
// Marker is a sealed-trait interface only types defined in this package
// can satisfy. Higher layers (e.g. router.SecureProvider) embed Marker so
// the compiler — not convention — enforces that any provider flowing
// through them has been wrapped here. Adding `func (x *Foo) secured() {}`
// to a non-wrapped type in another package would not satisfy this
// interface, because Go method sets distinguish unexported methods by
// their defining package.
type Marker interface {
secured()
}
// secured is the marker method that seals SecureProvider's embedded
// Marker interface. Intentionally no-op.
func (p *SafeProvider) secured() {}
func (p *SafeProvider) Stream(ctx context.Context, req provider.Request) (stream.Stream, error) {
if p.fwRef != nil {
if fw := p.fwRef.Get(); fw != nil {
req.Messages = fw.ScanOutgoingMessages(req.Messages)
req.SystemPrompt = fw.ScanSystemPrompt(req.SystemPrompt)
}
}
return p.inner.Stream(ctx, req)
}
func (p *SafeProvider) Name() string {
return p.inner.Name()
}
func (p *SafeProvider) Models(ctx context.Context) ([]provider.ModelInfo, error) {
return p.inner.Models(ctx)
}
func (p *SafeProvider) DefaultModel() string {
return p.inner.DefaultModel()
}
// Compile-time assertion that *SafeProvider satisfies provider.Provider.
var _ provider.Provider = (*SafeProvider)(nil)