diff --git a/internal/elf/elf_test.go b/internal/elf/elf_test.go index 5709f86..95a7b50 100644 --- a/internal/elf/elf_test.go +++ b/internal/elf/elf_test.go @@ -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}, }) diff --git a/internal/engine/buildrequest_test.go b/internal/engine/buildrequest_test.go index f45119b..cedfde3 100644 --- a/internal/engine/buildrequest_test.go +++ b/internal/engine/buildrequest_test.go @@ -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, }) diff --git a/internal/engine/earlystop_integration_test.go b/internal/engine/earlystop_integration_test.go index 839666f..36d69da 100644 --- a/internal/engine/earlystop_integration_test.go +++ b/internal/engine/earlystop_integration_test.go @@ -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) diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index d0cfb00..03e320b 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -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, diff --git a/internal/engine/hook_integration_test.go b/internal/engine/hook_integration_test.go index b22f6b3..a0e95a3 100644 --- a/internal/engine/hook_integration_test.go +++ b/internal/engine/hook_integration_test.go @@ -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 { diff --git a/internal/engine/paths_test.go b/internal/engine/paths_test.go index c4dc4f6..95c40da 100644 --- a/internal/engine/paths_test.go +++ b/internal/engine/paths_test.go @@ -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) diff --git a/internal/engine/race_test.go b/internal/engine/race_test.go index c045885..9eeacbf 100644 --- a/internal/engine/race_test.go +++ b/internal/engine/race_test.go @@ -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) diff --git a/internal/engine/restore_test.go b/internal/engine/restore_test.go index 985826d..7b1be0a 100644 --- a/internal/engine/restore_test.go +++ b/internal/engine/restore_test.go @@ -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 diff --git a/internal/engine/twostage_integration_test.go b/internal/engine/twostage_integration_test.go index 47ea826..fb9fe5b 100644 --- a/internal/engine/twostage_integration_test.go +++ b/internal/engine/twostage_integration_test.go @@ -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, }) diff --git a/internal/engine/twostage_test.go b/internal/engine/twostage_test.go index 4c4282c..5187b8e 100644 --- a/internal/engine/twostage_test.go +++ b/internal/engine/twostage_test.go @@ -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, }) diff --git a/internal/hook/posttooluse_redaction_test.go b/internal/hook/posttooluse_redaction_test.go index 57caea0..a5b07ff 100644 --- a/internal/hook/posttooluse_redaction_test.go +++ b/internal/hook/posttooluse_redaction_test.go @@ -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 diff --git a/internal/router/arm.go b/internal/router/arm.go index ed87d19..f3dc6cf 100644 --- a/internal/router/arm.go +++ b/internal/router/arm.go @@ -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. diff --git a/internal/router/discovery_test.go b/internal/router/discovery_test.go index 6a6afb7..4f33516 100644 --- a/internal/router/discovery_test.go +++ b/internal/router/discovery_test.go @@ -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 --- diff --git a/internal/security/safeprovider.go b/internal/security/safeprovider.go index 51d6f36..e0b008d 100644 --- a/internal/security/safeprovider.go +++ b/internal/security/safeprovider.go @@ -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 { diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 3069764..c820e92 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -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") diff --git a/internal/slm/classifier_test.go b/internal/slm/classifier_test.go index 50c6833..d434bb5 100644 --- a/internal/slm/classifier_test.go +++ b/internal/slm/classifier_test.go @@ -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 {