diff --git a/core/services/agents/config.go b/core/services/agents/config.go index 0016cf51e..875eedd36 100644 --- a/core/services/agents/config.go +++ b/core/services/agents/config.go @@ -107,6 +107,13 @@ type AgentConfig struct { LoopDetection int `json:"loop_detection"` EnableAutoCompaction bool `json:"enable_auto_compaction"` AutoCompactionThreshold int `json:"auto_compaction_threshold"` + + // Tool policy (see toolpolicy.go) + RequiredToolBeforeFinish string `json:"required_tool_before_finish"` + RequiredToolBeforeFinishPrompt string `json:"required_tool_before_finish_prompt"` + RequiredToolBeforeFinishAttempts int `json:"required_tool_before_finish_attempts"` + AllowedTools ToolNames `json:"allowed_tools"` + ExcludedTools ToolNames `json:"excluded_tools"` } // ConnectorConfig defines a connector integration (Slack, Discord, etc.). diff --git a/core/services/agents/configmeta.go b/core/services/agents/configmeta.go index d767aaff8..9f36ed1a9 100644 --- a/core/services/agents/configmeta.go +++ b/core/services/agents/configmeta.go @@ -134,6 +134,14 @@ func defaultFields() []ConfigField { {Name: "enable_reasoning_tool", Label: "Enable Reasoning for Tools", Type: FieldCheckbox, DefaultValue: true, Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, {Name: "enable_reasoning_for_instruct", Label: "Enable Reasoning for Instruct Models", Type: FieldCheckbox, DefaultValue: false, HelpText: "Force structured reasoning before tool selection (recommended for instruct-tuned models)", Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, {Name: "enable_guided_tools", Label: "Enable Guided Tools", Type: FieldCheckbox, DefaultValue: false, HelpText: "Filter tools through guidance using descriptions", Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, + {Name: "allowed_tools", Label: "Allowed Tools", Type: FieldTextarea, DefaultValue: "", Placeholder: "get_document_content, search", + HelpText: "Comma or newline separated tool names. When set, the agent is offered only these tools (actions, knowledge base tools and MCP tools). send_message, stop and update_state are always kept. Leave empty to offer every tool.", + Tags: ConfigFieldTags{Section: "AdvancedSettings"}, + }, + {Name: "excluded_tools", Label: "Excluded Tools", Type: FieldTextarea, DefaultValue: "", Placeholder: "search_memory", + HelpText: "Comma or newline separated tool names that are never offered to the agent, even if they are in Allowed Tools. send_message, stop and update_state cannot be excluded; use their own settings instead.", + Tags: ConfigFieldTags{Section: "AdvancedSettings"}, + }, {Name: "enable_skills", Label: "Enable Skills", Type: FieldCheckbox, DefaultValue: false, HelpText: "Inject skills into the agent", Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, {Name: "skills_mode", Label: "Skills Injection Mode", Type: FieldSelect, DefaultValue: "prompt", Options: []ConfigFieldOption{ @@ -146,6 +154,18 @@ func defaultFields() []ConfigField { }, {Name: "parallel_jobs", Label: "Parallel Jobs", Type: FieldNumber, DefaultValue: 5, Min: 1, Step: 1, Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, {Name: "max_attempts", Label: "Max Attempts", Type: FieldNumber, DefaultValue: 2, Min: 1, Step: 1, Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, + {Name: "required_tool_before_finish", Label: "Required Tool Before Finish", Type: FieldText, DefaultValue: "", Placeholder: "check_policy", + HelpText: "Name of a tool the agent must call successfully (a JSON result with \"ok\": true) before it may send its final answer. Has no effect if the agent does not have this tool. Leave empty to disable.", + Tags: ConfigFieldTags{Section: "AdvancedSettings"}, + }, + {Name: "required_tool_before_finish_prompt", Label: "Required Tool Prompt", Type: FieldTextarea, DefaultValue: "", + HelpText: "Instruction sent to the model when it tries to finish before the required tool has passed. Leave empty to use a default that names the tool.", + Tags: ConfigFieldTags{Section: "AdvancedSettings"}, + }, + {Name: "required_tool_before_finish_attempts", Label: "Required Tool Attempts", Type: FieldNumber, DefaultValue: 3, Min: 1, Step: 1, + HelpText: "How many times the model is told to run the required tool before its answer is sent anyway", + Tags: ConfigFieldTags{Section: "AdvancedSettings"}, + }, {Name: "max_iterations", Label: "Max Iterations", Type: FieldNumber, DefaultValue: 1, Min: 1, Step: 1, HelpText: "Maximum tool loop iterations per execution", Tags: ConfigFieldTags{Section: "AdvancedSettings"}}, // MCP diff --git a/core/services/agents/executor.go b/core/services/agents/executor.go index 2787aeabc..9fa3c336e 100644 --- a/core/services/agents/executor.go +++ b/core/services/agents/executor.go @@ -4,6 +4,7 @@ import ( "cmp" "context" "encoding/json" + "errors" "fmt" "strings" "time" @@ -181,6 +182,11 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m // Build cogito options var cogitoOpts []cogito.Option + // Local tools are collected first so the tool filter applies to all of + // them at once; cogito only runs tools it offered, so filtering what is + // offered also filters the lookup of the model's tool calls. + var localTools []cogito.ToolDefinitionInterface + filter := newToolFilter(cfg.AllowedTools, cfg.ExcludedTools) // MCP sessions sessions, cleanup := setupMCPSessions(ctx, cfg) @@ -188,7 +194,7 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m defer cleanup() } if len(sessions) > 0 { - cogitoOpts = append(cogitoOpts, cogito.WithMCPs(sessions...)) + cogitoOpts = append(cogitoOpts, cogito.WithMCPs(sessions...), cogito.WithMCPToolFilter(filter.mcpToolFilter())) } // KB tools (search_memory / add_memory) — when kb mode is "tools" or "both" @@ -197,7 +203,7 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m if kbResults <= 0 { kbResults = 5 } - cogitoOpts = append(cogitoOpts, cogito.WithTools( + localTools = append(localTools, cogito.NewToolDefinition( KBSearchMemoryTool{APIURL: effectiveURL, APIKey: effectiveKey, Collection: cfg.Name, MaxResults: kbResults, UserID: userID, CitationCollector: kbCitations}, KBSearchMemoryArgs{}, @@ -210,7 +216,7 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m "add_memory", "Store content in memory for later retrieval", ), - )) + ) } // Skill tools — when skills_mode is "tools" or "both" @@ -220,18 +226,36 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m allSkills, _ := skillProvider.ListSkills() filtered := FilterSkills(allSkills, cfg.SelectedSkills) if len(filtered) > 0 { - cogitoOpts = append(cogitoOpts, cogito.WithTools( + localTools = append(localTools, cogito.NewToolDefinition( RequestSkillTool{Skills: filtered}, RequestSkillArgs{}, "request_skill", "Request a skill by name. Available skills: "+skillNames(filtered), ), - )) + ) } } } + localTools = filter.filterTools(localTools) + if len(localTools) > 0 { + cogitoOpts = append(cogitoOpts, cogito.WithTools(localTools...)) + } + + // Required-tool gate: the agent must run the configured tool to success + // before its answer is final. It is enforced on the output because a + // model follows "always call X first" unreliably. + requiredTool := cfg.RequiredToolBeforeFinish + requiredPassed := false + requiredAttempts := 0 + maxRequiredAttempts := cfg.RequiredToolBeforeFinishAttempts + if maxRequiredAttempts <= 0 { + maxRequiredAttempts = defaultRequiredFinishAttempts + } + requiredPrompt := requiredFinishPromptFor(requiredTool, cfg.RequiredToolBeforeFinishPrompt) + requiredAvailable := requiredTool != "" && requiredToolAvailable(ctx, requiredTool, localTools, sessions, filter) + // Sink state is always disabled — the agent responds directly when no tools match. cogitoOpts = append(cogitoOpts, cogito.DisableSinkState) @@ -250,8 +274,11 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m } // Tool call result callback - if cb.OnToolResult != nil || cb.OnToolCall != nil { + if cb.OnToolResult != nil || cb.OnToolCall != nil || requiredAvailable { cogitoOpts = append(cogitoOpts, cogito.WithToolCallResultCallback(func(t cogito.ToolStatus) { + if requiredAvailable && t.Name == requiredTool && requiredToolResultOK(t.Result) { + requiredPassed = true + } if isInternalCogitoTool(t.Name) { return } @@ -327,6 +354,32 @@ func ExecuteChatWithLLM(ctx context.Context, llm cogito.LLM, cfg *AgentConfig, m return "", fmt.Errorf("agent execution failed: %w", err) } + for len(result.Messages) > 0 && textFinalizationNeedsRequiredTool(requiredAvailable, requiredPassed, + requiredAttempts, maxRequiredAttempts, result.LastMessage().Role, result.LastMessage().Content) { + requiredAttempts++ + xlog.Info("required-tool gate: answer without the required tool, nudging", + "agent", cfg.Name, "tool", requiredTool, "attempt", requiredAttempts) + answered := result + next, err := cogito.ExecuteTools(llm, result.AddMessage(cogito.UserMessageRole, requiredPrompt), cogitoOpts...) + if err != nil && ctx.Err() != nil { + if cb.OnStatus != nil { + cb.OnStatus("error: " + err.Error()) + } + return "", fmt.Errorf("agent execution failed: %w", err) + } + // A failed retry must not throw away the answer the model already gave. + if err != nil && !errors.Is(err, cogito.ErrNoToolSelected) { + xlog.Error("required-tool gate: retry failed, keeping the previous answer", "agent", cfg.Name, "error", err) + result = answered + break + } + result = next + } + if requiredAvailable && !requiredPassed && requiredAttempts >= maxRequiredAttempts { + xlog.Warn("required-tool gate: bypass after max attempts, answer finalized ungated", + "agent", cfg.Name, "tool", requiredTool) + } + // Extract response response := "" if len(result.Messages) > 0 { diff --git a/core/services/agents/toolpolicy.go b/core/services/agents/toolpolicy.go new file mode 100644 index 000000000..cb5d97cdd --- /dev/null +++ b/core/services/agents/toolpolicy.go @@ -0,0 +1,220 @@ +package agents + +// Tool policy for the distributed executor: the allowed/excluded tool lists and +// the required-tool-before-finish gate. The semantics mirror LocalAGI's +// core/agent/toolfilter.go and the gate in core/agent/agent.go. Those helpers +// are unexported there, so the small pieces below are kept in step by hand; the +// meta parity spec in toolpolicy_test.go catches drift in the form fields. + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + gomcp "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mudler/cogito" + "github.com/mudler/xlog" +) + +// ToolNames is a list of tool names. The agent form submits it as a comma or +// newline separated string and the API as a JSON array, so both are accepted. +type ToolNames []string + +// UnmarshalJSON accepts a JSON array of strings, a comma or newline separated +// string, or null. Names are trimmed and empty entries dropped. +func (t *ToolNames) UnmarshalJSON(data []byte) error { + var value any + if err := json.Unmarshal(data, &value); err != nil { + return err + } + var raw []string + switch v := value.(type) { + case nil: + *t = nil + return nil + case string: + raw = strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' }) + case []any: + for _, item := range v { + name, ok := item.(string) + if !ok { + return fmt.Errorf("expected a list of tool names, got %T", item) + } + raw = append(raw, name) + } + default: + return fmt.Errorf("expected a list of tool names or a comma separated string, got %T", value) + } + var names ToolNames + for _, n := range raw { + if n = strings.TrimSpace(n); n != "" { + names = append(names, n) + } + } + *t = names + return nil +} + +// controlActionNames are LocalAGI's loop-driving actions. The distributed +// executor does not offer them today, but an agent config is shared between +// both modes, so the filter must treat them the same way in both. +var controlActionNames = map[string]struct{}{ + "send_message": {}, + "stop": {}, + "update_state": {}, +} + +// toolFilter is an allow/deny list over tool names. A nil *toolFilter allows +// everything. +type toolFilter struct { + allow map[string]struct{} + deny map[string]struct{} +} + +func newToolFilter(allow, deny []string) *toolFilter { + f := &toolFilter{allow: toNameSet(allow), deny: toNameSet(deny)} + if len(f.allow) == 0 && len(f.deny) == 0 { + return nil + } + return f +} + +func toNameSet(names []string) map[string]struct{} { + set := make(map[string]struct{}, len(names)) + for _, n := range names { + if n = strings.TrimSpace(n); n != "" { + set[n] = struct{}{} + } + } + return set +} + +func (f *toolFilter) allows(name string) bool { + if f == nil { + return true + } + if _, ok := controlActionNames[name]; ok { + return true + } + if _, denied := f.deny[name]; denied { + return false + } + if len(f.allow) == 0 { + return true + } + _, allowed := f.allow[name] + return allowed +} + +func (f *toolFilter) filterTools(tools []cogito.ToolDefinitionInterface) []cogito.ToolDefinitionInterface { + if f == nil { + return tools + } + out := make([]cogito.ToolDefinitionInterface, 0, len(tools)) + for _, t := range tools { + if f.allows(t.Tool().Function.Name) { + out = append(out, t) + } + } + return out +} + +// mcpToolFilter is needed on top of filterTools because cogito discovers MCP +// tools straight from the live sessions. +func (f *toolFilter) mcpToolFilter() cogito.MCPToolFilter { + if f == nil { + return nil + } + return func(_ *gomcp.ClientSession, toolName string) bool { + return f.allows(toolName) + } +} + +// defaultRequiredFinishAttempts bounds the reminders: a gate that can loop +// forever is worse than one that gives up loudly. +const defaultRequiredFinishAttempts = 3 + +func requiredFinishPromptFor(tool, override string) string { + if override != "" { + return override + } + return "Before you send your final answer you MUST first call the tool " + tool + + " and it must succeed (ok:true). Call " + tool + " now; only send the final " + + "message after it passes." +} + +// requiredToolResultOK reports whether a tool result is a JSON object with a +// top-level "ok": true. When the result is not JSON as a whole (MCP content +// may wrap it in text), each top-level object embedded in it is checked. +func requiredToolResultOK(result string) bool { + trimmed := strings.TrimSpace(result) + if json.Valid([]byte(trimmed)) { + return jsonObjectOK([]byte(trimmed)) + } + for i := 0; i < len(result); { + j := strings.IndexByte(result[i:], '{') + if j < 0 { + return false + } + start := i + j + dec := json.NewDecoder(strings.NewReader(result[start:])) + var raw json.RawMessage + if err := dec.Decode(&raw); err != nil { + i = start + 1 + continue + } + if jsonObjectOK(raw) { + return true + } + // Skip the whole object so its nested objects are not checked on their own. + i = start + int(dec.InputOffset()) + } + return false +} + +func jsonObjectOK(data []byte) bool { + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err != nil { + return false + } + var ok bool + if err := json.Unmarshal(obj["ok"], &ok); err != nil { + return false + } + return ok +} + +// requiredToolAvailable reports whether the model is offered the required +// tool. The gate stays inert otherwise, so a pool-wide setting is harmless for +// agents that lack the tool. MCP sessions are only listed when the tool is not +// a local one. +func requiredToolAvailable(ctx context.Context, name string, local []cogito.ToolDefinitionInterface, sessions []*gomcp.ClientSession, filter *toolFilter) bool { + if name == "" || !filter.allows(name) { + return false + } + if cogito.Tools(local).Find(name) != nil { + return true + } + for _, s := range sessions { + res, err := s.ListTools(ctx, nil) + if err != nil { + xlog.Warn("required-tool gate: failed to list MCP tools", "error", err) + continue + } + for _, t := range res.Tools { + if t.Name == name { + return true + } + } + } + return false +} + +// textFinalizationNeedsRequiredTool reports whether the run ended with a +// non-empty assistant answer although the required tool has not passed and +// reminders are left. +func textFinalizationNeedsRequiredTool(toolAvailable, toolPassed bool, attempts, max int, lastRole, lastContent string) bool { + return toolAvailable && !toolPassed && attempts < max && + lastRole == "assistant" && strings.TrimSpace(lastContent) != "" +} diff --git a/core/services/agents/toolpolicy_test.go b/core/services/agents/toolpolicy_test.go new file mode 100644 index 000000000..4a18d3f80 --- /dev/null +++ b/core/services/agents/toolpolicy_test.go @@ -0,0 +1,387 @@ +package agents + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sort" + "sync" + "sync/atomic" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mudler/LocalAGI/core/state" + "github.com/mudler/cogito" + openai "github.com/sashabaranov/go-openai" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// mcpFixture serves an MCP server over SSE whose tools return fixed results, +// so the executor reaches it through the same transport as a real agent. +type mcpFixture struct { + server *httptest.Server + calls map[string]*atomic.Int32 +} + +func newMCPFixture(results map[string]string) *mcpFixture { + srv := mcp.NewServer(&mcp.Implementation{Name: "fixture", Version: "v0.0.1"}, nil) + fx := &mcpFixture{calls: map[string]*atomic.Int32{}} + for name, result := range results { + counter := &atomic.Int32{} + fx.calls[name] = counter + srv.AddTool(&mcp.Tool{ + Name: name, + Description: "fixture tool " + name, + InputSchema: json.RawMessage(`{"type":"object","properties":{}}`), + }, func(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + counter.Add(1) + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: result}}}, nil + }) + } + fx.server = httptest.NewServer(mcp.NewSSEHandler(func(*http.Request) *mcp.Server { return srv }, nil)) + return fx +} + +func (fx *mcpFixture) close() { fx.server.Close() } +func (fx *mcpFixture) callCount(n string) int32 { return fx.calls[n].Load() } + +// policyLLM answers each chat completion through respond (plain text answer +// when respond is nil) and records every +// request, so specs can see which tools were offered and which messages the +// executor added. +type policyLLM struct { + mu sync.Mutex + requests []openai.ChatCompletionRequest + asked [][]openai.ChatCompletionMessage + respond func(req openai.ChatCompletionRequest) openai.ChatCompletionMessage + answer string +} + +func (m *policyLLM) Ask(_ context.Context, f cogito.Fragment) (cogito.Fragment, error) { + m.mu.Lock() + m.asked = append(m.asked, append([]openai.ChatCompletionMessage(nil), f.Messages...)) + m.mu.Unlock() + return f.AddMessage(cogito.AssistantMessageRole, m.answer), nil +} + +func (m *policyLLM) CreateChatCompletion(_ context.Context, req openai.ChatCompletionRequest) (cogito.LLMReply, cogito.LLMUsage, error) { + m.mu.Lock() + m.requests = append(m.requests, req) + m.mu.Unlock() + msg := openai.ChatCompletionMessage{Role: "assistant", Content: m.answer} + if m.respond != nil { + msg = m.respond(req) + } + return cogito.LLMReply{ + ChatCompletionResponse: openai.ChatCompletionResponse{ + Choices: []openai.ChatCompletionChoice{{Message: msg}}, + }, + }, cogito.LLMUsage{}, nil +} + +// offeredTools returns the sorted tool names of the first request that +// offered tools to the model. +func (m *policyLLM) offeredTools() []string { + m.mu.Lock() + defer m.mu.Unlock() + for _, req := range m.requests { + if len(req.Tools) == 0 { + continue + } + names := []string{} + for _, t := range req.Tools { + if t.Function != nil { + names = append(names, t.Function.Name) + } + } + sort.Strings(names) + return names + } + return nil +} + +// nudges counts the user messages carrying prompt in the longest conversation +// the model saw, which is the number of times the gate sent it. +func (m *policyLLM) nudges(prompt string) int { + m.mu.Lock() + defer m.mu.Unlock() + best := 0 + count := func(msgs []openai.ChatCompletionMessage) { + n := 0 + for _, msg := range msgs { + if msg.Role == "user" && msg.Content == prompt { + n++ + } + } + if n > best { + best = n + } + } + for _, req := range m.requests { + count(req.Messages) + } + for _, msgs := range m.asked { + count(msgs) + } + return best +} + +func toolCallMessage(name string) openai.ChatCompletionMessage { + return openai.ChatCompletionMessage{ + Role: "assistant", + ToolCalls: []openai.ToolCall{{ + ID: "call-" + name, + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{Name: name, Arguments: `{}`}, + }}, + } +} + +func lastMessage(req openai.ChatCompletionRequest) openai.ChatCompletionMessage { + if len(req.Messages) == 0 { + return openai.ChatCompletionMessage{} + } + return req.Messages[len(req.Messages)-1] +} + +var _ = Describe("tool policy settings", func() { + Describe("config parsing", func() { + It("accepts the tool lists as a comma or newline separated string", func() { + var cfg AgentConfig + Expect(ParseConfigJSON(`{"allowed_tools":"a, b\nc,,","excluded_tools":" d \r\n"}`, &cfg)).To(Succeed()) + Expect([]string(cfg.AllowedTools)).To(Equal([]string{"a", "b", "c"})) + Expect([]string(cfg.ExcludedTools)).To(Equal([]string{"d"})) + }) + + It("accepts the tool lists as a JSON array", func() { + var cfg AgentConfig + Expect(ParseConfigJSON(`{"allowed_tools":["a"," b ",""],"excluded_tools":null}`, &cfg)).To(Succeed()) + Expect([]string(cfg.AllowedTools)).To(Equal([]string{"a", "b"})) + Expect(cfg.ExcludedTools).To(BeEmpty()) + }) + + It("rejects a list with non-string entries", func() { + var cfg AgentConfig + Expect(ParseConfigJSON(`{"allowed_tools":[1]}`, &cfg)).ToNot(Succeed()) + }) + + It("keeps every setting when the config is stored through LocalAGI's config", func() { + // The REST handlers decode into state.AgentConfig and store its JSON; + // the distributed dispatcher decodes that JSON into AgentConfig. + var in state.AgentConfig + Expect(json.Unmarshal([]byte(`{ + "name": "a", + "allowed_tools": "search, check_policy", + "excluded_tools": ["add_memory"], + "required_tool_before_finish": "check_policy", + "required_tool_before_finish_prompt": "run it", + "required_tool_before_finish_attempts": 4 + }`), &in)).To(Succeed()) + stored, err := json.Marshal(in) + Expect(err).ToNot(HaveOccurred()) + + var out AgentConfig + Expect(ParseConfigJSON(string(stored), &out)).To(Succeed()) + Expect([]string(out.AllowedTools)).To(Equal([]string{"search", "check_policy"})) + Expect([]string(out.ExcludedTools)).To(Equal([]string{"add_memory"})) + Expect(out.RequiredToolBeforeFinish).To(Equal("check_policy")) + Expect(out.RequiredToolBeforeFinishPrompt).To(Equal("run it")) + Expect(out.RequiredToolBeforeFinishAttempts).To(Equal(4)) + + again, err := json.Marshal(out) + Expect(err).ToNot(HaveOccurred()) + var back state.AgentConfig + Expect(json.Unmarshal(again, &back)).To(Succeed()) + Expect(back.AllowedTools).To(Equal([]string{"search", "check_policy"})) + Expect(back.RequiredToolBeforeFinishAttempts).To(Equal(4)) + }) + }) + + Describe("config meta", func() { + It("describes the settings exactly like LocalAGI does", func() { + upstream := map[string]ConfigField{} + for _, f := range state.NewAgentConfigMeta(nil, nil, nil, nil).Fields { + upstream[f.Name] = ConfigField{ + Name: f.Name, Type: string(f.Type), Label: f.Label, DefaultValue: f.DefaultValue, + Placeholder: f.Placeholder, HelpText: f.HelpText, Min: f.Min, Max: f.Max, Step: f.Step, + Tags: ConfigFieldTags{Section: f.Tags.Section}, + } + } + local := map[string]ConfigField{} + for _, f := range DefaultConfigMeta().Fields { + local[f.Name] = f + } + for _, name := range []string{ + "allowed_tools", "excluded_tools", + "required_tool_before_finish", "required_tool_before_finish_prompt", "required_tool_before_finish_attempts", + } { + Expect(upstream).To(HaveKey(name)) + Expect(local).To(HaveKeyWithValue(name, upstream[name]), name) + } + }) + }) + + Describe("tool filter", func() { + It("keeps the control actions even when they are excluded or not allowed", func() { + f := newToolFilter([]string{"search"}, []string{"send_message", "stop", "update_state", "search"}) + for _, name := range []string{"send_message", "stop", "update_state"} { + Expect(f.allows(name)).To(BeTrue(), name) + } + Expect(f.allows("search")).To(BeFalse()) + Expect(f.allows("other")).To(BeFalse()) + }) + }) + + Describe("ExecuteChatWithLLM", func() { + var fx *mcpFixture + + BeforeEach(func() { + fx = newMCPFixture(map[string]string{ + "check_policy": `{"ok":true}`, + "mcp_allowed": "allowed result", + "mcp_blocked": "blocked result", + }) + }) + + AfterEach(func() { fx.close() }) + + baseConfig := func() *AgentConfig { + return &AgentConfig{ + Name: "policy-agent", + Model: "test-model", + MCPServers: []MCPServer{{URL: fx.server.URL}}, + EnableKnowledgeBase: true, + KBMode: KBModeTools, + } + } + + Context("with allowed and excluded tools", func() { + It("offers the model only the allowed tools that are not excluded, MCP tools included", func() { + llm := &policyLLM{answer: "final"} + cfg := baseConfig() + cfg.AllowedTools = ToolNames{"mcp_allowed", "search_memory", "add_memory"} + cfg.ExcludedTools = ToolNames{"add_memory"} + + _, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(llm.offeredTools()).To(Equal([]string{"mcp_allowed", "search_memory"})) + }) + + It("offers every tool when no list is set", func() { + llm := &policyLLM{answer: "final"} + _, err := ExecuteChatWithLLM(context.Background(), llm, baseConfig(), "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(llm.offeredTools()).To(Equal([]string{"add_memory", "check_policy", "mcp_allowed", "mcp_blocked", "search_memory"})) + }) + + It("does not run a filtered MCP tool the model calls anyway", func() { + var calls atomic.Int32 + llm := &policyLLM{answer: "final", respond: func(openai.ChatCompletionRequest) openai.ChatCompletionMessage { + if calls.Add(1) == 1 { + return toolCallMessage("mcp_blocked") + } + return openai.ChatCompletionMessage{Role: "assistant", Content: "done"} + }} + cfg := baseConfig() + cfg.ExcludedTools = ToolNames{"mcp_blocked"} + + _, _ = ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(fx.callCount("mcp_blocked")).To(BeZero()) + }) + }) + + Context("with a required tool before finish", func() { + const prompt = "RUN check_policy NOW" + + It("nudges the model until the required tool passes, then returns its answer", func() { + llm := &policyLLM{answer: "final answer", respond: func(req openai.ChatCompletionRequest) openai.ChatCompletionMessage { + if last := lastMessage(req); last.Role == "user" && last.Content == prompt { + return toolCallMessage("check_policy") + } + return openai.ChatCompletionMessage{Role: "assistant", Content: "final answer"} + }} + cfg := baseConfig() + cfg.RequiredToolBeforeFinish = "check_policy" + cfg.RequiredToolBeforeFinishPrompt = prompt + + result, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("final answer")) + Expect(fx.callCount("check_policy")).To(Equal(int32(1))) + Expect(llm.nudges(prompt)).To(Equal(1)) + }) + + It("lets the answer through after the configured number of reminders", func() { + llm := &policyLLM{answer: "stubborn answer"} + cfg := baseConfig() + cfg.RequiredToolBeforeFinish = "check_policy" + cfg.RequiredToolBeforeFinishPrompt = prompt + cfg.RequiredToolBeforeFinishAttempts = 2 + + result, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("stubborn answer")) + Expect(llm.nudges(prompt)).To(Equal(2)) + Expect(fx.callCount("check_policy")).To(BeZero()) + }) + + It("uses three reminders and a prompt naming the tool by default", func() { + llm := &policyLLM{answer: "stubborn answer"} + cfg := baseConfig() + cfg.RequiredToolBeforeFinish = "check_policy" + + _, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(llm.nudges(requiredFinishPromptFor("check_policy", ""))).To(Equal(3)) + Expect(requiredFinishPromptFor("check_policy", "")).To(ContainSubstring("check_policy")) + }) + + It("keeps nudging when the required tool fails", func() { + fx.close() + fx = newMCPFixture(map[string]string{"check_policy": `{"ok":false}`}) + llm := &policyLLM{answer: "final answer", respond: func(req openai.ChatCompletionRequest) openai.ChatCompletionMessage { + if last := lastMessage(req); last.Role == "user" && last.Content == prompt { + return toolCallMessage("check_policy") + } + return openai.ChatCompletionMessage{Role: "assistant", Content: "final answer"} + }} + cfg := baseConfig() + cfg.RequiredToolBeforeFinish = "check_policy" + cfg.RequiredToolBeforeFinishPrompt = prompt + cfg.RequiredToolBeforeFinishAttempts = 2 + + _, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(fx.callCount("check_policy")).To(Equal(int32(2))) + Expect(llm.nudges(prompt)).To(Equal(2)) + }) + + It("does nothing when the agent does not have the required tool", func() { + llm := &policyLLM{answer: "final"} + cfg := baseConfig() + cfg.RequiredToolBeforeFinish = "check_policy" + cfg.RequiredToolBeforeFinishPrompt = prompt + cfg.ExcludedTools = ToolNames{"check_policy"} + + result, err := ExecuteChatWithLLM(context.Background(), llm, cfg, "hi", Callbacks{}) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("final")) + Expect(llm.nudges(prompt)).To(BeZero()) + }) + }) + }) +}) + +var _ = DescribeTable("requiredToolResultOK", + func(result string, want bool) { + Expect(requiredToolResultOK(result)).To(Equal(want)) + }, + Entry("top-level ok true", `{"ok":true}`, true), + Entry("top-level ok false", `{"ok":false}`, false), + Entry("ok as a string", `{"ok":"true"}`, false), + Entry("ok nested in another object", `{"data":{"ok":true}}`, false), + Entry("object embedded in text", `result: {"ok": true, "n": 1} done`, true), + Entry("plain text", `"ok": true`, false), +) diff --git a/docs/content/features/agents.md b/docs/content/features/agents.md index cde92862e..4e9026942 100644 --- a/docs/content/features/agents.md +++ b/docs/content/features/agents.md @@ -188,6 +188,8 @@ Each agent has its own configuration that controls its behavior. Key settings in - **Connectors** - external integrations (Slack, Discord, etc.) - **Knowledge Base** - collections of documents for RAG - **MCP Servers** - Model Context Protocol servers for additional tool access +- **Allowed / Excluded Tools** (`allowed_tools`, `excluded_tools`) - limit the tools the agent can see, including MCP tools. The agent always keeps its control actions (`send_message`, `stop`, `update_state`). If a tool is in both lists, it is excluded. +- **Required Tool Before Finish** (`required_tool_before_finish`) - a tool the agent must call successfully before it can give its final answer, for example a validation or policy check. `required_tool_before_finish_prompt` changes the reminder the model gets when it tries to finish early. `required_tool_before_finish_attempts` sets how many reminders it gets before the answer goes through anyway (default 3). The pool-level defaults (API URL, API key, models) can be set via environment variables. Individual agents can further override these in their configuration, allowing them to use different LLM providers (OpenAI, other LocalAI instances, etc.) on a per-agent basis. diff --git a/go.mod b/go.mod index c81fbd82e..ed69ee4ea 100644 --- a/go.mod +++ b/go.mod @@ -83,11 +83,9 @@ require ( ) require ( - cyphar.com/go-pathrs v0.2.1 // indirect filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/edwards25519 v1.1.0 // indirect filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect - github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect @@ -110,19 +108,12 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/chasefleming/elem-go v0.30.0 // indirect github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc // indirect github.com/chromedp/chromedp v0.15.1 // indirect github.com/chromedp/sysutil v1.1.0 // indirect - github.com/containerd/containerd/api v1.8.0 // indirect - github.com/containerd/fifo v1.1.0 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect - github.com/containerd/typeurl/v2 v2.2.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect - github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/dunglas/httpsfv v1.1.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect @@ -149,32 +140,23 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect - github.com/gofiber/template v1.8.3 // indirect - github.com/gofiber/template/html/v2 v2.1.3 // indirect - github.com/gofiber/utils v1.1.0 // indirect github.com/google/certificate-transparency-go v1.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/in-toto/attestation v1.1.2 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jolestar/go-commons-pool/v2 v2.1.2 // indirect github.com/klippa-app/go-pdfium v1.19.2 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-sqlite3 v1.14.28 // indirect - github.com/moby/locker v1.0.1 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/moby/moby/api v1.54.2 // indirect github.com/moby/moby/client v0.4.1 // indirect - github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/signal v0.7.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect - github.com/opencontainers/selinux v1.13.1 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.1 // indirect @@ -182,8 +164,6 @@ require ( github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect github.com/sigstore/sigstore v1.10.0 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20260508151727-1282bb917829 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/sv-tools/openapi v0.2.1 // indirect @@ -195,7 +175,6 @@ require ( github.com/transparency-dev/merkle v0.0.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect - google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) @@ -259,7 +238,7 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mschoch/smat v0.2.0 // indirect - github.com/mudler/LocalAGI v0.0.0-20260912140006-8253de99163e + github.com/mudler/LocalAGI v0.0.0-20260927202351-7e0947d7ebca github.com/mudler/localrecall v0.6.5 // indirect github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145 github.com/olekukonko/tablewriter v0.0.5 // indirect @@ -267,7 +246,7 @@ require ( github.com/philippgille/chromem-go v0.7.0 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect - github.com/rs/zerolog v1.31.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect @@ -287,14 +266,13 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.etcd.io/bbolt v1.4.3 // indirect - go.mau.fi/util v0.3.0 // indirect + go.mau.fi/util v0.9.2 // indirect go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect google.golang.org/appengine v1.6.8 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 // indirect - maunium.net/go/maulogger/v2 v2.4.1 // indirect - maunium.net/go/mautrix v0.17.0 // indirect + maunium.net/go/mautrix v0.25.2 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect ) diff --git a/go.sum b/go.sum index 863118db1..6a01b5bd9 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -67,8 +65,6 @@ fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= @@ -281,8 +277,6 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Y github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok= -github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc h1:wkN/LMi5vc60pBRWx6qpbk/aEvq3/ZVNpnMvsw8PVVU= @@ -304,31 +298,18 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.31 h1:jn3IMuTV4Bb1Uwb0MFPW2ASJAD3W1lh6QqqZHIZwDh4= -github.com/containerd/containerd v1.7.31/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= -github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= -github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= github.com/containerd/containerd v1.7.33 h1:iAkYGC/ifR/V+0eR4iXWHNGYUF0DF2PmGV5iz4Irj5M= github.com/containerd/containerd v1.7.33/go.mod h1:gSbSCVjPCdkfJCjyrzz7aRC+xFlqVbatNpfHfVCYGUM= -github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= -github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= -github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= -github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= -github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -338,7 +319,6 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creachadair/mds v0.21.3 h1:RRgEAPIb52cU0q7UxGyN+13QlCVTZIL4slRr0cYYQfA= github.com/creachadair/mds v0.21.3/go.mod h1:1ltMWZd9yXhaHEoZwBialMaviWVUpRPvMwVP7saFAzM= github.com/creachadair/otp v0.5.0 h1:q3Th7CXm2zlmCdBjw5tEPFOj4oWJMnVL5HXlq0sNKS0= @@ -351,8 +331,6 @@ github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVy github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= -github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0= -github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -384,8 +362,6 @@ github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqI github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= @@ -575,12 +551,6 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk= github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= -github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= -github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o= -github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= -github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= -github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -947,8 +917,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= -github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -993,8 +963,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= @@ -1005,8 +973,6 @@ github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9Kou github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -1028,24 +994,16 @@ github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/mudler/LocalAGI v0.0.0-20260606071251-14aed1ae4336 h1:iKBkSnpisOvMVxFoYsAObvAuOqXBakRPMD0PWxWG5EE= -github.com/mudler/LocalAGI v0.0.0-20260606071251-14aed1ae4336/go.mod h1:U+g6u8mF2wQxhkdBl3dr8G4db1cv3n7KTKmraoJ7D0c= -github.com/mudler/LocalAGI v0.0.0-20260911225740-d93d478e42f1 h1:AqQJhjUIMFvpJ+8ShSpzEp8ClaW5vNqJKq+/9bKTNpc= -github.com/mudler/LocalAGI v0.0.0-20260911225740-d93d478e42f1/go.mod h1:Z97IpFdxmKaigCCpIzfo2Jz6wLwwbnaQrcBTLxyrF+o= -github.com/mudler/LocalAGI v0.0.0-20260912140006-8253de99163e h1:ZaKo7Pp44STT196mJS0OUYSnN2TU62KQmSXKvQ8HS0Q= -github.com/mudler/LocalAGI v0.0.0-20260912140006-8253de99163e/go.mod h1:Wo2UItZdZZd2PkMvhDT19a9MPyiwC+8gnk2nLZniVcY= +github.com/mudler/LocalAGI v0.0.0-20260927202351-7e0947d7ebca h1:bHlzSuOc5cKvHGF21ZjFFUV7IlkS3wt9YkBsWtkcaC4= +github.com/mudler/LocalAGI v0.0.0-20260927202351-7e0947d7ebca/go.mod h1:nk6zt1s5ANgchJYTWGY1jfFPuITSn1gB5oHZ/uFeFDg= github.com/mudler/cogito v0.11.1-0.20260721122412-6eece18a6bb6 h1:eYTR8od5HdaHlh9AKCkxkRoHs2/wmx24BF5qrUh2TRY= github.com/mudler/cogito v0.11.1-0.20260721122412-6eece18a6bb6/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4= github.com/mudler/edgevpn v0.34.0 h1:qDrD/rCPFY/FdURbXudIZWihVKY4VOX3nMn3CcbeQEU= github.com/mudler/edgevpn v0.34.0/go.mod h1:yki7uMi5LR9gSMrw8PdPieuxsrk8BLV2Ui7VBEmbbIA= github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc h1:RxwneJl1VgvikiX28EkpdAyL4yQVnJMrbquKospjHyA= github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc/go.mod h1:O7SwdSWMilAWhBZMK9N9Y/oBDyMMzshE3ju8Xkexwig= -github.com/mudler/go-processmanager v0.1.2-0.20260720195933-3d64f5c974fc h1:NEFmd7+JoImN5dZI81/vcBRjtMg+GfEa7nEjQ029hd0= -github.com/mudler/go-processmanager v0.1.2-0.20260720195933-3d64f5c974fc/go.mod h1:h6kmHUZeafr+k5hRYpGLMzJFH4hItHffgpRo2QIkP+o= github.com/mudler/go-processmanager v0.1.2-0.20260823202314-dfa0ed852db6 h1:/nFm1Ttf8g1BnWtEth986JR34pCh9rzae5A2vKBZosc= github.com/mudler/go-processmanager v0.1.2-0.20260823202314-dfa0ed852db6/go.mod h1:h6kmHUZeafr+k5hRYpGLMzJFH4hItHffgpRo2QIkP+o= -github.com/mudler/localrecall v0.6.3 h1:uXOrP9JmetzxgVKzSrawviyBHZfAcvPBBIrvVUdZjDA= -github.com/mudler/localrecall v0.6.3/go.mod h1:28k5n19raUrkuwXkacdNsBlj8yuSnGhpT16tu+2+4dU= github.com/mudler/localrecall v0.6.5 h1:Q0atTJFFAyumKZG5dbGSrvQ+wsuA88hywIOfHdxpBEU= github.com/mudler/localrecall v0.6.5/go.mod h1:28k5n19raUrkuwXkacdNsBlj8yuSnGhpT16tu+2+4dU= github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8 h1:Ry8RiWy8fZ6Ff4E7dPmjRsBrnHOnPeOOj2LhCgyjQu0= @@ -1128,10 +1086,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= -github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= @@ -1239,13 +1193,12 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -1279,13 +1232,10 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.2.2 h1:4nc55oVv7nygGnfI9bhLCLzUEs4794y0Bkqx4q2zy7Y= github.com/shoenig/go-m1cpu v0.2.2/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= +github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= @@ -1341,7 +1291,6 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= @@ -1491,8 +1440,8 @@ go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs= -go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs= +go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM= +go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1666,8 +1615,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1756,7 +1703,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2050,10 +1996,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= -maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8= -maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8= +maunium.net/go/mautrix v0.25.2 h1:CUG23zp754yGOTMh9Q4mVSENS9FyweE/G+6ZsPDMCUU= +maunium.net/go/mautrix v0.25.2/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=