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.
This commit is contained in:
+15
-11
@@ -27,7 +27,6 @@ type mockProvider struct {
|
||||
func (m *mockProvider) Name() string { return m.name }
|
||||
func (m *mockProvider) DefaultModel() string { return "mock" }
|
||||
func (m *mockProvider) Models(_ context.Context) ([]provider.ModelInfo, error) { return nil, nil }
|
||||
func (m *mockProvider) IsSecure() bool { return true }
|
||||
func (m *mockProvider) Stream(_ context.Context, _ provider.Request) (stream.Stream, error) {
|
||||
idx := m.calls.Add(1) - 1
|
||||
if int(idx) >= len(m.streams) {
|
||||
@@ -36,6 +35,12 @@ func (m *mockProvider) Stream(_ context.Context, _ provider.Request) (stream.Str
|
||||
return m.streams[idx], nil
|
||||
}
|
||||
|
||||
// secureMock wraps a test provider in *security.SafeProvider so it
|
||||
// satisfies router.SecureProvider's sealed Marker.
|
||||
func secureMock(p provider.Provider) router.SecureProvider {
|
||||
return security.WrapProvider(p, nil)
|
||||
}
|
||||
|
||||
type eventStream struct {
|
||||
events []stream.Event
|
||||
idx int
|
||||
@@ -62,7 +67,7 @@ func TestBackgroundElf_RunsAndCompletes(t *testing.T) {
|
||||
name: "test",
|
||||
streams: []stream.Stream{newEventStream("Hello from elf!")},
|
||||
}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
elf := SpawnBackground(eng, "say hello")
|
||||
|
||||
@@ -93,7 +98,7 @@ func TestBackgroundElf_Cancel(t *testing.T) {
|
||||
name: "test",
|
||||
streams: []stream.Stream{slowStream},
|
||||
}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
elf := SpawnBackground(eng, "slow task")
|
||||
|
||||
@@ -111,7 +116,7 @@ func TestBackgroundElf_CollectEvents(t *testing.T) {
|
||||
name: "test",
|
||||
streams: []stream.Stream{newEventStream("event test")},
|
||||
}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
elf := SpawnBackground(eng, "generate events")
|
||||
|
||||
@@ -137,7 +142,7 @@ func TestManager_SpawnAndList(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "test/mock",
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
ModelName: "mock",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
@@ -198,7 +203,7 @@ func TestManager_WaitAll(t *testing.T) {
|
||||
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "test/mock", Provider: mp, ModelName: "mock",
|
||||
ID: "test/mock", Provider: secureMock(mp), ModelName: "mock",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
|
||||
@@ -229,7 +234,7 @@ func TestBackgroundElf_WaitIdempotent(t *testing.T) {
|
||||
name: "test",
|
||||
streams: []stream.Stream{newEventStream("hello")},
|
||||
}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
elf := SpawnBackground(eng, "do something")
|
||||
|
||||
r1 := elf.Wait()
|
||||
@@ -246,7 +251,7 @@ func TestBackgroundElf_WaitIdempotent(t *testing.T) {
|
||||
func TestBackgroundElf_PanicRecovery(t *testing.T) {
|
||||
// A provider that panics on Stream() — simulates an engine crash
|
||||
panicProvider := &panicOnStreamProvider{}
|
||||
eng, _ := engine.New(engine.Config{Provider: panicProvider, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(panicProvider), Tools: tool.NewRegistry()})
|
||||
elf := SpawnBackground(eng, "do something")
|
||||
|
||||
result := elf.Wait() // must not hang
|
||||
@@ -266,7 +271,6 @@ func (p *panicOnStreamProvider) DefaultModel() string { return "panic" }
|
||||
func (p *panicOnStreamProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (p *panicOnStreamProvider) IsSecure() bool { return true }
|
||||
func (p *panicOnStreamProvider) Stream(_ context.Context, _ provider.Request) (stream.Stream, error) {
|
||||
panic("intentional test panic")
|
||||
}
|
||||
@@ -279,7 +283,7 @@ func TestManager_CleanupRemovesMeta(t *testing.T) {
|
||||
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "test/mock", Provider: mp, ModelName: "mock",
|
||||
ID: "test/mock", Provider: secureMock(mp), ModelName: "mock",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
|
||||
@@ -325,7 +329,7 @@ func TestManager_ReportResultSuppressedWhenIncognito(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
armID := router.ArmID("test/mock")
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: armID, Provider: mp, ModelName: "mock",
|
||||
ID: armID, Provider: secureMock(mp), ModelName: "mock",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestForcedArmSupportsTools_ArmWithTools(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/qwen3",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
@@ -45,7 +45,7 @@ func TestForcedArmSupportsTools_ArmWithoutTools(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/gemma",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "gemma",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: false},
|
||||
@@ -62,7 +62,7 @@ func TestBuildRequest_ForcedArmNoToolSupport_OmitsTools(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/gemma",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "gemma",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: false},
|
||||
@@ -74,7 +74,7 @@ func TestBuildRequest_ForcedArmNoToolSupport_OmitsTools(t *testing.T) {
|
||||
reg.Register(&mockTool{name: "bash"})
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: reg,
|
||||
})
|
||||
@@ -92,7 +92,7 @@ func TestBuildRequest_ForcedArmWithToolSupport_IncludesTools(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/qwen3",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3",
|
||||
IsLocal: true,
|
||||
// ContextWindow > 16384 keeps two-stage routing inactive so this
|
||||
@@ -106,7 +106,7 @@ func TestBuildRequest_ForcedArmWithToolSupport_IncludesTools(t *testing.T) {
|
||||
reg.Register(&mockTool{name: "bash"})
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: reg,
|
||||
})
|
||||
@@ -129,7 +129,7 @@ func TestBuildRequest_AllowedToolsFilter(t *testing.T) {
|
||||
reg.Register(&mockTool{name: "agent"})
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Tools: reg,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -160,7 +160,7 @@ func TestBuildRequest_AllowedToolsFilter(t *testing.T) {
|
||||
func TestBuildRequest_Temperature(t *testing.T) {
|
||||
temp := 0.7
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
Temperature: &temp,
|
||||
})
|
||||
@@ -179,7 +179,7 @@ func TestBuildRequest_Temperature(t *testing.T) {
|
||||
|
||||
func TestBuildRequest_TemperatureNilWhenNotSet(t *testing.T) {
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -196,7 +196,7 @@ func TestBuildRequest_MultiArmRouting_IncludesTools(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/gemma",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "gemma",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: false},
|
||||
@@ -207,7 +207,7 @@ func TestBuildRequest_MultiArmRouting_IncludesTools(t *testing.T) {
|
||||
reg.Register(&mockTool{name: "fs.read"})
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: reg,
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestEarlyStop_PatchSpiral_InjectsCorrection(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
_, err := e.Submit(context.Background(), "fix the bug", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
@@ -95,7 +95,7 @@ func TestEarlyStop_PatchSpiral_PerPathIsolation(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
_, err := e.Submit(context.Background(), "edit two files", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
@@ -138,7 +138,7 @@ func TestEarlyStop_GreetingRegression_InjectsCorrection(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
_, err := e.Submit(context.Background(), "inspect /x.go", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
@@ -171,7 +171,7 @@ func TestEarlyStop_NoFalsePositive_GreetingOnFirstTurn(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
_, err := e.Submit(context.Background(), "hello", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
@@ -205,7 +205,7 @@ func TestEarlyStop_Repetition_BreaksAndCorrects(t *testing.T) {
|
||||
streams: []stream.Stream{round1, round2},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
_, err := e.Submit(context.Background(), "do something", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
|
||||
@@ -33,7 +33,6 @@ func (m *mockProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
}}, nil
|
||||
}
|
||||
func (m *mockProvider) IsSecure() bool { return true }
|
||||
func (m *mockProvider) Stream(_ context.Context, _ provider.Request) (stream.Stream, error) {
|
||||
if m.calls >= len(m.streams) {
|
||||
return nil, fmt.Errorf("mock: no more streams (called %d times)", m.calls+1)
|
||||
@@ -43,6 +42,14 @@ func (m *mockProvider) Stream(_ context.Context, _ provider.Request) (stream.Str
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// secureMock wraps a test provider in *security.SafeProvider so it
|
||||
// satisfies router.SecureProvider's sealed Marker. Tests use this at every
|
||||
// Config.Provider site; the wrapper is firewall-less (pass-through), so
|
||||
// behavior is unchanged from before the seal landed.
|
||||
func secureMock(p provider.Provider) router.SecureProvider {
|
||||
return security.WrapProvider(p, nil)
|
||||
}
|
||||
|
||||
// eventStream is a mock stream backed by a slice of events.
|
||||
type eventStream struct {
|
||||
events []stream.Event
|
||||
@@ -100,7 +107,7 @@ func (m *mockTool) Execute(ctx context.Context, args json.RawMessage) (tool.Resu
|
||||
|
||||
func TestNew_ValidConfig(t *testing.T) {
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -119,7 +126,7 @@ func TestNew_MissingProvider(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNew_MissingTools(t *testing.T) {
|
||||
_, err := New(Config{Provider: &mockProvider{name: "test"}})
|
||||
_, err := New(Config{Provider: secureMock(&mockProvider{name: "test"})})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing tool registry")
|
||||
}
|
||||
@@ -137,7 +144,7 @@ func TestSubmit_SimpleTextResponse(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
var events []stream.Event
|
||||
turn, err := e.Submit(context.Background(), "hi", func(evt stream.Event) {
|
||||
@@ -204,7 +211,7 @@ func TestSubmit_ToolCallLoop(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
turn, err := e.Submit(context.Background(), "list files", nil)
|
||||
if err != nil {
|
||||
@@ -265,7 +272,7 @@ func TestSubmit_UnknownTool(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
turn, err := e.Submit(context.Background(), "do something", nil)
|
||||
if err != nil {
|
||||
@@ -300,7 +307,7 @@ func TestSubmit_ToolExecutionError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
turn, err := e.Submit(context.Background(), "do it", nil)
|
||||
if err != nil {
|
||||
@@ -336,7 +343,7 @@ func TestSubmit_MaxTurnsLimit(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg, MaxTurns: 2})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg, MaxTurns: 2})
|
||||
|
||||
_, err := e.Submit(context.Background(), "loop forever", nil)
|
||||
if err == nil {
|
||||
@@ -379,7 +386,7 @@ func TestSubmit_MultipleToolCalls(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
turn, err := e.Submit(context.Background(), "run both", nil)
|
||||
if err != nil {
|
||||
@@ -407,7 +414,7 @@ func TestSubmit_NilCallback(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
// nil callback should not panic
|
||||
turn, err := e.Submit(context.Background(), "test", nil)
|
||||
@@ -430,7 +437,7 @@ func TestEngine_Reset(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
_, _ = e.Submit(context.Background(), "hello", nil)
|
||||
|
||||
if len(e.History()) == 0 {
|
||||
@@ -461,7 +468,7 @@ func TestEngine_Reset_ClearsContextWindow(t *testing.T) {
|
||||
},
|
||||
}
|
||||
e, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: tool.NewRegistry(),
|
||||
Context: ctxWindow,
|
||||
})
|
||||
@@ -503,7 +510,7 @@ func TestSubmit_ContextWindowTracksUserAndToolMessages(t *testing.T) {
|
||||
|
||||
ctxWindow := gnomactx.NewWindow(gnomactx.WindowConfig{MaxTokens: 200_000})
|
||||
e, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Context: ctxWindow,
|
||||
})
|
||||
@@ -542,7 +549,7 @@ func TestSubmit_TrackerReflectsInputTokens(t *testing.T) {
|
||||
),
|
||||
},
|
||||
}
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry(), Context: ctxWindow})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry(), Context: ctxWindow})
|
||||
|
||||
_, _ = e.Submit(context.Background(), "hi", nil)
|
||||
|
||||
@@ -568,7 +575,7 @@ func TestSubmit_CumulativeUsage(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
_, _ = e.Submit(context.Background(), "one", nil)
|
||||
_, _ = e.Submit(context.Background(), "two", nil)
|
||||
@@ -608,7 +615,7 @@ func TestSubmit_UsesInjectedClassifier(t *testing.T) {
|
||||
}
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: armID,
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
ModelName: "mock-model",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
@@ -616,7 +623,7 @@ func TestSubmit_UsesInjectedClassifier(t *testing.T) {
|
||||
|
||||
spy := &spyClassifier{}
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
Classifier: spy,
|
||||
@@ -647,7 +654,7 @@ func TestSubmit_NilClassifierFallsBackToHeuristic(t *testing.T) {
|
||||
}
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: armID,
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
ModelName: "mock-model",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
@@ -655,7 +662,7 @@ func TestSubmit_NilClassifierFallsBackToHeuristic(t *testing.T) {
|
||||
|
||||
// No Classifier set — should not panic, should use heuristic
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
@@ -685,14 +692,14 @@ func TestSubmit_ReportsOutcomeToRouter(t *testing.T) {
|
||||
}
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: armID,
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
ModelName: "mock-model",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
rtr.ForceArm(armID)
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
@@ -731,7 +738,7 @@ func TestSubmit_SuppressesOutcomeWhenIncognito(t *testing.T) {
|
||||
mp := &mockProvider{name: "test", streams: []stream.Stream{makeStream(), makeStream()}}
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: armID,
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
ModelName: "mock-model",
|
||||
Capabilities: provider.Capabilities{ToolUse: true},
|
||||
})
|
||||
@@ -741,7 +748,7 @@ func TestSubmit_SuppressesOutcomeWhenIncognito(t *testing.T) {
|
||||
fw.Incognito().Activate()
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
Firewall: fw,
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestHook_NilDispatcher_NoChange(t *testing.T) {
|
||||
),
|
||||
},
|
||||
}
|
||||
eng, err := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, err := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func TestHook_PreToolUse_Deny(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PreToolUse, &blockingExecutor{}),
|
||||
})
|
||||
@@ -151,7 +151,7 @@ func TestHook_PreToolUse_Allow(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PreToolUse, &allowingExecutor{}),
|
||||
})
|
||||
@@ -181,7 +181,7 @@ func TestHook_PreToolUse_DenyMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PreToolUse, &blockingExecutor{}),
|
||||
})
|
||||
@@ -221,7 +221,7 @@ func TestHook_PreToolUse_Transform(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PreToolUse,
|
||||
&argTransformExecutor{newArgs: json.RawMessage(`{"command":"safe-replacement"}`)}),
|
||||
@@ -254,7 +254,7 @@ func TestHook_PostToolUse_Transform(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PostToolUse,
|
||||
&resultTransformExecutor{newOutput: "transformed output"}),
|
||||
@@ -293,7 +293,7 @@ func TestHook_PostToolUse_DenyTreatedAsSkip(t *testing.T) {
|
||||
}
|
||||
|
||||
eng, _ := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
Hooks: hookDispatcher(hook.PostToolUse, &blockingExecutor{}),
|
||||
})
|
||||
@@ -334,7 +334,7 @@ func TestHook_Stop_MaxTurns(t *testing.T) {
|
||||
),
|
||||
})
|
||||
|
||||
eng, _ := New(Config{Provider: mp, Tools: reg, Hooks: d, MaxTurns: 1})
|
||||
eng, _ := New(Config{Provider: secureMock(mp), Tools: reg, Hooks: d, MaxTurns: 1})
|
||||
_, err := eng.Submit(context.Background(), "run", nil)
|
||||
// MaxTurns exceeded returns an error
|
||||
if err == nil {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestSubmitWithOptions_AllowedPaths_DeniesBash(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
_, err := e.SubmitWithOptions(context.Background(), "run bash",
|
||||
TurnOptions{AllowedPaths: []string{"/tmp"}}, nil)
|
||||
@@ -111,7 +111,7 @@ func TestSubmitWithOptions_AllowedPaths_DeniesOutsidePath(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
_, err := e.SubmitWithOptions(context.Background(), "read file",
|
||||
TurnOptions{AllowedPaths: []string{"/tmp"}}, nil)
|
||||
@@ -150,7 +150,7 @@ func TestSubmitWithOptions_AllowedPaths_AllowsInsidePath(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
_, err := e.SubmitWithOptions(context.Background(), "read file",
|
||||
TurnOptions{AllowedPaths: []string{"/tmp/allowed"}}, nil)
|
||||
@@ -189,7 +189,7 @@ func TestSubmitWithOptions_NilAllowedPaths_NoRestriction(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
// No AllowedPaths → no restriction
|
||||
_, err := e.SubmitWithOptions(context.Background(), "read file", TurnOptions{}, nil)
|
||||
@@ -230,7 +230,7 @@ func TestSubmitWithOptions_AllowedPaths_NonPathSensitiveToolAllowed(t *testing.T
|
||||
// Register arm with tool support
|
||||
from := provider.Capabilities{ToolUse: true}
|
||||
_ = from
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
_, err := e.SubmitWithOptions(context.Background(), "get info",
|
||||
TurnOptions{AllowedPaths: []string{"/tmp"}}, nil)
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestEngine_ConcurrentSubmitAndSetters(t *testing.T) {
|
||||
name: "test",
|
||||
streams: []stream.Stream{newBlockingStream(release, "mock-model")},
|
||||
}
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
@@ -28,7 +28,7 @@ func (d *deferredMockTool) Execute(_ context.Context, _ json.RawMessage) (tool.R
|
||||
|
||||
func TestSetHistory_ReplacesHistory(t *testing.T) {
|
||||
e, _ := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestSetHistory_OverwritesPreviousHistory(t *testing.T) {
|
||||
),
|
||||
},
|
||||
}
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
_, _ = e.Submit(context.Background(), "first message", nil)
|
||||
|
||||
if len(e.History()) == 0 {
|
||||
@@ -83,7 +83,7 @@ func TestSetHistory_OverwritesPreviousHistory(t *testing.T) {
|
||||
func TestSetHistory_SyncsContextWindow(t *testing.T) {
|
||||
ctxWindow := gnomactx.NewWindow(gnomactx.WindowConfig{MaxTokens: 200_000})
|
||||
e, _ := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
Context: ctxWindow,
|
||||
})
|
||||
@@ -106,7 +106,7 @@ func TestSetHistory_SyncsContextWindow(t *testing.T) {
|
||||
func TestSetHistory_SyncsTrackerTokenCount(t *testing.T) {
|
||||
ctxWindow := gnomactx.NewWindow(gnomactx.WindowConfig{MaxTokens: 200_000})
|
||||
e, _ := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
Context: ctxWindow,
|
||||
})
|
||||
@@ -130,7 +130,7 @@ func TestSetHistory_SyncsTrackerTokenCount(t *testing.T) {
|
||||
|
||||
func TestSetHistory_NilContextWindow_NoPanic(t *testing.T) {
|
||||
e, _ := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
// Context intentionally nil
|
||||
})
|
||||
@@ -147,7 +147,7 @@ func TestSetHistory_NilContextWindow_NoPanic(t *testing.T) {
|
||||
|
||||
func TestSetUsage_ReplacesUsage(t *testing.T) {
|
||||
e, _ := New(Config{
|
||||
Provider: &mockProvider{name: "test"},
|
||||
Provider: secureMock(&mockProvider{name: "test"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestSetUsage_OverwritesPreviousUsage(t *testing.T) {
|
||||
),
|
||||
},
|
||||
}
|
||||
e, _ := New(Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
_, _ = e.Submit(context.Background(), "hello", nil)
|
||||
|
||||
if e.Usage().InputTokens == 0 {
|
||||
@@ -205,7 +205,7 @@ func TestSetActivatedTools_DeferredToolIncludedInRequest(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
// Before activation: buildRequest should omit "bash" (deferred).
|
||||
reqBefore := e.buildRequest(context.Background())
|
||||
@@ -237,7 +237,7 @@ func TestSetActivatedTools_EmptyMap_DeactivatesAll(t *testing.T) {
|
||||
reg.Register(&deferredMockTool{name: "bash"})
|
||||
|
||||
mp := &mockProvider{name: "test"}
|
||||
e, _ := New(Config{Provider: mp, Tools: reg})
|
||||
e, _ := New(Config{Provider: secureMock(mp), Tools: reg})
|
||||
|
||||
// Manually activate, then restore to empty.
|
||||
e.activatedTools["bash"] = true
|
||||
|
||||
@@ -31,8 +31,6 @@ func (m *recordingProvider) Models(_ context.Context) ([]provider.ModelInfo, err
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 8192},
|
||||
}}, nil
|
||||
}
|
||||
func (m *recordingProvider) IsSecure() bool { return true }
|
||||
|
||||
func (m *recordingProvider) Stream(_ context.Context, req provider.Request) (stream.Stream, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -90,7 +88,7 @@ func TestTwoStage_FullRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
ForceTwoStageTools: true, // no router needed; just force the path
|
||||
})
|
||||
@@ -161,7 +159,7 @@ func TestTwoStage_InvalidCategoryFallsBackToRoundOne(t *testing.T) {
|
||||
}
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: mp,
|
||||
Provider: secureMock(mp),
|
||||
Tools: reg,
|
||||
ForceTwoStageTools: true,
|
||||
})
|
||||
|
||||
@@ -29,14 +29,14 @@ func twoStageEngine(t *testing.T, reg *tool.Registry) *Engine {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/qwen3-1b",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3-1b",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 8192},
|
||||
})
|
||||
rtr.ForceArm("llamacpp/qwen3-1b")
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: reg,
|
||||
})
|
||||
@@ -49,28 +49,28 @@ func twoStageEngine(t *testing.T, reg *tool.Registry) *Engine {
|
||||
func TestUseTwoStageTools(t *testing.T) {
|
||||
smallLocal := &router.Arm{
|
||||
ID: "llamacpp/qwen3-1b",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3-1b",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 8192},
|
||||
}
|
||||
bigLocal := &router.Arm{
|
||||
ID: "llamacpp/qwen3-30b",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3-30b",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 32768},
|
||||
}
|
||||
cloud := &router.Arm{
|
||||
ID: "anthropic/sonnet",
|
||||
Provider: &mockProvider{name: "anthropic"},
|
||||
Provider: secureMock(&mockProvider{name: "anthropic"}),
|
||||
ModelName: "sonnet",
|
||||
IsLocal: false,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 200000},
|
||||
}
|
||||
localUnknownCtx := &router.Arm{
|
||||
ID: "ollama/mystery",
|
||||
Provider: &mockProvider{name: "ollama"},
|
||||
Provider: secureMock(&mockProvider{name: "ollama"}),
|
||||
ModelName: "mystery",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 0},
|
||||
@@ -118,7 +118,7 @@ func TestUseTwoStageTools(t *testing.T) {
|
||||
rtr.ForceArm(tc.arm.ID)
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: string(tc.arm.ID.Provider())},
|
||||
Provider: secureMock(&mockProvider{name: string(tc.arm.ID.Provider())}),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
ForceTwoStageTools: tc.forced,
|
||||
@@ -136,7 +136,7 @@ func TestUseTwoStageTools(t *testing.T) {
|
||||
|
||||
func TestUseTwoStageTools_NoRouter(t *testing.T) {
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "anthropic"},
|
||||
Provider: secureMock(&mockProvider{name: "anthropic"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -149,7 +149,7 @@ func TestUseTwoStageTools_NoRouter(t *testing.T) {
|
||||
|
||||
func TestUseTwoStageTools_NoRouter_ForcedOverride(t *testing.T) {
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "anthropic"},
|
||||
Provider: secureMock(&mockProvider{name: "anthropic"}),
|
||||
Tools: tool.NewRegistry(),
|
||||
ForceTwoStageTools: true,
|
||||
})
|
||||
@@ -165,14 +165,14 @@ func TestUseTwoStageTools_NoForcedArm(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/qwen3-1b",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3-1b",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 8192},
|
||||
})
|
||||
// No ForceArm called — multi-arm routing
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: tool.NewRegistry(),
|
||||
})
|
||||
@@ -286,7 +286,7 @@ func TestBuildRequest_NonTwoStage_UnchangedBehavior(t *testing.T) {
|
||||
rtr := router.New(router.Config{})
|
||||
rtr.RegisterArm(&router.Arm{
|
||||
ID: "llamacpp/qwen3-30b",
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
ModelName: "qwen3-30b",
|
||||
IsLocal: true,
|
||||
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 32768},
|
||||
@@ -298,7 +298,7 @@ func TestBuildRequest_NonTwoStage_UnchangedBehavior(t *testing.T) {
|
||||
reg.Register(&categorizedMockTool{mockTool: mockTool{name: "fs.write"}, cat: tool.CategoryWrite})
|
||||
|
||||
e, err := New(Config{
|
||||
Provider: &mockProvider{name: "llamacpp"},
|
||||
Provider: secureMock(&mockProvider{name: "llamacpp"}),
|
||||
Router: rtr,
|
||||
Tools: reg,
|
||||
})
|
||||
|
||||
@@ -44,8 +44,6 @@ func (p *recordingProvider) Stream(_ context.Context, req provider.Request) (str
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (p *recordingProvider) IsSecure() bool { return true }
|
||||
|
||||
type finalEventStream struct {
|
||||
events []stream.Event
|
||||
idx int
|
||||
|
||||
@@ -6,17 +6,20 @@ import (
|
||||
"time"
|
||||
|
||||
"somegit.dev/Owlibou/gnoma/internal/provider"
|
||||
"somegit.dev/Owlibou/gnoma/internal/security"
|
||||
)
|
||||
|
||||
// ArmID uniquely identifies a model+provider pair.
|
||||
type ArmID string
|
||||
|
||||
// SecureProvider is the interface that all router arms must satisfy.
|
||||
// It ensures that the provider has been wrapped with security controls
|
||||
// (e.g. security.SafeProvider).
|
||||
// SecureProvider is the interface that all router arms must satisfy. It
|
||||
// embeds security.Marker — a sealed trait whose unexported marker method
|
||||
// can only be satisfied by types defined in internal/security. That makes
|
||||
// "the provider passed in has been wrapped" a compile-time guarantee, not
|
||||
// a convention enforced by reviewers.
|
||||
type SecureProvider interface {
|
||||
provider.Provider
|
||||
IsSecure() bool
|
||||
security.Marker
|
||||
}
|
||||
|
||||
// Arm represents a provider+model pair available for routing.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"somegit.dev/Owlibou/gnoma/internal/provider"
|
||||
"somegit.dev/Owlibou/gnoma/internal/security"
|
||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||
)
|
||||
|
||||
@@ -144,7 +145,7 @@ func TestReconcileArms_NoForcedArm(t *testing.T) {
|
||||
}
|
||||
|
||||
factory := func(name, model string) SecureProvider {
|
||||
return &stubProvider{name: name, model: model}
|
||||
return security.WrapProvider(&stubProvider{name: name, model: model}, nil)
|
||||
}
|
||||
|
||||
reconcileArms(r, discovered, factory, slog.Default(), nil)
|
||||
@@ -216,7 +217,6 @@ func (s *stubProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
|
||||
func (s *stubProvider) Stream(_ context.Context, _ provider.Request) (stream.Stream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubProvider) IsSecure() bool { return true }
|
||||
|
||||
// --- DiscoverOllama / cache + default context size ---
|
||||
|
||||
|
||||
@@ -40,11 +40,21 @@ func (p *SafeProvider) Inner() provider.Provider {
|
||||
return p.inner
|
||||
}
|
||||
|
||||
// IsSecure returns true. Satisfies the router's SecureProvider interface.
|
||||
func (p *SafeProvider) IsSecure() bool {
|
||||
return true
|
||||
// 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 {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"somegit.dev/Owlibou/gnoma/internal/engine"
|
||||
"somegit.dev/Owlibou/gnoma/internal/message"
|
||||
"somegit.dev/Owlibou/gnoma/internal/provider"
|
||||
"somegit.dev/Owlibou/gnoma/internal/router"
|
||||
"somegit.dev/Owlibou/gnoma/internal/security"
|
||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
||||
)
|
||||
@@ -26,7 +28,6 @@ type mockProvider struct {
|
||||
|
||||
func (m *mockProvider) Name() string { return m.name }
|
||||
func (m *mockProvider) DefaultModel() string { return "mock-model" }
|
||||
func (m *mockProvider) IsSecure() bool { return true }
|
||||
func (m *mockProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -39,6 +40,12 @@ func (m *mockProvider) Stream(_ context.Context, _ provider.Request) (stream.Str
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// secureMock wraps a test provider in *security.SafeProvider so it
|
||||
// satisfies router.SecureProvider's sealed Marker.
|
||||
func secureMock(p provider.Provider) router.SecureProvider {
|
||||
return security.WrapProvider(p, nil)
|
||||
}
|
||||
|
||||
type eventStream struct {
|
||||
events []stream.Event
|
||||
idx int
|
||||
@@ -67,7 +74,7 @@ func TestLocal_SendAndReceive(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "mock-model"})
|
||||
|
||||
// Initial state
|
||||
@@ -122,7 +129,7 @@ func TestLocal_SendWhileBusy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "model"})
|
||||
|
||||
_ = sess.Send("first")
|
||||
@@ -149,7 +156,7 @@ func TestLocal_Cancel(t *testing.T) {
|
||||
streams: []stream.Stream{&slowStream{events: events}},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "model"})
|
||||
|
||||
_ = sess.Send("slow task")
|
||||
@@ -172,7 +179,7 @@ func TestLocal_Cancel(t *testing.T) {
|
||||
|
||||
func TestLocal_Close(t *testing.T) {
|
||||
mp := &mockProvider{name: "test"}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "model"})
|
||||
|
||||
if err := sess.Close(); err != nil {
|
||||
@@ -200,7 +207,7 @@ func TestLocal_StatusTracking(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "mock-model"})
|
||||
|
||||
// Turn 1
|
||||
@@ -259,7 +266,7 @@ func TestLocal_AutoSave(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
store := NewSessionStore(t.TempDir(), 10, slog.Default())
|
||||
sess := NewLocal(LocalConfig{
|
||||
Engine: eng,
|
||||
@@ -303,7 +310,7 @@ func TestLocal_AutoSave_SkipsWhenNoStore(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
// No store — must not panic
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "mock-model"})
|
||||
|
||||
@@ -321,7 +328,7 @@ func TestLocal_AutoSave_SkipsWhenNoStore(t *testing.T) {
|
||||
|
||||
func TestLocal_SessionID(t *testing.T) {
|
||||
mp := &mockProvider{name: "test"}
|
||||
eng, _ := engine.New(engine.Config{Provider: mp, Tools: tool.NewRegistry()})
|
||||
eng, _ := engine.New(engine.Config{Provider: secureMock(mp), Tools: tool.NewRegistry()})
|
||||
sess := NewLocal(LocalConfig{Engine: eng, Provider: "test", Model: "m", SessionID: "my-id"})
|
||||
if sess.SessionID() != "my-id" {
|
||||
t.Errorf("SessionID() = %q, want %q", sess.SessionID(), "my-id")
|
||||
|
||||
@@ -24,7 +24,6 @@ func (m *mockProvider) DefaultModel() string { return "default" }
|
||||
func (m *mockProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockProvider) IsSecure() bool { return true }
|
||||
func (m *mockProvider) Stream(ctx context.Context, _ provider.Request) (stream.Stream, error) {
|
||||
if m.delay > 0 {
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user