Merge PR #12302: chore(deps): bump LocalAGI to 7e0947d (no-RAG-DB crash fix, tool filters, per-collection models)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-09-28 01:48:00 +00:00
commit 0565fc06af
8 files changed
+713 -102

No files matched your search

+7
View File
@@ -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.).
+20
View File
@@ -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
+59 -6
View File
@@ -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 {
+220
View File
@@ -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) != ""
}
+387
View File
@@ -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),
)
+2
View File
@@ -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.
+6 -28
View File
@@ -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
)
+12 -68
View File
@@ -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=