diff --git a/CLAUDE.md b/CLAUDE.md index acfc1b69..4bc032b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,7 @@ cd ui/web && pnpm install && pnpm dev # Web dashboard (dev) After implementing or modifying Go code, run these checks: ```bash +go fix ./... # Apply Go version upgrades (run before commit) go build ./... # Compile check go vet ./... # Static analysis go test -race ./tests/integration/ # Integration tests with race detector diff --git a/Dockerfile b/Dockerfile index 466747e8..151503c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # ── Stage 1: Build ── -FROM golang:1.25-bookworm AS builder +FROM golang:1.26-bookworm AS builder WORKDIR /src diff --git a/cmd/agent_chat_client.go b/cmd/agent_chat_client.go index fe203ac6..a5f8e51b 100644 --- a/cmd/agent_chat_client.go +++ b/cmd/agent_chat_client.go @@ -115,7 +115,7 @@ func wsConnect(conn *websocket.Conn, token string) error { // displaying events (tool calls, chunks) in real-time. func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (string, error) { reqID := uuid.NewString()[:8] - params, _ := json.Marshal(map[string]interface{}{ + params, _ := json.Marshal(map[string]any{ "message": message, "agentId": agentID, "sessionKey": sessionKey, @@ -159,7 +159,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri return "", fmt.Errorf("agent error (unknown)") } // Extract content from payload - if payload, ok := resp.Payload.(map[string]interface{}); ok { + if payload, ok := resp.Payload.(map[string]any); ok { if content, ok := payload["content"].(string); ok && content != "" { finalContent = content } @@ -178,7 +178,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri // handleCLIEvent displays agent events in the terminal. func handleCLIEvent(evt protocol.EventFrame) { - payload, ok := evt.Payload.(map[string]interface{}) + payload, ok := evt.Payload.(map[string]any) if !ok { return } @@ -189,7 +189,7 @@ func handleCLIEvent(evt protocol.EventFrame) { case protocol.EventAgent: switch evtType { case protocol.AgentEventToolCall: - if p, ok := payload["payload"].(map[string]interface{}); ok { + if p, ok := payload["payload"].(map[string]any); ok { name, _ := p["toolName"].(string) if name == "" { name, _ = p["name"].(string) @@ -197,7 +197,7 @@ func handleCLIEvent(evt protocol.EventFrame) { fmt.Fprintf(os.Stderr, " [tool] %s\n", name) } case protocol.AgentEventToolResult: - if p, ok := payload["payload"].(map[string]interface{}); ok { + if p, ok := payload["payload"].(map[string]any); ok { isErr, _ := p["is_error"].(bool) name, _ := p["toolName"].(string) if name == "" { diff --git a/cmd/auth.go b/cmd/auth.go index 8ce074e9..463767d3 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -39,7 +39,7 @@ func gatewayURL() string { } // gatewayRequest sends an authenticated request to the running gateway. -func gatewayRequest(method, path string) (map[string]interface{}, error) { +func gatewayRequest(method, path string) (map[string]any, error) { url := gatewayURL() + path req, err := http.NewRequest(method, url, nil) if err != nil { @@ -57,7 +57,7 @@ func gatewayRequest(method, path string) (map[string]interface{}, error) { defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - var result map[string]interface{} + var result map[string]any if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("invalid response from gateway: %s", string(body)) } diff --git a/cmd/config_cmd.go b/cmd/config_cmd.go index 73002fd7..07ce6d14 100644 --- a/cmd/config_cmd.go +++ b/cmd/config_cmd.go @@ -68,15 +68,15 @@ func configValidateCmd() *cobra.Command { } // redactConfig returns a JSON-safe copy with secrets masked. -func redactConfig(cfg *config.Config) interface{} { +func redactConfig(cfg *config.Config) any { data, _ := json.Marshal(cfg) - var raw map[string]interface{} + var raw map[string]any json.Unmarshal(data, &raw) redactMap(raw) return raw } -func redactMap(m map[string]interface{}) { +func redactMap(m map[string]any) { secretKeys := map[string]bool{ "apiKey": true, "api_key": true, "token": true, "botToken": true, "bot_token": true, "secret": true, @@ -89,7 +89,7 @@ func redactMap(m map[string]interface{}) { } else if s, ok := v.(string); ok && s != "" { m[k] = "****" } - } else if sub, ok := v.(map[string]interface{}); ok { + } else if sub, ok := v.(map[string]any); ok { redactMap(sub) } } diff --git a/cmd/cron_cmd.go b/cmd/cron_cmd.go index 6c85ad4d..b7edd573 100644 --- a/cmd/cron_cmd.go +++ b/cmd/cron_cmd.go @@ -67,7 +67,7 @@ func cronToggleCmd() *cobra.Command { func cronListRPC(showDisabled, jsonOutput bool) { requireGateway() - params, _ := json.Marshal(map[string]interface{}{"includeDisabled": showDisabled}) + params, _ := json.Marshal(map[string]any{"includeDisabled": showDisabled}) resp, err := gatewayRPC(protocol.MethodCronList, params) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -109,7 +109,7 @@ func cronDeleteRPC(jobID string) { func cronToggleRPC(jobID string, enabled bool) { requireGateway() - params, _ := json.Marshal(map[string]interface{}{"jobId": jobID, "enabled": enabled}) + params, _ := json.Marshal(map[string]any{"jobId": jobID, "enabled": enabled}) resp, err := gatewayRPC(protocol.MethodCronToggle, params) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/cmd/gateway.go b/cmd/gateway.go index 75d255e0..91745067 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -771,7 +771,7 @@ func runGateway() { // Route activity events to Router (status registry) and DelegateManager (progress tracking). if agentEvent.Type == protocol.AgentEventActivity { - payloadMap, _ := agentEvent.Payload.(map[string]interface{}) + payloadMap, _ := agentEvent.Payload.(map[string]any) phase, _ := payloadMap["phase"].(string) tool, _ := payloadMap["tool"].(string) iteration := 0 diff --git a/cmd/pairing.go b/cmd/pairing.go index 00976371..aed8b9a0 100644 --- a/cmd/pairing.go +++ b/cmd/pairing.go @@ -205,7 +205,7 @@ func gatewayRPC(method string, params json.RawMessage) (*protocol.ResponseFrame, defer conn.Close() // Step 1: Send connect handshake - connectParams, _ := json.Marshal(map[string]interface{}{ + connectParams, _ := json.Marshal(map[string]any{ "token": cfg.Gateway.Token, "protocol": protocol.ProtocolVersion, }) diff --git a/go.mod b/go.mod index 38c5e7b9..f96b1804 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nextlevelbuilder/goclaw -go 1.25.5 +go 1.26.0 require ( github.com/adhocore/gronx v1.19.6 @@ -16,6 +16,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 github.com/mymmrac/telego v1.6.0 github.com/redis/go-redis/v9 v9.18.0 + github.com/slack-go/slack v0.19.0 github.com/spf13/cobra v1.10.2 github.com/titanous/json5 v1.0.0 go.opentelemetry.io/otel v1.40.0 @@ -86,7 +87,6 @@ require ( github.com/pires/go-proxyproto v0.8.1 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/slack-go/slack v0.19.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect diff --git a/go.sum b/go.sum index 2f9634ae..31adfd7f 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= diff --git a/internal/agent/intent_classify.go b/internal/agent/intent_classify.go index b2920ed7..b479e361 100644 --- a/internal/agent/intent_classify.go +++ b/internal/agent/intent_classify.go @@ -81,7 +81,7 @@ func ClassifyIntent(ctx context.Context, provider providers.Provider, model, use {Role: "user", Content: userMessage}, }, Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ providers.OptMaxTokens: 20, providers.OptTemperature: 0.0, }, diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 5e71c9c5..f7611fa0 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -334,7 +334,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) totalToolCalls := 0 var finalContent string var finalThinking string - var asyncToolCalls []string // track async spawn tool names for fallback + var asyncToolCalls []string // track async spawn tool names for fallback var mediaResults []MediaResult // media files from tool MEDIA: results var deliverables []string // actual content from tool outputs (for team task results) var blockReplies int // count of block.reply events emitted (for dedup in consumer) @@ -347,7 +347,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) // Team task orphan detection: track team_tasks create vs spawn calls. // If the LLM creates tasks but forgets to spawn, inject a reminder. var teamTaskCreates int // count of team_tasks action=create calls - var teamTaskSpawns int // count of spawn calls with team_task_id + var teamTaskSpawns int // count of spawn calls with team_task_id var teamTaskRetried bool // only retry once to prevent infinite loops // Inject retry hook so channels can update placeholder on LLM retries. @@ -379,7 +379,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Type: protocol.AgentEventActivity, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"phase": "thinking", "iteration": iteration}, + Payload: map[string]any{"phase": "thinking", "iteration": iteration}, }) // Build provider request with policy-filtered tools @@ -399,7 +399,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Messages: messages, Tools: toolDefs, Model: l.model, - Options: map[string]interface{}{ + Options: map[string]any{ providers.OptMaxTokens: 8192, providers.OptTemperature: 0.7, providers.OptSessionKey: req.SessionKey, @@ -505,7 +505,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Type: protocol.AgentEventActivity, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"phase": "compacting", "iteration": iteration}, + Payload: map[string]any{"phase": "compacting", "iteration": iteration}, }) if compacted := l.compactMessagesInPlace(ctx, messages); compacted != nil { messages = compacted @@ -562,7 +562,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Content: resp.Content, Thinking: resp.Thinking, // reasoning_content passback for thinking models (Kimi, DeepSeek) ToolCalls: resp.ToolCalls, - Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex) + Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex) RawAssistantContent: resp.RawAssistantContent, // preserve thinking blocks for Anthropic passback } messages = append(messages, assistantMsg) @@ -617,7 +617,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Type: protocol.AgentEventActivity, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{ + Payload: map[string]any{ "phase": "tool_exec", "tool": toolNames[0], "tools": toolNames, @@ -634,7 +634,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Type: protocol.AgentEventToolCall, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)}, + Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)}, }) argsJSON, _ := json.Marshal(tc.Arguments) @@ -675,7 +675,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } } - toolResultPayload := map[string]interface{}{ + toolResultPayload := map[string]any{ "name": tc.Name, "id": tc.ID, "is_error": result.IsError, @@ -748,7 +748,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) Type: protocol.AgentEventToolCall, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)}, + Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)}, }) } @@ -816,7 +816,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } } - parToolResultPayload := map[string]interface{}{ + parToolResultPayload := map[string]any{ "name": r.tc.Name, "id": r.tc.ID, "is_error": r.result.IsError, @@ -981,8 +981,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } // truncateToolArgs returns a copy of arguments with string values truncated to maxLen. -func truncateToolArgs(args map[string]interface{}, maxLen int) map[string]interface{} { - out := make(map[string]interface{}, len(args)) +func truncateToolArgs(args map[string]any, maxLen int) map[string]any { + out := make(map[string]any, len(args)) for k, v := range args { if s, ok := v.(string); ok && len(s) > maxLen { out[k] = truncateStr(s, maxLen) diff --git a/internal/agent/loop_compact.go b/internal/agent/loop_compact.go index dced5040..108a00d3 100644 --- a/internal/agent/loop_compact.go +++ b/internal/agent/loop_compact.go @@ -66,7 +66,7 @@ func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers. Content: "Provide a concise summary of this conversation, preserving key findings, data, and context:\n\n" + sb.String(), }}, Model: l.model, - Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3}, + Options: map[string]any{"max_tokens": 1024, "temperature": 0.3}, }) if err != nil { slog.Warn("mid_loop_compaction_failed", "agent", l.id, "error", err) diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 0950970e..c68b20a6 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -120,7 +120,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s HasSpawn: l.tools != nil && hasSpawn, HasSkillSearch: hasSkillSearch, HasMCPToolSearch: hasMCPToolSearch, - HasKnowledgeGraph: hasKG, + HasKnowledgeGraph: hasKG, MCPToolDescs: mcpToolDescs, ContextFiles: contextFiles, AgentType: l.agentType, @@ -397,46 +397,47 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) { summary := l.sessions.GetSummary(sessionKey) toSummarize := history[:len(history)-keepLast] - var sb string + var sb strings.Builder var mediaKinds []string for _, m := range toSummarize { if m.Role == "user" { - sb += fmt.Sprintf("user: %s\n", m.Content) + sb.WriteString(fmt.Sprintf("user: %s\n", m.Content)) } else if m.Role == "assistant" { - sb += fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content)) + sb.WriteString(fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content))) } for _, ref := range m.MediaRefs { mediaKinds = append(mediaKinds, ref.Kind) } } - prompt := "Provide a concise summary of this conversation, preserving key context:\n" + var prompt strings.Builder + prompt.WriteString("Provide a concise summary of this conversation, preserving key context:\n") if len(mediaKinds) > 0 { // Deduplicate and count media types for a compact note. counts := make(map[string]int) for _, k := range mediaKinds { counts[k]++ } - prompt += "\nNote: user shared media files (" + prompt.WriteString("\nNote: user shared media files (") first := true for k, n := range counts { if !first { - prompt += ", " + prompt.WriteString(", ") } - prompt += fmt.Sprintf("%d %s(s)", n, k) + prompt.WriteString(fmt.Sprintf("%d %s(s)", n, k)) first = false } - prompt += ") which are no longer in context. Mention briefly if relevant.\n" + prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n") } if summary != "" { - prompt += "Existing context: " + summary + "\n" + prompt.WriteString("Existing context: " + summary + "\n") } - prompt += "\n" + sb + prompt.WriteString("\n" + sb.String()) resp, err := l.provider.Chat(sctx, providers.ChatRequest{ - Messages: []providers.Message{{Role: "user", Content: prompt}}, + Messages: []providers.Message{{Role: "user", Content: prompt.String()}}, Model: l.model, - Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3}, + Options: map[string]any{"max_tokens": 1024, "temperature": 0.3}, }) if err != nil { slog.Warn("summarization failed", "session", sessionKey, "error", err) diff --git a/internal/agent/loop_run.go b/internal/agent/loop_run.go index f6e49591..daa5c7f0 100644 --- a/internal/agent/loop_run.go +++ b/internal/agent/loop_run.go @@ -36,7 +36,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) { Type: protocol.AgentEventRunStarted, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"message": req.Message}, + Payload: map[string]any{"message": req.Message}, }) // Create trace @@ -132,7 +132,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) { Type: protocol.AgentEventRunCompleted, AgentID: l.id, RunID: req.RunID, - Payload: map[string]interface{}{"content": result.Content}, + Payload: map[string]any{"content": result.Content}, }) if !isChildTrace && l.traceCollector != nil && traceID != uuid.Nil { l.traceCollector.FinishTrace(ctx, traceID, store.TraceStatusCompleted, "", truncateStr(result.Content, 500)) diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index b1dfc49c..e46faf2a 100644 --- a/internal/agent/loop_types.go +++ b/internal/agent/loop_types.go @@ -47,12 +47,12 @@ type Loop struct { maxToolCalls int workspace string - eventPub bus.EventPublisher // currently unused by Loop; kept for future use - sessions store.SessionStore + eventPub bus.EventPublisher // currently unused by Loop; kept for future use + sessions store.SessionStore tools *tools.Registry toolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM agentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions) - activeRuns atomic.Int32 // number of currently executing runs + activeRuns atomic.Int32 // number of currently executing runs // Per-session summarization lock: prevents concurrent summarize goroutines for the same session. summarizeMu sync.Map // sessionKey → *sync.Mutex @@ -65,10 +65,10 @@ type Loop struct { contextFiles []bootstrap.ContextFile // Per-user file seeding + dynamic context loading - ensureUserFiles EnsureUserFilesFunc - contextFileLoader ContextFileLoaderFunc - bootstrapCleanup BootstrapCleanupFunc - userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles) + ensureUserFiles EnsureUserFilesFunc + contextFileLoader ContextFileLoaderFunc + bootstrapCleanup BootstrapCleanupFunc + userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles) // Compaction config (memory flush settings) compactionCfg *config.CompactionConfig @@ -77,8 +77,8 @@ type Loop struct { contextPruningCfg *config.ContextPruningConfig // Sandbox info - sandboxEnabled bool - sandboxContainerDir string + sandboxEnabled bool + sandboxContainerDir string sandboxWorkspaceAccess string // Event callback for broadcasting agent events (run.started, chunk, tool.call, etc.) @@ -113,11 +113,11 @@ type Loop struct { // AgentEvent is emitted during agent execution for WS broadcasting. type AgentEvent struct { - Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result" - AgentID string `json:"agentId"` - RunID string `json:"runId"` - RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs - Payload interface{} `json:"payload,omitempty"` + Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result" + AgentID string `json:"agentId"` + RunID string `json:"runId"` + RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs + Payload any `json:"payload,omitempty"` // Delegation context (omitempty — only present when agent runs inside a delegation) DelegationID string `json:"delegationId,omitempty"` @@ -133,15 +133,15 @@ type AgentEvent struct { // LoopConfig configures a new Loop. type LoopConfig struct { - ID string - Provider providers.Provider - Model string - ContextWindow int - MaxIterations int - MaxToolCalls int - Workspace string - Bus bus.EventPublisher - Sessions store.SessionStore + ID string + Provider providers.Provider + Model string + ContextWindow int + MaxIterations int + MaxToolCalls int + Workspace string + Bus bus.EventPublisher + Sessions store.SessionStore Tools *tools.Registry ToolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM AgentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions) @@ -161,8 +161,8 @@ type LoopConfig struct { ContextPruningCfg *config.ContextPruningConfig // Sandbox info (injected into system prompt) - SandboxEnabled bool - SandboxContainerDir string // e.g. "/workspace" + SandboxEnabled bool + SandboxContainerDir string // e.g. "/workspace" SandboxWorkspaceAccess string // "none", "ro", "rw" // Agent UUID for context propagation to tools @@ -178,9 +178,9 @@ type LoopConfig struct { TraceCollector *tracing.Collector // Security: input guard for injection detection, max message size - InputGuard *InputGuard // nil = auto-create when InjectionAction != "off" - InjectionAction string // "log", "warn" (default), "block", "off" - MaxMessageChars int // 0 = use default (32000) + InputGuard *InputGuard // nil = auto-create when InjectionAction != "off" + InjectionAction string // "log", "warn" (default), "block", "off" + MaxMessageChars int // 0 = use default (32000) // Global builtin tool settings (from builtin_tools table) BuiltinToolSettings tools.BuiltinToolSettings @@ -225,71 +225,71 @@ func NewLoop(cfg LoopConfig) *Loop { } return &Loop{ - id: cfg.ID, - agentUUID: cfg.AgentUUID, - agentType: cfg.AgentType, - provider: cfg.Provider, - model: cfg.Model, - contextWindow: cfg.ContextWindow, - maxIterations: cfg.MaxIterations, - maxToolCalls: cfg.MaxToolCalls, - workspace: cfg.Workspace, - eventPub: cfg.Bus, - sessions: cfg.Sessions, - tools: cfg.Tools, - toolPolicy: cfg.ToolPolicy, - agentToolPolicy: cfg.AgentToolPolicy, - onEvent: cfg.OnEvent, - ownerIDs: cfg.OwnerIDs, - skillsLoader: cfg.SkillsLoader, - skillAllowList: cfg.SkillAllowList, - hasMemory: cfg.HasMemory, - contextFiles: cfg.ContextFiles, - ensureUserFiles: cfg.EnsureUserFiles, - contextFileLoader: cfg.ContextFileLoader, - bootstrapCleanup: cfg.BootstrapCleanup, - compactionCfg: cfg.CompactionCfg, - contextPruningCfg: cfg.ContextPruningCfg, - sandboxEnabled: cfg.SandboxEnabled, - sandboxContainerDir: cfg.SandboxContainerDir, + id: cfg.ID, + agentUUID: cfg.AgentUUID, + agentType: cfg.AgentType, + provider: cfg.Provider, + model: cfg.Model, + contextWindow: cfg.ContextWindow, + maxIterations: cfg.MaxIterations, + maxToolCalls: cfg.MaxToolCalls, + workspace: cfg.Workspace, + eventPub: cfg.Bus, + sessions: cfg.Sessions, + tools: cfg.Tools, + toolPolicy: cfg.ToolPolicy, + agentToolPolicy: cfg.AgentToolPolicy, + onEvent: cfg.OnEvent, + ownerIDs: cfg.OwnerIDs, + skillsLoader: cfg.SkillsLoader, + skillAllowList: cfg.SkillAllowList, + hasMemory: cfg.HasMemory, + contextFiles: cfg.ContextFiles, + ensureUserFiles: cfg.EnsureUserFiles, + contextFileLoader: cfg.ContextFileLoader, + bootstrapCleanup: cfg.BootstrapCleanup, + compactionCfg: cfg.CompactionCfg, + contextPruningCfg: cfg.ContextPruningCfg, + sandboxEnabled: cfg.SandboxEnabled, + sandboxContainerDir: cfg.SandboxContainerDir, sandboxWorkspaceAccess: cfg.SandboxWorkspaceAccess, - traceCollector: cfg.TraceCollector, - inputGuard: guard, - injectionAction: action, - maxMessageChars: cfg.MaxMessageChars, - builtinToolSettings: cfg.BuiltinToolSettings, - thinkingLevel: cfg.ThinkingLevel, - selfEvolve: cfg.SelfEvolve, - groupWriterCache: cfg.GroupWriterCache, - teamStore: cfg.TeamStore, - mediaStore: cfg.MediaStore, + traceCollector: cfg.TraceCollector, + inputGuard: guard, + injectionAction: action, + maxMessageChars: cfg.MaxMessageChars, + builtinToolSettings: cfg.BuiltinToolSettings, + thinkingLevel: cfg.ThinkingLevel, + selfEvolve: cfg.SelfEvolve, + groupWriterCache: cfg.GroupWriterCache, + teamStore: cfg.TeamStore, + mediaStore: cfg.MediaStore, } } // RunRequest is the input for processing a message through the agent. type RunRequest struct { - SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId} - Message string // user message - Media []bus.MediaFile // local media files with MIME types - ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results) - Channel string // source channel instance name (e.g. "my-telegram-bot") - ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context - ChatID string // source chat ID - PeerKind string // "direct" or "group" (for session key building and tool context) - RunID string // unique run identifier - UserID string // external user ID (TEXT, free-form) for multi-tenant scoping - SenderID string // original individual sender ID (preserved in group chats for permission checks) - Stream bool // whether to stream response chunks - ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.) - SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist - HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config) - ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx") - LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42") - ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs) - ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span - TraceName string // override trace name (default: "chat ") - TraceTags []string // additional tags for the trace (e.g. "cron") - MaxIterations int // per-request override (0 = use agent default, must be lower) + SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId} + Message string // user message + Media []bus.MediaFile // local media files with MIME types + ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results) + Channel string // source channel instance name (e.g. "my-telegram-bot") + ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context + ChatID string // source chat ID + PeerKind string // "direct" or "group" (for session key building and tool context) + RunID string // unique run identifier + UserID string // external user ID (TEXT, free-form) for multi-tenant scoping + SenderID string // original individual sender ID (preserved in group chats for permission checks) + Stream bool // whether to stream response chunks + ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.) + SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist + HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config) + ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx") + LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42") + ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs) + ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span + TraceName string // override trace name (default: "chat ") + TraceTags []string // additional tags for the trace (e.g. "cron") + MaxIterations int // per-request override (0 = use agent default, must be lower) // Run classification RunKind string // "delegation", "announce" — empty for user-initiated runs @@ -305,19 +305,19 @@ type RunRequest struct { // RunResult is the output of a completed agent run. type RunResult struct { - Content string `json:"content"` - RunID string `json:"runId"` - Iterations int `json:"iterations"` - Usage *providers.Usage `json:"usage,omitempty"` - Media []MediaResult `json:"media,omitempty"` // media files from tool results (MEDIA: prefix) - Deliverables []string `json:"deliverables,omitempty"` // actual content from tool outputs (for team task results) - BlockReplies int `json:"blockReplies,omitempty"` // number of block.reply events emitted - LastBlockReply string `json:"lastBlockReply,omitempty"` // last block reply content (for dedup) + Content string `json:"content"` + RunID string `json:"runId"` + Iterations int `json:"iterations"` + Usage *providers.Usage `json:"usage,omitempty"` + Media []MediaResult `json:"media,omitempty"` // media files from tool results (MEDIA: prefix) + Deliverables []string `json:"deliverables,omitempty"` // actual content from tool outputs (for team task results) + BlockReplies int `json:"blockReplies,omitempty"` // number of block.reply events emitted + LastBlockReply string `json:"lastBlockReply,omitempty"` // last block reply content (for dedup) } // MediaResult represents a media file produced by a tool during the agent run. type MediaResult struct { - Path string `json:"path"` // local file path + Path string `json:"path"` // local file path ContentType string `json:"content_type,omitempty"` // MIME type AsVoice bool `json:"as_voice,omitempty"` // send as voice message (Telegram OGG) } diff --git a/internal/agent/memoryflush.go b/internal/agent/memoryflush.go index b8dbc249..00c38456 100644 --- a/internal/agent/memoryflush.go +++ b/internal/agent/memoryflush.go @@ -173,12 +173,12 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings * // Run LLM iteration loop (max 5 iterations for flush) maxFlushIter := 5 - for i := 0; i < maxFlushIter; i++ { + for range maxFlushIter { resp, err := l.provider.Chat(flushCtx, providers.ChatRequest{ Messages: messages, Tools: toolDefs, Model: l.model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 4096, "temperature": 0.3, }, diff --git a/internal/agent/resolver_helpers.go b/internal/agent/resolver_helpers.go index 055ca257..b32fed6e 100644 --- a/internal/agent/resolver_helpers.go +++ b/internal/agent/resolver_helpers.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "slices" "strings" "github.com/google/uuid" @@ -172,10 +173,8 @@ func agentToolPolicyForTeam(policy *config.ToolPolicySpec, isLead bool) *config. if policy == nil { policy = &config.ToolPolicySpec{} } - for _, d := range policy.Deny { - if d == "team_message" { - return policy - } + if slices.Contains(policy.Deny, "team_message") { + return policy } policy.Deny = append(policy.Deny, "team_message") return policy @@ -191,10 +190,8 @@ func agentToolPolicyWithMCP(policy *config.ToolPolicySpec, hasMCP bool) *config. policy = &config.ToolPolicySpec{} } // Check if group:mcp is already present - for _, a := range policy.AlsoAllow { - if a == "group:mcp" { - return policy - } + if slices.Contains(policy.AlsoAllow, "group:mcp") { + return policy } policy.AlsoAllow = append(policy.AlsoAllow, "group:mcp") return policy diff --git a/internal/agent/router.go b/internal/agent/router.go index e812b2e0..118d7f4a 100644 --- a/internal/agent/router.go +++ b/internal/agent/router.go @@ -22,8 +22,8 @@ type agentEntry struct { // AgentActivityStatus tracks the current phase of a running agent for status queries. type AgentActivityStatus struct { RunID string - Phase string // "thinking", "tool_exec", "compacting" - Tool string // current tool name (when Phase == "tool_exec") + Phase string // "thinking", "tool_exec", "compacting" + Tool string // current tool name (when Phase == "tool_exec") Iteration int StartedAt time.Time } @@ -34,9 +34,9 @@ type AgentActivityStatus struct { type Router struct { agents map[string]*agentEntry mu sync.RWMutex - activeRuns sync.Map // runID → *ActiveRun - sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy) - agentActivity sync.Map // sessionKey → *AgentActivityStatus + activeRuns sync.Map // runID → *ActiveRun + sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy) + agentActivity sync.Map // sessionKey → *AgentActivityStatus resolver ResolverFunc // optional: lazy creation from DB ttl time.Duration } @@ -262,7 +262,7 @@ func (r *Router) IsSessionBusy(sessionKey string) bool { // Returns the list of aborted run IDs. func (r *Router) AbortRunsForSession(sessionKey string) []string { var aborted []string - r.activeRuns.Range(func(key, val interface{}) bool { + r.activeRuns.Range(func(key, val any) bool { run := val.(*ActiveRun) if run.SessionKey == sessionKey { run.Cancel() diff --git a/internal/agent/toolloop.go b/internal/agent/toolloop.go index 76693ef4..2c2cc76c 100644 --- a/internal/agent/toolloop.go +++ b/internal/agent/toolloop.go @@ -28,7 +28,7 @@ type toolCallRecord struct { } // record adds a tool call to history and returns its argsHash. -func (s *toolLoopState) record(toolName string, args map[string]interface{}) string { +func (s *toolLoopState) record(toolName string, args map[string]any) string { h := hashToolCall(toolName, args) s.history = append(s.history, toolCallRecord{ toolName: toolName, @@ -98,7 +98,7 @@ func (s *toolLoopState) detect(toolName string, argsHash string) (level, message } // hashToolCall produces a deterministic hash of tool name + arguments. -func hashToolCall(toolName string, args map[string]interface{}) string { +func hashToolCall(toolName string, args map[string]any) string { s := toolName + ":" + stableJSON(args) h := sha256.Sum256([]byte(s)) return fmt.Sprintf("%x", h[:16]) // 32 hex chars, enough for dedup @@ -111,9 +111,9 @@ func hashResult(content string) string { } // stableJSON serializes a value with sorted keys for deterministic hashing. -func stableJSON(v interface{}) string { +func stableJSON(v any) string { switch val := v.(type) { - case map[string]interface{}: + case map[string]any: keys := make([]string, 0, len(val)) for k := range val { keys = append(keys, k) @@ -124,7 +124,7 @@ func stableJSON(v interface{}) string { parts[i] = fmt.Sprintf("%q:%s", k, stableJSON(val[k])) } return "{" + strings.Join(parts, ",") + "}" - case []interface{}: + case []any: parts := make([]string, len(val)) for i, elem := range val { parts[i] = stableJSON(elem) diff --git a/internal/agent/toolloop_test.go b/internal/agent/toolloop_test.go index 525f6269..6aeeac59 100644 --- a/internal/agent/toolloop_test.go +++ b/internal/agent/toolloop_test.go @@ -6,8 +6,8 @@ func TestToolLoopDetection_NoLoop(t *testing.T) { var s toolLoopState // 2 identical calls with same result → below threshold, no detection - for i := 0; i < 2; i++ { - h := s.record("list_files", map[string]interface{}{"path": "."}) + for i := range 2 { + h := s.record("list_files", map[string]any{"path": "."}) s.recordResult(h, "access denied") level, _ := s.detect("list_files", h) if level != "" { @@ -20,8 +20,8 @@ func TestToolLoopDetection_Warning(t *testing.T) { var s toolLoopState var lastLevel string - for i := 0; i < toolLoopWarningThreshold; i++ { - h := s.record("list_files", map[string]interface{}{"path": "."}) + for range toolLoopWarningThreshold { + h := s.record("list_files", map[string]any{"path": "."}) s.recordResult(h, "access denied") lastLevel, _ = s.detect("list_files", h) } @@ -34,8 +34,8 @@ func TestToolLoopDetection_Critical(t *testing.T) { var s toolLoopState var lastLevel string - for i := 0; i < toolLoopCriticalThreshold; i++ { - h := s.record("list_files", map[string]interface{}{"path": "."}) + for range toolLoopCriticalThreshold { + h := s.record("list_files", map[string]any{"path": "."}) s.recordResult(h, "access denied") lastLevel, _ = s.detect("list_files", h) } @@ -48,8 +48,8 @@ func TestToolLoopDetection_DifferentArgs(t *testing.T) { var s toolLoopState // Same tool but different args each time → no detection - for i := 0; i < 15; i++ { - args := map[string]interface{}{"path": string(rune('a' + i))} + for i := range 15 { + args := map[string]any{"path": string(rune('a' + i))} h := s.record("list_files", args) s.recordResult(h, "access denied") level, _ := s.detect("list_files", h) @@ -63,8 +63,8 @@ func TestToolLoopDetection_DifferentResults(t *testing.T) { var s toolLoopState // Same args but different results each time → progress, no detection - for i := 0; i < 15; i++ { - h := s.record("web_fetch", map[string]interface{}{"url": "https://example.com"}) + for i := range 15 { + h := s.record("web_fetch", map[string]any{"url": "https://example.com"}) s.recordResult(h, "result content "+string(rune('a'+i))) level, _ := s.detect("web_fetch", h) if level != "" { @@ -78,12 +78,12 @@ func TestToolLoopDetection_MixedTools(t *testing.T) { // Alternate between two tools with same result → each tool only hit ~half // With 8 iterations, each tool is called 4 times → below critical (5) - for i := 0; i < 8; i++ { + for i := range 8 { toolName := "list_files" if i%2 == 1 { toolName = "read_file" } - h := s.record(toolName, map[string]interface{}{"path": "."}) + h := s.record(toolName, map[string]any{"path": "."}) s.recordResult(h, "error") level, _ := s.detect(toolName, h) // Each tool is only called 4 times, should at most warn @@ -95,8 +95,8 @@ func TestToolLoopDetection_MixedTools(t *testing.T) { func TestStableJSON(t *testing.T) { // Same keys in different order → same hash - a := stableJSON(map[string]interface{}{"b": 2, "a": 1}) - b := stableJSON(map[string]interface{}{"a": 1, "b": 2}) + a := stableJSON(map[string]any{"b": 2, "a": 1}) + b := stableJSON(map[string]any{"a": 1, "b": 2}) if a != b { t.Fatalf("stableJSON not deterministic: %q != %q", a, b) } @@ -104,14 +104,14 @@ func TestStableJSON(t *testing.T) { func TestHashToolCall(t *testing.T) { // Same input → same hash - h1 := hashToolCall("list_files", map[string]interface{}{"path": "."}) - h2 := hashToolCall("list_files", map[string]interface{}{"path": "."}) + h1 := hashToolCall("list_files", map[string]any{"path": "."}) + h2 := hashToolCall("list_files", map[string]any{"path": "."}) if h1 != h2 { t.Fatal("hashToolCall not deterministic") } // Different tool → different hash - h3 := hashToolCall("read_file", map[string]interface{}{"path": "."}) + h3 := hashToolCall("read_file", map[string]any{"path": "."}) if h1 == h3 { t.Fatal("different tools should have different hashes") } diff --git a/internal/bootstrap/seed_store_test.go b/internal/bootstrap/seed_store_test.go index ab1a9055..0bb2af5c 100644 --- a/internal/bootstrap/seed_store_test.go +++ b/internal/bootstrap/seed_store_test.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "maps" "testing" "github.com/google/uuid" @@ -253,9 +254,7 @@ func TestSeedUserFiles_IdempotentOnSecondCall(t *testing.T) { SeedUserFiles(context.Background(), as, agentID, "user-frank", store.AgentTypePredefined) // Simulate what the first call wrote (move seededUserFiles → userFiles) - for k, v := range as.seededUserFiles { - as.userFiles[k] = v - } + maps.Copy(as.userFiles, as.seededUserFiles) as.seededUserFiles = make(map[string]string) // Second call — must seed nothing (all files already exist) diff --git a/internal/bus/types.go b/internal/bus/types.go index f7207b18..54a8a2e3 100644 --- a/internal/bus/types.go +++ b/internal/bus/types.go @@ -43,8 +43,8 @@ type MediaAttachment struct { // Event represents a server-side event to broadcast to WebSocket clients. type Event struct { - Name string `json:"name"` // event name (e.g. "agent", "chat", "health") - Payload interface{} `json:"payload,omitempty"` + Name string `json:"name"` // event name (e.g. "agent", "chat", "health") + Payload any `json:"payload,omitempty"` } // Cache invalidation kind constants. diff --git a/internal/channels/events.go b/internal/channels/events.go index 320866a1..6148e4c8 100644 --- a/internal/channels/events.go +++ b/internal/channels/events.go @@ -13,7 +13,7 @@ import ( // HandleAgentEvent routes agent lifecycle events to streaming/reaction channels. // Called from the bus event subscriber — must be non-blocking. // eventType: "run.started", "chunk", "tool.call", "tool.result", "run.completed", "run.failed" -func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{}) { +func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) { val, ok := m.runs.Load(runID) if !ok { return @@ -187,11 +187,11 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{}) } // extractPayloadString extracts a string field from a payload (map[string]string or map[string]interface{}). -func extractPayloadString(payload interface{}, key string) string { +func extractPayloadString(payload any, key string) string { switch p := payload.(type) { case map[string]string: return p[key] - case map[string]interface{}: + case map[string]any: if v, ok := p[key].(string); ok { return v } @@ -214,28 +214,28 @@ func copyRoutingMeta(src map[string]string) map[string]string { // toolStatusMap maps builtin tool names to user-friendly status messages. var toolStatusMap = map[string]string{ // Filesystem - "read_file": "📝 Reading file...", - "write_file": "📝 Writing file...", - "list_files": "📝 Listing files...", - "edit": "📝 Editing file...", + "read_file": "📝 Reading file...", + "write_file": "📝 Writing file...", + "list_files": "📝 Listing files...", + "edit": "📝 Editing file...", // Runtime "exec": "⚡ Running code...", // Web "web_search": "🔍 Searching the web...", "web_fetch": "🔍 Fetching web content...", // Memory - "memory_search": "🧠 Searching memory...", - "memory_get": "🧠 Retrieving memory...", + "memory_search": "🧠 Searching memory...", + "memory_get": "🧠 Retrieving memory...", "knowledge_graph_search": "🧠 Querying knowledge graph...", // Media - "read_image": "👁 Analyzing image...", + "read_image": "👁 Analyzing image...", "read_document": "📄 Reading document...", - "read_audio": "🎧 Processing audio...", - "read_video": "🎬 Processing video...", - "create_image": "🎨 Creating image...", - "create_video": "🎬 Creating video...", - "create_audio": "🎵 Creating audio...", - "tts": "🔊 Generating speech...", + "read_audio": "🎧 Processing audio...", + "read_video": "🎬 Processing video...", + "create_image": "🎨 Creating image...", + "create_video": "🎬 Creating video...", + "create_audio": "🎵 Creating audio...", + "tts": "🔊 Generating speech...", // Browser "browser": "🌐 Browsing...", // Delegation & teams diff --git a/internal/channels/feishu/bot_parse.go b/internal/channels/feishu/bot_parse.go index cdd8604d..92c4610a 100644 --- a/internal/channels/feishu/bot_parse.go +++ b/internal/channels/feishu/bot_parse.go @@ -100,12 +100,12 @@ func parseMessageContent(rawContent, messageType string) string { } func parsePostContent(rawContent string) string { - var post map[string]interface{} + var post map[string]any if err := json.Unmarshal([]byte(rawContent), &post); err != nil { return rawContent } - var langContent interface{} + var langContent any for _, lang := range []string{"zh_cn", "en_us"} { if lc, ok := post[lang]; ok { langContent = lc @@ -122,25 +122,25 @@ func parsePostContent(rawContent string) string { return rawContent } - langMap, ok := langContent.(map[string]interface{}) + langMap, ok := langContent.(map[string]any) if !ok { return rawContent } - contentArr, ok := langMap["content"].([]interface{}) + contentArr, ok := langMap["content"].([]any) if !ok { return rawContent } var textParts []string for _, para := range contentArr { - paraArr, ok := para.([]interface{}) + paraArr, ok := para.([]any) if !ok { continue } var lineParts []string for _, elem := range paraArr { - elemMap, ok := elem.(map[string]interface{}) + elemMap, ok := elem.(map[string]any) if !ok { continue } diff --git a/internal/channels/feishu/feishu.go b/internal/channels/feishu/feishu.go index 37a7fbeb..d47b06ba 100644 --- a/internal/channels/feishu/feishu.go +++ b/internal/channels/feishu/feishu.go @@ -40,7 +40,7 @@ type Channel struct { dedup sync.Map // message_id → struct{} pairingDebounce sync.Map // senderID → time.Time reactions sync.Map // chatID → *reactionState - approvedGroups sync.Map // chatID → true (in-memory cache for paired groups) + approvedGroups sync.Map // chatID → true (in-memory cache for paired groups) groupAllowList []string groupHistory *channels.PendingHistory historyLimit int @@ -391,9 +391,9 @@ func resolveReceiveIDType(id string) string { // --- Content builders --- func buildPostContent(text string) string { - content := map[string]interface{}{ - "zh_cn": map[string]interface{}{ - "content": [][]map[string]interface{}{ + content := map[string]any{ + "zh_cn": map[string]any{ + "content": [][]map[string]any{ { { "tag": "md", @@ -407,14 +407,14 @@ func buildPostContent(text string) string { return string(data) } -func buildMarkdownCard(text string) map[string]interface{} { - return map[string]interface{}{ +func buildMarkdownCard(text string) map[string]any { + return map[string]any{ "schema": "2.0", - "config": map[string]interface{}{ + "config": map[string]any{ "wide_screen_mode": true, }, - "body": map[string]interface{}{ - "elements": []map[string]interface{}{ + "body": map[string]any{ + "elements": []map[string]any{ { "tag": "markdown", "content": text, diff --git a/internal/channels/feishu/larkclient.go b/internal/channels/feishu/larkclient.go index f17805ad..0e58bfb3 100644 --- a/internal/channels/feishu/larkclient.go +++ b/internal/channels/feishu/larkclient.go @@ -107,7 +107,7 @@ type apiResponse struct { } // doJSON performs an authenticated JSON API call with auto token refresh. -func (c *LarkClient) doJSON(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) { +func (c *LarkClient) doJSON(ctx context.Context, method, path string, body any) (*apiResponse, error) { resp, err := c.doJSONOnce(ctx, method, path, body) if err != nil { return nil, err @@ -120,7 +120,7 @@ func (c *LarkClient) doJSON(ctx context.Context, method, path string, body inter return resp, nil } -func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) { +func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body any) (*apiResponse, error) { token, err := c.getToken(ctx) if err != nil { return nil, err diff --git a/internal/channels/feishu/larkclient_messaging.go b/internal/channels/feishu/larkclient_messaging.go index 440b62fe..f828c733 100644 --- a/internal/channels/feishu/larkclient_messaging.go +++ b/internal/channels/feishu/larkclient_messaging.go @@ -112,7 +112,7 @@ func (c *LarkClient) CreateCard(ctx context.Context, cardType, data string) (str func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings string, seq int, uuid string) error { path := "/open-apis/cardkit/v1/cards/" + cardID - resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{ + resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{ "settings": settings, "sequence": seq, "uuid": uuid, @@ -128,7 +128,7 @@ func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings st func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, content string, seq int, uuid string) error { path := fmt.Sprintf("/open-apis/cardkit/v1/cards/%s/elements/%s", cardID, elementID) - resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{ + resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{ "content": content, "sequence": seq, "uuid": uuid, @@ -150,7 +150,7 @@ func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, c // Lark API: POST /open-apis/im/v1/messages/{message_id}/reactions func (c *LarkClient) AddMessageReaction(ctx context.Context, messageID, emojiType string) (string, error) { path := fmt.Sprintf("/open-apis/im/v1/messages/%s/reactions", messageID) - body := map[string]interface{}{ + body := map[string]any{ "reaction_type": map[string]string{ "emoji_type": emojiType, }, diff --git a/internal/channels/feishu/larkws.go b/internal/channels/feishu/larkws.go index 5a37ba31..02ec451e 100644 --- a/internal/channels/feishu/larkws.go +++ b/internal/channels/feishu/larkws.go @@ -16,12 +16,12 @@ import ( ) const ( - defaultPingInterval = 120 * time.Second - defaultReconnectNonce = 30 // seconds max jitter - defaultReconnectWait = 120 * time.Second - frameTypeControl = 0 - frameTypeData = 1 - fragmentBufferTTL = 5 * time.Second + defaultPingInterval = 120 * time.Second + defaultReconnectNonce = 30 // seconds max jitter + defaultReconnectWait = 120 * time.Second + frameTypeControl = 0 + frameTypeData = 1 + fragmentBufferTTL = 5 * time.Second ) // WSEventHandler processes incoming WebSocket events. @@ -315,7 +315,7 @@ func (c *WSClient) sendResponse(original *wsFrame, headers map[string]string) { } respHeaders = append(respHeaders, wsHeader{Key: "biz_rt", Value: "0"}) - respPayload, _ := json.Marshal(map[string]interface{}{ + respPayload, _ := json.Marshal(map[string]any{ "code": 0, "msg": "success", }) @@ -415,4 +415,3 @@ func (c *WSClient) reassemble(msgID string, total, seq int, data []byte) []byte delete(c.fragments, msgID) return result } - diff --git a/internal/channels/history_compaction.go b/internal/channels/history_compaction.go index 57c422f8..6f2c680c 100644 --- a/internal/channels/history_compaction.go +++ b/internal/channels/history_compaction.go @@ -103,7 +103,7 @@ func (ph *PendingHistory) runCompaction(historyKey string, cfg *CompactionConfig Content: "Summarize these group chat messages concisely, preserving key topics, decisions, names, and important context:\n\n" + sb.String(), }}, Model: cfg.Model, - Options: map[string]interface{}{"max_tokens": 512, "temperature": 0.3}, + Options: map[string]any{"max_tokens": 512, "temperature": 0.3}, }) if err != nil { slog.Warn("compaction.llm_failed", "channel", ph.channelName, "key", historyKey, "error", err) diff --git a/internal/channels/instance_loader.go b/internal/channels/instance_loader.go index cd90e75a..3e2cfc72 100644 --- a/internal/channels/instance_loader.go +++ b/internal/channels/instance_loader.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "sync" "time" @@ -146,7 +147,7 @@ func coerceStringBools(data json.RawMessage) json.RawMessage { if len(data) == 0 { return data } - var m map[string]interface{} + var m map[string]any if json.Unmarshal(data, &m) != nil { return data } @@ -176,9 +177,7 @@ func (l *InstanceLoader) LoadedNames() map[string]struct{} { defer l.mu.Unlock() result := make(map[string]struct{}, len(l.loaded)) - for k, v := range l.loaded { - result[k] = v - } + maps.Copy(result, l.loaded) return result } diff --git a/internal/channels/manager.go b/internal/channels/manager.go index 8e0adad5..d1a6bef6 100644 --- a/internal/channels/manager.go +++ b/internal/channels/manager.go @@ -107,13 +107,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) { } // GetStatus returns the running status of all channels. -func (m *Manager) GetStatus() map[string]interface{} { +func (m *Manager) GetStatus() map[string]any { m.mu.RLock() defer m.mu.RUnlock() - status := make(map[string]interface{}) + status := make(map[string]any) for name, channel := range m.channels { - status[name] = map[string]interface{}{ + status[name] = map[string]any{ "enabled": true, "running": channel.IsRunning(), } diff --git a/internal/channels/telegram/format.go b/internal/channels/telegram/format.go index 77027a14..2be7df09 100644 --- a/internal/channels/telegram/format.go +++ b/internal/channels/telegram/format.go @@ -298,12 +298,12 @@ func parseTableRow(line string) []string { // stripInlineMarkdown removes common inline markdown markers from text. // Used for table cells that render inside code blocks where formatting has no effect. var ( - reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`) - reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`) - reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`) + reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`) + reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`) + reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`) reStripItalicUnderscore = regexp.MustCompile(`_([^_]+)_`) - reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`) - reStripInlineCode = regexp.MustCompile("`([^`]+)`") + reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`) + reStripInlineCode = regexp.MustCompile("`([^`]+)`") ) func stripInlineMarkdown(s string) string { @@ -325,10 +325,7 @@ func renderRow(cells []string, colWidths []int) string { cell = cells[j] } // Pad with spaces to align columns - padding := w - displayWidth(cell) - if padding < 0 { - padding = 0 - } + padding := max(w-displayWidth(cell), 0) parts = append(parts, " "+cell+strings.Repeat(" ", padding)+" ") } return "|" + strings.Join(parts, "|") + "|" diff --git a/internal/channels/telegram/topic_config_test.go b/internal/channels/telegram/topic_config_test.go index 4a167ebb..0d7331fa 100644 --- a/internal/channels/telegram/topic_config_test.go +++ b/internal/channels/telegram/topic_config_test.go @@ -6,12 +6,13 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/config" ) -func boolPtr(b bool) *bool { return &b } +//go:fix inline +func boolPtr(b bool) *bool { return new(b) } func TestResolveTopicConfig_Defaults(t *testing.T) { cfg := config.TelegramConfig{ GroupPolicy: "open", - RequireMention: boolPtr(true), + RequireMention: new(true), AllowFrom: []string{"user1"}, } @@ -37,7 +38,7 @@ func TestResolveTopicConfig_WildcardGroup(t *testing.T) { Groups: map[string]*config.TelegramGroupConfig{ "*": { GroupPolicy: "allowlist", - RequireMention: boolPtr(false), + RequireMention: new(false), AllowFrom: []string{"admin1"}, }, }, @@ -62,7 +63,7 @@ func TestResolveTopicConfig_SpecificGroupOverridesWildcard(t *testing.T) { Groups: map[string]*config.TelegramGroupConfig{ "*": { GroupPolicy: "allowlist", - RequireMention: boolPtr(false), + RequireMention: new(false), AllowFrom: []string{"admin1"}, SystemPrompt: "wildcard prompt", }, @@ -96,12 +97,12 @@ func TestResolveTopicConfig_TopicOverridesGroup(t *testing.T) { GroupPolicy: "open", Groups: map[string]*config.TelegramGroupConfig{ "-100123": { - RequireMention: boolPtr(true), + RequireMention: new(true), SystemPrompt: "group prompt", Skills: []string{"skill_a", "skill_b"}, Topics: map[string]*config.TelegramTopicConfig{ "42": { - RequireMention: boolPtr(false), + RequireMention: new(false), Skills: []string{"skill_c"}, SystemPrompt: "topic prompt", }, @@ -171,10 +172,10 @@ func TestResolveTopicConfig_DisabledTopic(t *testing.T) { cfg := config.TelegramConfig{ Groups: map[string]*config.TelegramGroupConfig{ "-100123": { - Enabled: boolPtr(true), + Enabled: new(true), Topics: map[string]*config.TelegramTopicConfig{ "42": { - Enabled: boolPtr(false), + Enabled: new(false), }, }, }, @@ -230,7 +231,7 @@ func TestEffectiveRequireMention(t *testing.T) { } // explicit requireMention overrides default - r2 := resolvedTopicConfig{requireMention: boolPtr(false)} + r2 := resolvedTopicConfig{requireMention: new(false)} if r2.effectiveRequireMention(true) != false { t.Error("effectiveRequireMention(true) with override=false should be false") } diff --git a/internal/channels/whatsapp/whatsapp.go b/internal/channels/whatsapp/whatsapp.go index 809de502..d42353ed 100644 --- a/internal/channels/whatsapp/whatsapp.go +++ b/internal/channels/whatsapp/whatsapp.go @@ -101,7 +101,7 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error { return fmt.Errorf("whatsapp bridge not connected") } - payload := map[string]interface{}{ + payload := map[string]any{ "type": "message", "to": msg.ChatID, "content": msg.Content, @@ -188,7 +188,7 @@ func (c *Channel) listenLoop() { continue } - var msg map[string]interface{} + var msg map[string]any if err := json.Unmarshal(message, &msg); err != nil { slog.Warn("invalid whatsapp message JSON", "error", err) continue @@ -203,7 +203,7 @@ func (c *Channel) listenLoop() { // handleIncomingMessage processes a message received from the bridge. // Expected format: {"type":"message","from":"...","chat":"...","content":"...","id":"...","from_name":"...","media":[...]} -func (c *Channel) handleIncomingMessage(msg map[string]interface{}) { +func (c *Channel) handleIncomingMessage(msg map[string]any) { senderID, ok := msg["from"].(string) if !ok || senderID == "" { return @@ -244,7 +244,7 @@ func (c *Channel) handleIncomingMessage(msg map[string]interface{}) { } var media []string - if mediaData, ok := msg["media"].([]interface{}); ok { + if mediaData, ok := msg["media"].([]any); ok { media = make([]string, 0, len(mediaData)) for _, m := range mediaData { if path, ok := m.(string); ok { @@ -368,7 +368,7 @@ func (c *Channel) sendPairingReply(senderID, chatID string) { return } - payload, _ := json.Marshal(map[string]interface{}{ + payload, _ := json.Marshal(map[string]any{ "type": "message", "to": chatID, "content": replyText, diff --git a/internal/channels/zalo/personal/protocol/auth.go b/internal/channels/zalo/personal/protocol/auth.go index ba186474..2a8109de 100644 --- a/internal/channels/zalo/personal/protocol/auth.go +++ b/internal/channels/zalo/personal/protocol/auth.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "net/http" "net/url" "regexp" @@ -294,13 +295,13 @@ func loadLoginPage(ctx context.Context, sess *Session) (string, error) { } var qrHeaders = http.Header{ - "Accept": {"*/*"}, - "Content-Type": {"application/x-www-form-urlencoded"}, - "Sec-Fetch-Dest": {"empty"}, - "Sec-Fetch-Mode": {"cors"}, - "Sec-Fetch-Site": {"same-origin"}, - "Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"}, - "Referrer-Policy": {"strict-origin-when-cross-origin"}, + "Accept": {"*/*"}, + "Content-Type": {"application/x-www-form-urlencoded"}, + "Sec-Fetch-Dest": {"empty"}, + "Sec-Fetch-Mode": {"cors"}, + "Sec-Fetch-Site": {"same-origin"}, + "Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"}, + "Referrer-Policy": {"strict-origin-when-cross-origin"}, } func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[string]string) (*http.Response, error) { @@ -310,9 +311,7 @@ func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[st return nil, err } req.Header.Set("User-Agent", sess.UserAgent) - for k, v := range qrHeaders { - req.Header[k] = v - } + maps.Copy(req.Header, qrHeaders) return sess.Client.Do(req) } @@ -456,9 +455,7 @@ func qrGetUserInfo(ctx context.Context, sess *Session) (*QRUserInfo, error) { func setDefaultHeaders(req *http.Request, sess *Session) { h := defaultHeaders(sess) - for k, v := range h { - req.Header[k] = v - } + maps.Copy(req.Header, h) } // readJSON decodes a potentially gzip/deflate-encoded JSON response. diff --git a/internal/channels/zalo/personal/protocol/client.go b/internal/channels/zalo/personal/protocol/client.go index 8dfe8276..8e062cef 100644 --- a/internal/channels/zalo/personal/protocol/client.go +++ b/internal/channels/zalo/personal/protocol/client.go @@ -104,13 +104,14 @@ func generateSignKey(typeStr string, params map[string]any) string { } sort.Strings(keys) - signStr := "zsecure" + typeStr + var signStr strings.Builder + signStr.WriteString("zsecure" + typeStr) for _, k := range keys { if v := params[k]; v != nil { - signStr += convertToString(v) + signStr.WriteString(convertToString(v)) } } - hash := md5.Sum([]byte(signStr)) + hash := md5.Sum([]byte(signStr.String())) return hex.EncodeToString(hash[:]) } @@ -131,11 +132,11 @@ func getEncryptParam(sess *Session, typeStr string) (params map[string]any, enk } params = map[string]any{ - "zcid": zcid, - "enc_ver": DefaultEncryptVer, - "zcid_ext": zcidExt, - "params": encKey.encData, - "type": DefaultAPIType, + "zcid": zcid, + "enc_ver": DefaultEncryptVer, + "zcid_ext": zcidExt, + "params": encKey.encData, + "type": DefaultAPIType, "client_version": DefaultAPIVersion, } diff --git a/internal/channels/zalo/personal/protocol/client_test.go b/internal/channels/zalo/personal/protocol/client_test.go index 4580a785..37274aec 100644 --- a/internal/channels/zalo/personal/protocol/client_test.go +++ b/internal/channels/zalo/personal/protocol/client_test.go @@ -145,7 +145,7 @@ func TestReverseCopy(t *testing.T) { } func TestRandomHexString(t *testing.T) { - for i := 0; i < 10; i++ { + for range 10 { s := randomHexString(6, 12) if len(s) < 6 || len(s) > 12 { t.Errorf("randomHexString(6,12) = len %d, want 6-12", len(s)) diff --git a/internal/channels/zalo/zalo.go b/internal/channels/zalo/zalo.go index 91825153..c727e50e 100644 --- a/internal/channels/zalo/zalo.go +++ b/internal/channels/zalo/zalo.go @@ -35,14 +35,14 @@ const ( // Channel connects to the Zalo OA Bot API. type Channel struct { *channels.BaseChannel - token string - dmPolicy string - mediaMaxMB int - blockReply *bool - pairingService store.PairingStore + token string + dmPolicy string + mediaMaxMB int + blockReply *bool + pairingService store.PairingStore pairingDebounce sync.Map // senderID → time.Time - stopCh chan struct{} - client *http.Client + stopCh chan struct{} + client *http.Client } // New creates a new Zalo channel. @@ -379,7 +379,7 @@ type zaloUpdate struct { Message *zaloMessage `json:"message,omitempty"` } -func (c *Channel) callAPI(method string, body interface{}) (json.RawMessage, error) { +func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) { url := fmt.Sprintf("%s/bot%s/%s", apiBase, c.token, method) var reqBody io.Reader @@ -436,7 +436,7 @@ func (c *Channel) getMe() (*zaloBotInfo, error) { } func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) { - params := map[string]interface{}{ + params := map[string]any{ "timeout": timeout, } @@ -453,7 +453,7 @@ func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) { } func (c *Channel) sendMessage(chatID, text string) error { - params := map[string]interface{}{ + params := map[string]any{ "chat_id": chatID, "text": text, } @@ -463,7 +463,7 @@ func (c *Channel) sendMessage(chatID, text string) error { } func (c *Channel) sendPhoto(chatID, photoURL, caption string) error { - params := map[string]interface{}{ + params := map[string]any{ "chat_id": chatID, "photo": photoURL, } diff --git a/internal/config/config.go b/internal/config/config.go index de1f10ac..bff5b475 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,7 +19,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error { *f = ss return nil } - var raw []interface{} + var raw []any if err := json.Unmarshal(data, &raw); err != nil { return err } @@ -46,22 +46,22 @@ type Config struct { Gateway GatewayConfig `json:"gateway"` Tools ToolsConfig `json:"tools"` Sessions SessionsConfig `json:"sessions"` - Database DatabaseConfig `json:"database,omitempty"` - Tts TtsConfig `json:"tts,omitempty"` - Cron CronConfig `json:"cron,omitempty"` - Telemetry TelemetryConfig `json:"telemetry,omitempty"` - Tailscale TailscaleConfig `json:"tailscale,omitempty"` + Database DatabaseConfig `json:"database"` + Tts TtsConfig `json:"tts"` + Cron CronConfig `json:"cron"` + Telemetry TelemetryConfig `json:"telemetry"` + Tailscale TailscaleConfig `json:"tailscale"` Bindings []AgentBinding `json:"bindings,omitempty"` - mu sync.RWMutex + mu sync.RWMutex } // TailscaleConfig configures the optional Tailscale tsnet listener. // Requires building with -tags tsnet. Auth key from env only (never persisted). type TailscaleConfig struct { - Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway") - StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw) - AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only - Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false) + Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway") + StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw) + AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only + Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false) EnableTLS bool `json:"enable_tls,omitempty"` // use ListenTLS for auto HTTPS certs } @@ -86,7 +86,7 @@ type AgentBinding struct { // BindingMatch specifies what messages this binding applies to. type BindingMatch struct { - Channel string `json:"channel"` // "telegram", "discord", "slack", etc. + Channel string `json:"channel"` // "telegram", "discord", "slack", etc. AccountID string `json:"accountId,omitempty"` // bot account ID Peer *BindingPeer `json:"peer,omitempty"` // specific DM/group GuildID string `json:"guildId,omitempty"` // Discord guild @@ -106,21 +106,21 @@ type AgentsConfig struct { // AgentDefaults are default settings for all agents. type AgentDefaults struct { - Workspace string `json:"workspace"` - RestrictToWorkspace bool `json:"restrict_to_workspace"` - Provider string `json:"provider"` - Model string `json:"model"` - MaxTokens int `json:"max_tokens"` - Temperature float64 `json:"temperature"` - MaxToolIterations int `json:"max_tool_iterations"` - ContextWindow int `json:"context_window"` - MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25) - AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined" - Subagents *SubagentsConfig `json:"subagents,omitempty"` - Sandbox *SandboxConfig `json:"sandbox,omitempty"` + Workspace string `json:"workspace"` + RestrictToWorkspace bool `json:"restrict_to_workspace"` + Provider string `json:"provider"` + Model string `json:"model"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` + MaxToolIterations int `json:"max_tool_iterations"` + ContextWindow int `json:"context_window"` + MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25) + AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined" + Subagents *SubagentsConfig `json:"subagents,omitempty"` + Sandbox *SandboxConfig `json:"sandbox,omitempty"` Memory *MemoryConfig `json:"memory,omitempty"` - Compaction *CompactionConfig `json:"compaction,omitempty"` - ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"` + Compaction *CompactionConfig `json:"compaction,omitempty"` + ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"` // Bootstrap context truncation limits (matching TS bootstrapMaxChars / bootstrapTotalMaxChars) BootstrapMaxChars int `json:"bootstrapMaxChars,omitempty"` // per-file max before truncation (default 20000) BootstrapTotalMaxChars int `json:"bootstrapTotalMaxChars,omitempty"` // total budget across all files (default 24000) @@ -137,7 +137,7 @@ type CompactionConfig struct { MaxHistoryShare float64 `json:"maxHistoryShare,omitempty"` // max share of context for history (default 0.75) MinMessages int `json:"minMessages,omitempty"` // min messages before compaction triggers (default 50) KeepLastMessages int `json:"keepLastMessages,omitempty"` // messages to keep after compaction (default 4) - MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush + MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush } // MemoryFlushConfig configures the pre-compaction memory flush. @@ -146,20 +146,20 @@ type MemoryFlushConfig struct { Enabled *bool `json:"enabled,omitempty"` // default true (nil = enabled) SoftThresholdTokens int `json:"softThresholdTokens,omitempty"` // flush when within N tokens of compaction (default 4000) Prompt string `json:"prompt,omitempty"` // user prompt for flush turn - SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn + SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn } // ContextPruningConfig configures in-memory context pruning of old tool results. // Matching TS src/agents/pi-extensions/context-pruning/settings.ts. // Mode "cache-ttl": prune when context exceeds softTrimRatio of context window. type ContextPruningConfig struct { - Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl" - KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3) - SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3) - HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5) + Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl" + KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3) + SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3) + HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5) MinPrunableToolChars int `json:"minPrunableToolChars,omitempty"` // min chars in prunable tools before acting (default 50000) - SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"` - HardClear *ContextPruningHardClear `json:"hardClear,omitempty"` + SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"` + HardClear *ContextPruningHardClear `json:"hardClear,omitempty"` } // ContextPruningSoftTrim configures how long tool results are trimmed. @@ -205,9 +205,9 @@ type SandboxConfig struct { Env map[string]string `json:"env,omitempty"` // extra environment variables // Enhanced security - User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody") - TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default) - MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB) + User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody") + TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default) + MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB) // Pruning (matching TS SandboxPruneSettings) IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24) @@ -355,9 +355,9 @@ type AgentSpec struct { MaxToolIterations int `json:"max_tool_iterations,omitempty"` ContextWindow int `json:"context_window,omitempty"` MaxToolCalls int `json:"max_tool_calls,omitempty"` // per-agent override - AgentType string `json:"agent_type,omitempty"` // "open" or "predefined" - Skills []string `json:"skills,omitempty"` // nil = all skills allowed - Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy + AgentType string `json:"agent_type,omitempty"` // "open" or "predefined" + Skills []string `json:"skills,omitempty"` // nil = all skills allowed + Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy Workspace string `json:"workspace,omitempty"` Default bool `json:"default,omitempty"` Sandbox *SandboxConfig `json:"sandbox,omitempty"` diff --git a/internal/config/config_channels.go b/internal/config/config_channels.go index 347072c4..95b32c67 100644 --- a/internal/config/config_channels.go +++ b/internal/config/config_channels.go @@ -70,11 +70,11 @@ type TelegramTopicConfig struct { } type DiscordConfig struct { - Enabled bool `json:"enabled"` - Token string `json:"token"` - AllowFrom FlexibleStringSlice `json:"allow_from"` - DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled" - GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled" + Enabled bool `json:"enabled"` + Token string `json:"token"` + AllowFrom FlexibleStringSlice `json:"allow_from"` + DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled" + GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled" RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in groups (default true) HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled) BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit) @@ -88,22 +88,22 @@ type DiscordConfig struct { type SlackConfig struct { Enabled bool `json:"enabled"` - BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token) - AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode) - UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity) + BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token) + AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode) + UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity) AllowFrom FlexibleStringSlice `json:"allow_from"` - DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled" - GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled" - RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true) - HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled) - DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false) - GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false) - NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false) - ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full" - BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit) - DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled) - ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention) - MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB) + DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled" + GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled" + RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true) + HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled) + DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false) + GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false) + NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false) + ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full" + BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit) + DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled) + ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention) + MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB) } type WhatsAppConfig struct { @@ -159,7 +159,7 @@ type FeishuConfig struct { Streaming *bool `json:"streaming,omitempty"` // default true ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full" — typing emoji reactions HistoryLimit int `json:"history_limit,omitempty"` - BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit) + BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit) STTProxyURL string `json:"stt_proxy_url,omitempty"` STTAPIKey string `json:"stt_api_key,omitempty"` STTTenantID string `json:"stt_tenant_id,omitempty"` @@ -169,22 +169,22 @@ type FeishuConfig struct { // ProvidersConfig maps provider name to its config. type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic"` - OpenAI ProviderConfig `json:"openai"` - OpenRouter ProviderConfig `json:"openrouter"` - Groq ProviderConfig `json:"groq"` - Gemini ProviderConfig `json:"gemini"` - DeepSeek ProviderConfig `json:"deepseek"` - Mistral ProviderConfig `json:"mistral"` - XAI ProviderConfig `json:"xai"` - MiniMax ProviderConfig `json:"minimax"` - Cohere ProviderConfig `json:"cohere"` - Perplexity ProviderConfig `json:"perplexity"` - DashScope ProviderConfig `json:"dashscope"` - Bailian ProviderConfig `json:"bailian"` - Zai ProviderConfig `json:"zai"` - ZaiCoding ProviderConfig `json:"zai_coding"` - ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` + Anthropic ProviderConfig `json:"anthropic"` + OpenAI ProviderConfig `json:"openai"` + OpenRouter ProviderConfig `json:"openrouter"` + Groq ProviderConfig `json:"groq"` + Gemini ProviderConfig `json:"gemini"` + DeepSeek ProviderConfig `json:"deepseek"` + Mistral ProviderConfig `json:"mistral"` + XAI ProviderConfig `json:"xai"` + MiniMax ProviderConfig `json:"minimax"` + Cohere ProviderConfig `json:"cohere"` + Perplexity ProviderConfig `json:"perplexity"` + DashScope ProviderConfig `json:"dashscope"` + Bailian ProviderConfig `json:"bailian"` + Zai ProviderConfig `json:"zai"` + ZaiCoding ProviderConfig `json:"zai_coding"` + ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` } // ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key). @@ -259,13 +259,13 @@ type GatewayConfig struct { // ToolsConfig controls tool availability, policy, and web search. type ToolsConfig struct { - Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full" - Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx") - Deny []string `json:"deny,omitempty"` // global deny list - AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing - ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides - ExecApproval ExecApprovalCfg `json:"execApproval,omitempty"` // exec command approval settings - WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching + Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full" + Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx") + Deny []string `json:"deny,omitempty"` // global deny list + AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing + ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides + ExecApproval ExecApprovalCfg `json:"execApproval"` // exec command approval settings + WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching Web WebToolsConfig `json:"web"` Browser BrowserToolConfig `json:"browser"` RateLimitPerHour int `json:"rate_limit_per_hour,omitempty"` // max tool executions per hour per session (0 = disabled) @@ -307,9 +307,9 @@ type WebFetchPolicyConfig struct { // BrowserToolConfig controls the browser automation tool. type BrowserToolConfig struct { - Enabled bool `json:"enabled"` // enable the browser tool (default false) - Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set) - RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222" + Enabled bool `json:"enabled"` // enable the browser tool (default false) + Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set) + RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222" } // ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider). @@ -370,10 +370,10 @@ type TtsConfig struct { Mode string `json:"mode,omitempty"` // "final" (default), "all" MaxLength int `json:"max_length,omitempty"` // max text length before truncation (default 1500) TimeoutMs int `json:"timeout_ms,omitempty"` // API timeout in ms (default 30000) - OpenAI TtsOpenAIConfig `json:"openai,omitempty"` - ElevenLabs TtsElevenLabsConfig `json:"elevenlabs,omitempty"` - Edge TtsEdgeConfig `json:"edge,omitempty"` - MiniMax TtsMiniMaxConfig `json:"minimax,omitempty"` + OpenAI TtsOpenAIConfig `json:"openai"` + ElevenLabs TtsElevenLabsConfig `json:"elevenlabs"` + Edge TtsEdgeConfig `json:"edge"` + MiniMax TtsMiniMaxConfig `json:"minimax"` } // TtsOpenAIConfig configures the OpenAI TTS provider. diff --git a/internal/cron/retry.go b/internal/cron/retry.go index d2926735..cf0f7d12 100644 --- a/internal/cron/retry.go +++ b/internal/cron/retry.go @@ -40,10 +40,9 @@ func ExecuteWithRetry(fn func() (string, error), cfg RetryConfig) (result string // backoffWithJitter computes delay = min(base * 2^attempt, max) + jitter(±25%). func backoffWithJitter(base, max time.Duration, attempt int) time.Duration { - delay := base << uint(attempt) // base * 2^attempt - if delay > max { - delay = max - } + delay := min( + // base * 2^attempt + base<= level { @@ -194,7 +194,7 @@ func (t *LogTee) Subscribe(client *Client, level slog.Level) { } // Send sentinel so the client knows tailing started. - client.SendEvent(*protocol.NewEvent("log", map[string]interface{}{ + client.SendEvent(*protocol.NewEvent("log", map[string]any{ "timestamp": time.Now().UnixMilli(), "level": "info", "message": "Log tailing started", @@ -223,7 +223,7 @@ func levelName(l slog.Level) string { } // logLevelValue converts a level name string back to slog.Level for filtering. -func logLevelValue(v interface{}) slog.Level { +func logLevelValue(v any) slog.Level { s, _ := v.(string) switch s { case "error": diff --git a/internal/gateway/methods/agent_links.go b/internal/gateway/methods/agent_links.go index d3ab6b15..be110238 100644 --- a/internal/gateway/methods/agent_links.go +++ b/internal/gateway/methods/agent_links.go @@ -88,7 +88,7 @@ func (m *AgentLinksMethods) handleList(ctx context.Context, client *gateway.Clie return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "links": links, "count": len(links), })) @@ -182,7 +182,7 @@ func (m *AgentLinksMethods) handleCreate(ctx context.Context, client *gateway.Cl } m.emitTeamCacheInvalidate() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "link": link, })) @@ -281,7 +281,7 @@ func (m *AgentLinksMethods) handleUpdate(ctx context.Context, client *gateway.Cl m.invalidateLinkAgents(ctx, linkID) m.emitTeamCacheInvalidate() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit agent_link.updated event if m.msgBus != nil { @@ -350,7 +350,7 @@ func (m *AgentLinksMethods) handleDelete(ctx context.Context, client *gateway.Cl } m.emitTeamCacheInvalidate() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit agent_link.deleted event if m.msgBus != nil && link != nil { diff --git a/internal/gateway/methods/agents.go b/internal/gateway/methods/agents.go index 25cfb8d7..b9729dfc 100644 --- a/internal/gateway/methods/agents.go +++ b/internal/gateway/methods/agents.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log/slog" + "slices" "github.com/nextlevelbuilder/goclaw/internal/agent" "github.com/nextlevelbuilder/goclaw/internal/config" @@ -34,12 +35,7 @@ func (m *AgentsMethods) isOwnerUser(userID string) bool { if userID == "" { return false } - for _, id := range m.cfg.Gateway.OwnerIDs { - if id == userID { - return true - } - } - return false + return slices.Contains(m.cfg.Gateway.OwnerIDs, userID) } func (m *AgentsMethods) Register(router *gateway.MethodRouter) { @@ -74,7 +70,7 @@ func (m *AgentsMethods) handleAgent(_ context.Context, client *gateway.Client, r return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "id": loop.ID(), "isRunning": loop.IsRunning(), })) @@ -96,7 +92,7 @@ func (m *AgentsMethods) handleAgentWait(_ context.Context, client *gateway.Clien } // Return current status (blocking wait is a future enhancement). - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "id": loop.ID(), "status": "idle", })) @@ -124,12 +120,12 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client, return } - infos := make([]map[string]interface{}, 0, len(agents)) + infos := make([]map[string]any, 0, len(agents)) for _, a := range agents { if a.Status != store.AgentStatusActive { continue } - infos = append(infos, map[string]interface{}{ + infos = append(infos, map[string]any{ "id": a.AgentKey, "name": a.DisplayName, "model": a.Model, @@ -139,7 +135,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client, "isRunning": m.agents.IsRunning(a.AgentKey), }) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agents": infos, })) return @@ -147,7 +143,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client, // Fallback: return router-cached agents. infos := m.agents.ListInfo() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agents": infos, })) } diff --git a/internal/gateway/methods/agents_create.go b/internal/gateway/methods/agents_create.go index b4049894..4189e330 100644 --- a/internal/gateway/methods/agents_create.go +++ b/internal/gateway/methods/agents_create.go @@ -157,7 +157,7 @@ func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client os.MkdirAll(ws, 0755) bootstrap.EnsureWorkspaceFiles(ws) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "agentId": agentID, "name": params.Name, diff --git a/internal/gateway/methods/agents_delete.go b/internal/gateway/methods/agents_delete.go index 98459ab8..bdf5a5f5 100644 --- a/internal/gateway/methods/agents_delete.go +++ b/internal/gateway/methods/agents_delete.go @@ -92,7 +92,7 @@ func (m *AgentsMethods) handleDelete(ctx context.Context, client *gateway.Client } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "agentId": params.AgentID, "removedBindings": removedBindings, diff --git a/internal/gateway/methods/agents_files.go b/internal/gateway/methods/agents_files.go index f723ddb8..534c8377 100644 --- a/internal/gateway/methods/agents_files.go +++ b/internal/gateway/methods/agents_files.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "slices" "github.com/nextlevelbuilder/goclaw/internal/bootstrap" "github.com/nextlevelbuilder/goclaw/internal/config" @@ -55,23 +56,23 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli dbMap[f.FileName] = f } - files := make([]map[string]interface{}, 0, len(allowedAgentFiles)) + files := make([]map[string]any, 0, len(allowedAgentFiles)) for _, name := range allowedAgentFiles { if f, ok := dbMap[name]; ok { - files = append(files, map[string]interface{}{ + files = append(files, map[string]any{ "name": name, "missing": false, "size": len(f.Content), }) } else { - files = append(files, map[string]interface{}{ + files = append(files, map[string]any{ "name": name, "missing": true, }) } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, "files": files, })) @@ -80,19 +81,19 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli // --- Fallback: filesystem --- ws := m.resolveWorkspace(params.AgentID) - files := make([]map[string]interface{}, 0, len(allowedAgentFiles)) + files := make([]map[string]any, 0, len(allowedAgentFiles)) for _, name := range allowedAgentFiles { p := filepath.Join(ws, name) info, err := os.Stat(p) if err != nil { - files = append(files, map[string]interface{}{ + files = append(files, map[string]any{ "name": name, "path": p, "missing": true, }) } else { - files = append(files, map[string]interface{}{ + files = append(files, map[string]any{ "name": name, "path": p, "missing": false, @@ -102,7 +103,7 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, "workspace": ws, "files": files, @@ -150,9 +151,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie for _, f := range dbFiles { if f.FileName == params.Name { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "missing": false, "size": len(f.Content), @@ -164,9 +165,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie } // File not found in DB - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "missing": true, }, @@ -180,10 +181,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie info, err := os.Stat(p) if err != nil { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, "workspace": ws, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "path": p, "missing": true, @@ -193,10 +194,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie } content, _ := os.ReadFile(p) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, "workspace": ws, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "path": p, "missing": false, @@ -253,9 +254,9 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie m.interceptor.InvalidateAgent(ag.ID) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "missing": false, "size": len(params.Content), @@ -276,10 +277,10 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie } info, _ := os.Stat(p) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agentId": params.AgentID, "workspace": ws, - "file": map[string]interface{}{ + "file": map[string]any{ "name": params.Name, "path": p, "missing": false, @@ -300,10 +301,5 @@ func (m *AgentsMethods) resolveWorkspace(agentID string) string { } func isAllowedFile(name string) bool { - for _, f := range allowedAgentFiles { - if f == name { - return true - } - } - return false + return slices.Contains(allowedAgentFiles, name) } diff --git a/internal/gateway/methods/agents_identity.go b/internal/gateway/methods/agents_identity.go index 3876b22f..f8791911 100644 --- a/internal/gateway/methods/agents_identity.go +++ b/internal/gateway/methods/agents_identity.go @@ -36,7 +36,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli } } - result := map[string]interface{}{ + result := map[string]any{ "agentId": params.AgentID, } @@ -106,7 +106,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli // parseIdentityContent parses IDENTITY.md content string and extracts Key: Value fields. func parseIdentityContent(content string) map[string]string { result := make(map[string]string) - for _, line := range strings.Split(content, "\n") { + for line := range strings.SplitSeq(content, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "#") || line == "" { continue diff --git a/internal/gateway/methods/agents_update.go b/internal/gateway/methods/agents_update.go index 1416ce6f..730d7fca 100644 --- a/internal/gateway/methods/agents_update.go +++ b/internal/gateway/methods/agents_update.go @@ -155,7 +155,7 @@ func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "agentId": params.AgentID, })) diff --git a/internal/gateway/methods/channel_instances.go b/internal/gateway/methods/channel_instances.go index 7cc7799b..6a54b6ed 100644 --- a/internal/gateway/methods/channel_instances.go +++ b/internal/gateway/methods/channel_instances.go @@ -54,12 +54,12 @@ func (m *ChannelInstancesMethods) handleList(ctx context.Context, client *gatewa } // Mask credentials in response — never expose secrets via WS. - result := make([]map[string]interface{}, 0, len(instances)) + result := make([]map[string]any, 0, len(instances)) for _, inst := range instances { result = append(result, maskInstance(inst)) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "instances": result, })) } @@ -160,7 +160,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate return } - var updates map[string]interface{} + var updates map[string]any if err := json.Unmarshal(params.Updates, &updates); err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidUpdates))) return @@ -173,7 +173,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate } m.emitCacheInvalidate() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "updated"})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "updated"})) } func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { @@ -209,31 +209,31 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate } m.emitCacheInvalidate() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "deleted"})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "deleted"})) } // maskInstance returns a map representation with credentials masked. -func maskInstance(inst store.ChannelInstanceData) map[string]interface{} { - result := map[string]interface{}{ - "id": inst.ID, - "name": inst.Name, - "display_name": inst.DisplayName, - "channel_type": inst.ChannelType, - "agent_id": inst.AgentID, - "config": inst.Config, - "enabled": inst.Enabled, - "is_default": store.IsDefaultChannelInstance(inst.Name), - "has_credentials": len(inst.Credentials) > 0, - "created_by": inst.CreatedBy, - "created_at": inst.CreatedAt, - "updated_at": inst.UpdatedAt, +func maskInstance(inst store.ChannelInstanceData) map[string]any { + result := map[string]any{ + "id": inst.ID, + "name": inst.Name, + "display_name": inst.DisplayName, + "channel_type": inst.ChannelType, + "agent_id": inst.AgentID, + "config": inst.Config, + "enabled": inst.Enabled, + "is_default": store.IsDefaultChannelInstance(inst.Name), + "has_credentials": len(inst.Credentials) > 0, + "created_by": inst.CreatedBy, + "created_at": inst.CreatedAt, + "updated_at": inst.UpdatedAt, } // Mask credentials: show keys with "***" values if len(inst.Credentials) > 0 { - var raw map[string]interface{} + var raw map[string]any if json.Unmarshal(inst.Credentials, &raw) == nil { - masked := make(map[string]interface{}, len(raw)) + masked := make(map[string]any, len(raw)) for k := range raw { masked[k] = "***" } diff --git a/internal/gateway/methods/channels.go b/internal/gateway/methods/channels.go index f40c7d8a..dd3241f6 100644 --- a/internal/gateway/methods/channels.go +++ b/internal/gateway/methods/channels.go @@ -28,7 +28,7 @@ func (m *ChannelsMethods) Register(router *gateway.MethodRouter) { func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { enabled := m.manager.GetEnabledChannels() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "channels": enabled, })) } @@ -36,7 +36,7 @@ func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client, func (m *ChannelsMethods) handleStatus(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { status := m.manager.GetStatus() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "channels": status, })) } diff --git a/internal/gateway/methods/chat.go b/internal/gateway/methods/chat.go index 162ad1e9..6a783c78 100644 --- a/internal/gateway/methods/chat.go +++ b/internal/gateway/methods/chat.go @@ -155,7 +155,7 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re return } - resp := map[string]interface{}{ + resp := map[string]any{ "runId": result.RunID, "content": result.Content, "usage": result.Usage, @@ -191,7 +191,7 @@ func (m *ChatMethods) handleHistory(ctx context.Context, client *gateway.Client, history := m.sessions.GetHistory(sessionKey) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "messages": history, })) } @@ -237,7 +237,7 @@ func (m *ChatMethods) handleInject(ctx context.Context, client *gateway.Client, Content: text, }) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "messageId": messageID, })) @@ -281,7 +281,7 @@ func (m *ChatMethods) handleAbort(ctx context.Context, client *gateway.Client, r abortedIDs = m.agents.AbortRunsForSession(params.SessionKey) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "aborted": len(abortedIDs) > 0, "runIds": abortedIDs, diff --git a/internal/gateway/methods/config.go b/internal/gateway/methods/config.go index 0543b803..eb9e38e9 100644 --- a/internal/gateway/methods/config.go +++ b/internal/gateway/methods/config.go @@ -36,7 +36,7 @@ func (m *ConfigMethods) Register(router *gateway.MethodRouter) { } func (m *ConfigMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "config": m.cfg.MaskedCopy(), "hash": m.cfg.Hash(), "path": m.cfgPath, @@ -93,7 +93,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client, m.cfg.ApplyEnvOverrides() m.broadcastChanged() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "path": m.cfgPath, "config": m.cfg.MaskedCopy(), @@ -165,7 +165,7 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client, m.cfg.ApplyEnvOverrides() m.broadcastChanged() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "path": m.cfgPath, "config": m.cfg.MaskedCopy(), @@ -184,37 +184,37 @@ func (m *ConfigMethods) broadcastChanged() { // handleSchema returns the config JSON schema for UI form generation. // Matching TS config.schema (src/gateway/server-methods/config.ts:276-289). func (m *ConfigMethods) handleSchema(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "agents": map[string]interface{}{ + "properties": map[string]any{ + "agents": map[string]any{ "type": "object", "description": "Agent configuration (defaults + per-agent overrides)", }, - "channels": map[string]interface{}{ + "channels": map[string]any{ "type": "object", "description": "Channel configuration (telegram, discord, slack, etc.)", }, - "providers": map[string]interface{}{ + "providers": map[string]any{ "type": "object", "description": "AI provider API keys and settings", }, - "gateway": map[string]interface{}{ + "gateway": map[string]any{ "type": "object", "description": "Gateway server settings (host, port, token)", }, - "tools": map[string]interface{}{ + "tools": map[string]any{ "type": "object", "description": "Tool configuration (browser, exec, web search)", }, - "sessions": map[string]interface{}{ + "sessions": map[string]any{ "type": "object", "description": "Session storage configuration", }, }, } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "json": schema, })) } diff --git a/internal/gateway/methods/cron.go b/internal/gateway/methods/cron.go index 1968f318..240a4f5d 100644 --- a/internal/gateway/methods/cron.go +++ b/internal/gateway/methods/cron.go @@ -44,7 +44,7 @@ func (m *CronMethods) handleList(_ context.Context, client *gateway.Client, req jobs := m.service.ListJobs(params.IncludeDisabled, "", "") - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "jobs": jobs, "status": m.service.Status(), })) @@ -84,7 +84,7 @@ func (m *CronMethods) handleCreate(ctx context.Context, client *gateway.Client, return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "job": job, })) } @@ -108,7 +108,7 @@ func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client, return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "deleted": true, })) } @@ -133,7 +133,7 @@ func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client, return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "jobId": params.JobID, "enabled": params.Enabled, })) @@ -146,8 +146,8 @@ func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, re func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { locale := store.LocaleFromContext(ctx) var params struct { - JobID string `json:"jobId"` - ID string `json:"id"` // alias (matching TS) + JobID string `json:"jobId"` + ID string `json:"id"` // alias (matching TS) Patch store.CronJobPatch `json:"patch"` } if req.Params != nil { @@ -169,7 +169,7 @@ func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client, return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "job": job, })) } @@ -204,7 +204,7 @@ func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req } // Respond immediately — job execution happens in background - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "ran": true, })) @@ -233,7 +233,7 @@ func (m *CronMethods) handleRuns(_ context.Context, client *gateway.Client, req } entries, total := m.service.GetRunLog(jobID, params.Limit, params.Offset) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "entries": entries, "total": total, })) diff --git a/internal/gateway/methods/delegations.go b/internal/gateway/methods/delegations.go index 14b1811f..32110098 100644 --- a/internal/gateway/methods/delegations.go +++ b/internal/gateway/methods/delegations.go @@ -87,7 +87,7 @@ func (m *DelegationsMethods) handleList(ctx context.Context, client *gateway.Cli } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "records": records, "total": total, })) diff --git a/internal/gateway/methods/exec_approval.go b/internal/gateway/methods/exec_approval.go index 76ffd644..b9658ecc 100644 --- a/internal/gateway/methods/exec_approval.go +++ b/internal/gateway/methods/exec_approval.go @@ -28,7 +28,7 @@ func (m *ExecApprovalMethods) Register(router *gateway.MethodRouter) { func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { if m.manager == nil { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "pending": []any{}, })) return @@ -52,7 +52,7 @@ func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Clie }) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "pending": items, })) } @@ -87,7 +87,7 @@ func (m *ExecApprovalMethods) handleApprove(ctx context.Context, client *gateway return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "resolved": true, "decision": string(decision), })) @@ -117,7 +117,7 @@ func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Cl return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "resolved": true, "decision": "deny", })) diff --git a/internal/gateway/methods/logs.go b/internal/gateway/methods/logs.go index 21675a35..26785625 100644 --- a/internal/gateway/methods/logs.go +++ b/internal/gateway/methods/logs.go @@ -38,13 +38,13 @@ func (m *LogsMethods) handleTail(ctx context.Context, client *gateway.Client, re case "start": level := parseLogLevel(params.Level) m.logTee.Subscribe(client, level) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "status": "tailing", "level": params.Level, })) case "stop": m.logTee.Unsubscribe(client.ID()) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "status": "stopped", })) default: diff --git a/internal/gateway/methods/pairing.go b/internal/gateway/methods/pairing.go index f519832c..c39590bc 100644 --- a/internal/gateway/methods/pairing.go +++ b/internal/gateway/methods/pairing.go @@ -74,7 +74,7 @@ func (m *PairingMethods) handleRequest(ctx context.Context, client *gateway.Clie return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "code": code, })) } @@ -113,7 +113,7 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "approved"})) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "paired": paired, })) } @@ -141,7 +141,7 @@ func (m *PairingMethods) handleDeny(ctx context.Context, client *gateway.Client, m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "denied"})) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "denied": true, })) } @@ -150,7 +150,7 @@ func (m *PairingMethods) handleList(_ context.Context, client *gateway.Client, r pending := m.service.ListPending() paired := m.service.ListPaired() - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "pending": pending, "paired": paired, })) @@ -191,7 +191,7 @@ func (m *PairingMethods) handleRevoke(ctx context.Context, client *gateway.Clien }) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "revoked": true, })) } @@ -213,7 +213,7 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client } if m.service.IsPaired(params.SenderID, "browser") { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "status": "approved", })) return @@ -223,14 +223,14 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client pending := m.service.ListPending() for _, p := range pending { if p.SenderID == params.SenderID && p.Channel == "browser" { - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "status": "pending", })) return } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "status": "expired", })) } diff --git a/internal/gateway/methods/send.go b/internal/gateway/methods/send.go index 589cd25d..99b5304b 100644 --- a/internal/gateway/methods/send.go +++ b/internal/gateway/methods/send.go @@ -55,7 +55,7 @@ func (m *SendMethods) handleSend(ctx context.Context, client *gateway.Client, re Content: params.Message, }) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "channel": params.Channel, "to": params.To, diff --git a/internal/gateway/methods/sessions.go b/internal/gateway/methods/sessions.go index 965d72d0..119705eb 100644 --- a/internal/gateway/methods/sessions.go +++ b/internal/gateway/methods/sessions.go @@ -48,7 +48,7 @@ func (m *SessionsMethods) handleList(_ context.Context, client *gateway.Client, Limit: params.Limit, Offset: params.Offset, }) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "sessions": result.Sessions, "total": result.Total, "limit": params.Limit, @@ -71,7 +71,7 @@ func (m *SessionsMethods) handlePreview(ctx context.Context, client *gateway.Cli history := m.sessions.GetHistory(params.Key) summary := m.sessions.GetSummary(params.Key) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "key": params.Key, "messages": history, "summary": summary, @@ -116,7 +116,7 @@ func (m *SessionsMethods) handlePatch(ctx context.Context, client *gateway.Clien // Save changes to DB m.sessions.Save(params.Key) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "key": params.Key, })) @@ -135,7 +135,7 @@ func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Clie return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, })) } @@ -150,7 +150,7 @@ func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Clien m.sessions.Reset(params.Key) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, })) } diff --git a/internal/gateway/methods/skills.go b/internal/gateway/methods/skills.go index 3332b003..90e7be0b 100644 --- a/internal/gateway/methods/skills.go +++ b/internal/gateway/methods/skills.go @@ -30,9 +30,9 @@ func (m *SkillsMethods) Register(router *gateway.MethodRouter) { func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { allSkills := m.store.ListSkills() - result := make([]map[string]interface{}, 0, len(allSkills)) + result := make([]map[string]any, 0, len(allSkills)) for _, s := range allSkills { - entry := map[string]interface{}{ + entry := map[string]any{ "name": s.Name, "slug": s.Slug, "description": s.Description, @@ -51,7 +51,7 @@ func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, re result = append(result, entry) } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "skills": result, })) } @@ -77,7 +77,7 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r content, _ := m.store.LoadSkill(params.Name) - resp := map[string]interface{}{ + resp := map[string]any{ "name": info.Name, "slug": info.Slug, "description": info.Description, @@ -99,15 +99,15 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r // skillUpdater is an optional interface for stores that support skill updates (e.g. PGSkillStore). type skillUpdater interface { - UpdateSkill(id uuid.UUID, updates map[string]interface{}) error + UpdateSkill(id uuid.UUID, updates map[string]any) error } func (m *SkillsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { locale := store.LocaleFromContext(ctx) var params struct { - Name string `json:"name"` - ID string `json:"id"` - Updates map[string]interface{} `json:"updates"` + Name string `json:"name"` + ID string `json:"id"` + Updates map[string]any `json:"updates"` } if req.Params != nil { json.Unmarshal(req.Params, ¶ms) diff --git a/internal/gateway/methods/teams.go b/internal/gateway/methods/teams.go index cc3af568..276d1244 100644 --- a/internal/gateway/methods/teams.go +++ b/internal/gateway/methods/teams.go @@ -64,7 +64,7 @@ func (m *TeamsMethods) handleList(ctx context.Context, client *gateway.Client, r return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "teams": teams, "count": len(teams), })) @@ -164,7 +164,7 @@ func (m *TeamsMethods) handleCreate(ctx context.Context, client *gateway.Client, } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "team": team, })) diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index 5b572641..daedca73 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -55,7 +55,7 @@ func (m *TeamsMethods) handleGet(ctx context.Context, client *gateway.Client, re return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "team": team, "members": members, })) @@ -107,7 +107,7 @@ func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client, } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit team.deleted event if m.msgBus != nil && team != nil { @@ -157,7 +157,7 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "tasks": tasks, "count": len(tasks), })) @@ -166,8 +166,8 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien // --- Update (settings) --- type teamsUpdateParams struct { - TeamID string `json:"teamId"` - Settings map[string]interface{} `json:"settings"` + TeamID string `json:"teamId"` + Settings map[string]any `json:"settings"` } func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { @@ -225,7 +225,7 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, m.invalidateTeamCaches(ctx, teamID) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit team.updated event if m.msgBus != nil { @@ -280,7 +280,7 @@ func (m *TeamsMethods) handleKnownUsers(ctx context.Context, client *gateway.Cli return } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "users": users, })) } diff --git a/internal/gateway/methods/teams_members.go b/internal/gateway/methods/teams_members.go index 179aeb78..42e9e060 100644 --- a/internal/gateway/methods/teams_members.go +++ b/internal/gateway/methods/teams_members.go @@ -81,7 +81,7 @@ func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Clie // Invalidate caches for all team members m.invalidateTeamCaches(ctx, teamID) - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit team.member.added event if m.msgBus != nil { @@ -170,7 +170,7 @@ func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.C } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true})) + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true})) // Emit team.member.removed event if m.msgBus != nil && removedAgent != nil { diff --git a/internal/gateway/methods/usage.go b/internal/gateway/methods/usage.go index 7cff81f6..c2f705e2 100644 --- a/internal/gateway/methods/usage.go +++ b/internal/gateway/methods/usage.go @@ -83,17 +83,11 @@ func (m *UsageMethods) handleGet(_ context.Context, client *gateway.Client, req total := len(records) // Apply offset + limit - offset := params.Offset - if offset > total { - offset = total - } - end := offset + params.Limit - if end > total { - end = total - } + offset := min(params.Offset, total) + end := min(offset+params.Limit, total) records = records[offset:end] - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "records": records, "total": total, "limit": params.Limit, @@ -132,7 +126,7 @@ func (m *UsageMethods) handleSummary(_ context.Context, client *gateway.Client, totalRecords++ } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "byAgent": byAgent, "totalRecords": totalRecords, })) diff --git a/internal/gateway/router.go b/internal/gateway/router.go index 3a5b8d3a..a64b3fd5 100644 --- a/internal/gateway/router.go +++ b/internal/gateway/router.go @@ -142,12 +142,12 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p client.pairingCode = code client.pairingPending = true // Not authenticated — can only call browser.pairing.status - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "protocol": protocol.ProtocolVersion, "status": "pending_pairing", "pairing_code": code, "sender_id": client.id, - "server": map[string]interface{}{ + "server": map[string]any{ "name": "goclaw", "version": "0.2.0", }, @@ -164,11 +164,11 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p } func (r *MethodRouter) sendConnectResponse(client *Client, reqID string) { - client.SendResponse(protocol.NewOKResponse(reqID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(reqID, map[string]any{ "protocol": protocol.ProtocolVersion, "role": string(client.role), "user_id": client.userID, - "server": map[string]interface{}{ + "server": map[string]any{ "name": "goclaw", "version": "0.2.0", }, @@ -219,14 +219,14 @@ func (r *MethodRouter) handleHealth(ctx context.Context, client *Client, req *pr toolCount = s.tools.Count() } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ - "status": "ok", - "version": s.version, - "uptime": uptimeMs, - "mode": mode, - "database": dbStatus, - "tools": toolCount, - "clients": clientList, + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ + "status": "ok", + "version": s.version, + "uptime": uptimeMs, + "mode": mode, + "database": dbStatus, + "tools": toolCount, + "clients": clientList, "currentId": client.ID(), })) } @@ -248,11 +248,10 @@ func (r *MethodRouter) handleStatus(ctx context.Context, client *Client, req *pr } } - client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "agents": agents, "agentTotal": agentTotal, "clients": len(r.server.clients), "sessions": sessionCount, })) } - diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index f5c84d69..cff0d674 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -2,6 +2,8 @@ // user-facing error messages. Messages use fmt.Sprintf-style templates. package i18n +import "maps" + import "fmt" // Supported locales. @@ -21,9 +23,7 @@ func register(locale string, msgs map[string]string) { if catalogs[locale] == nil { catalogs[locale] = make(map[string]string, len(msgs)) } - for k, v := range msgs { - catalogs[locale][k] = v - } + maps.Copy(catalogs[locale], msgs) } // T returns a localized message for the given key. diff --git a/internal/knowledgegraph/extractor.go b/internal/knowledgegraph/extractor.go index 7f6f05a5..b9fb19af 100644 --- a/internal/knowledgegraph/extractor.go +++ b/internal/knowledgegraph/extractor.go @@ -46,7 +46,7 @@ func (e *Extractor) Extract(ctx context.Context, text string) (*ExtractionResult {Role: "user", Content: text}, }, Model: e.model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 8192, "temperature": 0.0, }, diff --git a/internal/mcp/bridge_tool.go b/internal/mcp/bridge_tool.go index 91593068..2adb4ccf 100644 --- a/internal/mcp/bridge_tool.go +++ b/internal/mcp/bridge_tool.go @@ -19,7 +19,7 @@ type BridgeTool struct { toolName string // original MCP tool name registeredName string // may include prefix: "{prefix}__{toolName}" description string - inputSchema map[string]interface{} // JSON Schema for parameters + inputSchema map[string]any // JSON Schema for parameters client *mcpclient.Client timeoutSec int connected *atomic.Bool @@ -73,9 +73,9 @@ func ensureMCPPrefix(prefix, serverName string) string { return prefix } -func (t *BridgeTool) Name() string { return t.registeredName } -func (t *BridgeTool) Description() string { return t.description } -func (t *BridgeTool) Parameters() map[string]interface{} { return t.inputSchema } +func (t *BridgeTool) Name() string { return t.registeredName } +func (t *BridgeTool) Description() string { return t.description } +func (t *BridgeTool) Parameters() map[string]any { return t.inputSchema } // ServerName returns the name of the MCP server this tool belongs to. func (t *BridgeTool) ServerName() string { return t.serverName } @@ -83,7 +83,7 @@ func (t *BridgeTool) ServerName() string { return t.serverName } // OriginalName returns the original MCP tool name (without prefix). func (t *BridgeTool) OriginalName() string { return t.toolName } -func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BridgeTool) Execute(ctx context.Context, args map[string]any) *tools.Result { if !t.connected.Load() { return tools.ErrorResult(fmt.Sprintf("MCP server %q is disconnected", t.serverName)) } @@ -113,8 +113,8 @@ func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) * } // inputSchemaToMap converts mcp.ToolInputSchema to the map format expected by tools.Tool.Parameters(). -func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]interface{} { - m := map[string]interface{}{ +func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]any { + m := map[string]any{ "type": schema.Type, } if schema.Type == "" { diff --git a/internal/mcp/helpers.go b/internal/mcp/helpers.go index 79699b47..ab24fe78 100644 --- a/internal/mcp/helpers.go +++ b/internal/mcp/helpers.go @@ -3,6 +3,6 @@ package mcp import "encoding/json" // jsonUnmarshal is a thin wrapper to keep the main files import-clean. -func jsonUnmarshal(data []byte, v interface{}) error { +func jsonUnmarshal(data []byte, v any) error { return json.Unmarshal(data, v) } diff --git a/internal/mcp/manager_connect.go b/internal/mcp/manager_connect.go index 1590bc97..9d4816cf 100644 --- a/internal/mcp/manager_connect.go +++ b/internal/mcp/manager_connect.go @@ -269,10 +269,7 @@ func (m *Manager) tryReconnect(ctx context.Context, ss *serverState) { attempt := ss.reconnAttempts ss.mu.Unlock() - backoff := initialBackoff * time.Duration(1<<(attempt-1)) - if backoff > maxBackoff { - backoff = maxBackoff - } + backoff := min(initialBackoff*time.Duration(1<<(attempt-1)), maxBackoff) slog.Info("mcp.server.reconnecting", "server", ss.name, diff --git a/internal/mcp/manager_util.go b/internal/mcp/manager_util.go index 6dee2cc6..a26930ae 100644 --- a/internal/mcp/manager_util.go +++ b/internal/mcp/manager_util.go @@ -1,5 +1,7 @@ package mcp +import "strings" + func mapToEnvSlice(env map[string]string) []string { if len(env) == 0 { return nil @@ -23,14 +25,14 @@ func toSet(items []string) map[string]struct{} { } func joinErrors(errs []string) string { - result := "" + var result strings.Builder for i, e := range errs { if i > 0 { - result += "; " + result.WriteString("; ") } - result += e + result.WriteString(e) } - return result + return result.String() } // jsonBytesToStringSlice converts JSONB []byte to []string. Returns nil on error. diff --git a/internal/mcp/mcp_tool_search.go b/internal/mcp/mcp_tool_search.go index e8dc5b08..44b06f68 100644 --- a/internal/mcp/mcp_tool_search.go +++ b/internal/mcp/mcp_tool_search.go @@ -47,15 +47,15 @@ func (t *MCPToolSearchTool) Description() string { "Discovered tools become immediately available for use." } -func (t *MCPToolSearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *MCPToolSearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "English keywords describing the operation you need (e.g. 'create github issue', 'query postgres', 'send slack message')", }, - "max_results": map[string]interface{}{ + "max_results": map[string]any{ "type": "integer", "description": "Maximum number of tools to return (default: 5)", }, @@ -64,7 +64,7 @@ func (t *MCPToolSearchTool) Parameters() map[string]interface{} { } } -func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]any) *tools.Result { query, _ := args["query"].(string) if query == "" { return tools.ErrorResult("query parameter is required") @@ -91,7 +91,7 @@ func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interfa } t.manager.ActivateTools(names) - data, _ := json.MarshalIndent(map[string]interface{}{ + data, _ := json.MarshalIndent(map[string]any{ "tools": results, "count": len(results), }, "", " ") diff --git a/internal/memory/embeddings.go b/internal/memory/embeddings.go index 172bb1a1..5ce0f3a5 100644 --- a/internal/memory/embeddings.go +++ b/internal/memory/embeddings.go @@ -128,7 +128,7 @@ func (p *OpenAIEmbeddingProvider) Name() string { return p.name } func (p *OpenAIEmbeddingProvider) Model() string { return p.model } func (p *OpenAIEmbeddingProvider) Embed(ctx context.Context, texts []string) ([][]float32, error) { - reqBody := map[string]interface{}{ + reqBody := map[string]any{ "input": texts, "model": p.model, } diff --git a/internal/permissions/policy.go b/internal/permissions/policy.go index 8529649f..631f2c7e 100644 --- a/internal/permissions/policy.go +++ b/internal/permissions/policy.go @@ -12,6 +12,7 @@ package permissions import ( + "slices" "strings" "sync" @@ -90,25 +91,15 @@ func (pe *PolicyEngine) CanAccessWithScopes(scopes []Scope, method string) bool // RoleFromScopes determines the effective role from a set of scopes. func RoleFromScopes(scopes []Scope) Role { - for _, s := range scopes { - if s == ScopeAdmin { - return RoleAdmin - } - } - hasWrite := false - for _, s := range scopes { - if s == ScopeWrite { - hasWrite = true - break - } + if slices.Contains(scopes, ScopeAdmin) { + return RoleAdmin } + hasWrite := slices.Contains(scopes, ScopeWrite) if hasWrite { return RoleOperator } - for _, s := range scopes { - if s == ScopeRead { - return RoleViewer - } + if slices.Contains(scopes, ScopeRead) { + return RoleViewer } return RoleViewer } @@ -166,12 +157,7 @@ func isAdminMethod(method string) bool { protocol.MethodTeamsDelete, protocol.MethodTeamsTaskList, } - for _, m := range adminMethods { - if method == m { - return true - } - } - return false + return slices.Contains(adminMethods, method) } func isWriteMethod(method string) bool { diff --git a/internal/providers/anthropic.go b/internal/providers/anthropic.go index c3c6fff2..48777fcb 100644 --- a/internal/providers/anthropic.go +++ b/internal/providers/anthropic.go @@ -12,9 +12,9 @@ import ( ) const ( - defaultClaudeModel = "claude-sonnet-4-5-20250929" - anthropicAPIBase = "https://api.anthropic.com/v1" - anthropicAPIVersion = "2023-06-01" + defaultClaudeModel = "claude-sonnet-4-5-20250929" + anthropicAPIBase = "https://api.anthropic.com/v1" + anthropicAPIVersion = "2023-06-01" ) // AnthropicProvider implements Provider using the Anthropic Claude API via net/http. @@ -55,9 +55,9 @@ func WithAnthropicBaseURL(baseURL string) AnthropicOption { } } -func (p *AnthropicProvider) Name() string { return "anthropic" } -func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel } -func (p *AnthropicProvider) SupportsThinking() bool { return true } +func (p *AnthropicProvider) Name() string { return "anthropic" } +func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel } +func (p *AnthropicProvider) SupportsThinking() bool { return true } func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) { model := req.Model @@ -83,7 +83,7 @@ func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatRes }) } -func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) { +func (p *AnthropicProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) { data, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("anthropic: marshal request: %w", err) @@ -99,7 +99,7 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io httpReq.Header.Set("anthropic-version", anthropicAPIVersion) // Add beta header for interleaved thinking when thinking is enabled - if bodyMap, ok := body.(map[string]interface{}); ok { + if bodyMap, ok := body.(map[string]any); ok { if _, hasThinking := bodyMap["thinking"]; hasThinking { httpReq.Header.Set("anthropic-beta", "interleaved-thinking-2025-05-14") } @@ -138,7 +138,7 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse case "redacted_thinking": // Encrypted thinking — cannot display but must preserve for passback case "tool_use": - args := make(map[string]interface{}) + args := make(map[string]any) _ = json.Unmarshal(block.Input, &args) result.ToolCalls = append(result.ToolCalls, ToolCall{ ID: block.ID, @@ -182,8 +182,8 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse type anthropicResponse struct { Content []anthropicContentBlock `json:"content"` - StopReason string `json:"stop_reason"` - Usage anthropicUsage `json:"usage"` + StopReason string `json:"stop_reason"` + Usage anthropicUsage `json:"usage"` } type anthropicContentBlock struct { @@ -221,8 +221,8 @@ type anthropicContentBlockDeltaEvent struct { Delta struct { Type string `json:"type"` Text string `json:"text,omitempty"` - Thinking string `json:"thinking,omitempty"` // for thinking_delta - Signature string `json:"signature,omitempty"` // for signature_delta + Thinking string `json:"thinking,omitempty"` // for thinking_delta + Signature string `json:"signature,omitempty"` // for signature_delta PartialJSON string `json:"partial_json,omitempty"` } `json:"delta"` } diff --git a/internal/providers/anthropic_request.go b/internal/providers/anthropic_request.go index 62d7652a..f8259ed8 100644 --- a/internal/providers/anthropic_request.go +++ b/internal/providers/anthropic_request.go @@ -7,7 +7,7 @@ import "encoding/json" func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse, toolCallJSON map[int]string, _ int) json.RawMessage { switch blockType { case "thinking": - block := map[string]interface{}{ + block := map[string]any{ "type": "thinking", "thinking": result.Thinking, } @@ -15,7 +15,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse return b } case "text": - block := map[string]interface{}{ + block := map[string]any{ "type": "text", "text": result.Content, } @@ -26,13 +26,13 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse if len(result.ToolCalls) > 0 { tc := result.ToolCalls[len(result.ToolCalls)-1] // Parse accumulated JSON for this tool call - args := make(map[string]interface{}) + args := make(map[string]any) for i, rawJSON := range toolCallJSON { if i == len(result.ToolCalls)-1 && rawJSON != "" { _ = json.Unmarshal([]byte(rawJSON), &args) } } - block := map[string]interface{}{ + block := map[string]any{ "type": "tool_use", "id": tc.ID, "name": tc.Name, @@ -44,7 +44,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse } case "redacted_thinking": // Pass through as-is (we don't have the encrypted data in streaming) - block := map[string]interface{}{ + block := map[string]any{ "type": "redacted_thinking", } if b, err := json.Marshal(block); err == nil { @@ -54,26 +54,26 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse return nil } -func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} { +func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any { // Separate system messages and build conversation messages - var systemBlocks []map[string]interface{} - var messages []map[string]interface{} + var systemBlocks []map[string]any + var messages []map[string]any for _, msg := range req.Messages { switch msg.Role { case "system": - systemBlocks = append(systemBlocks, map[string]interface{}{ + systemBlocks = append(systemBlocks, map[string]any{ "type": "text", "text": msg.Content, }) case "user": if len(msg.Images) > 0 { - var blocks []map[string]interface{} + var blocks []map[string]any for _, img := range msg.Images { - blocks = append(blocks, map[string]interface{}{ + blocks = append(blocks, map[string]any{ "type": "image", - "source": map[string]interface{}{ + "source": map[string]any{ "type": "base64", "media_type": img.MimeType, "data": img.Data, @@ -81,17 +81,17 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre }) } if msg.Content != "" { - blocks = append(blocks, map[string]interface{}{ + blocks = append(blocks, map[string]any{ "type": "text", "text": msg.Content, }) } - messages = append(messages, map[string]interface{}{ + messages = append(messages, map[string]any{ "role": "user", "content": blocks, }) } else { - messages = append(messages, map[string]interface{}{ + messages = append(messages, map[string]any{ "role": "user", "content": msg.Content, }) @@ -103,7 +103,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre if msg.RawAssistantContent != nil { var rawBlocks []json.RawMessage if json.Unmarshal(msg.RawAssistantContent, &rawBlocks) == nil && len(rawBlocks) > 0 { - messages = append(messages, map[string]interface{}{ + messages = append(messages, map[string]any{ "role": "assistant", "content": rawBlocks, }) @@ -111,30 +111,30 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre } } - var blocks []map[string]interface{} + var blocks []map[string]any if msg.Content != "" { - blocks = append(blocks, map[string]interface{}{ + blocks = append(blocks, map[string]any{ "type": "text", "text": msg.Content, }) } for _, tc := range msg.ToolCalls { - blocks = append(blocks, map[string]interface{}{ + blocks = append(blocks, map[string]any{ "type": "tool_use", "id": tc.ID, "name": tc.Name, "input": tc.Arguments, }) } - messages = append(messages, map[string]interface{}{ + messages = append(messages, map[string]any{ "role": "assistant", "content": blocks, }) case "tool": - messages = append(messages, map[string]interface{}{ + messages = append(messages, map[string]any{ "role": "user", - "content": []map[string]interface{}{ + "content": []map[string]any{ { "type": "tool_result", "tool_use_id": msg.ToolCallID, @@ -145,11 +145,11 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre } } - body := map[string]interface{}{ + body := map[string]any{ "model": model, "max_tokens": 4096, "messages": messages, - "cache_control": map[string]interface{}{"type": "ephemeral"}, + "cache_control": map[string]any{"type": "ephemeral"}, } if stream { @@ -162,10 +162,10 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre // Translate tools to Anthropic format if len(req.Tools) > 0 { - var tools []map[string]interface{} + var tools []map[string]any for _, t := range req.Tools { cleanedParams := CleanSchemaForProvider("anthropic", t.Function.Parameters) - tool := map[string]interface{}{ + tool := map[string]any{ "name": t.Function.Name, "description": t.Function.Description, "input_schema": cleanedParams, @@ -186,7 +186,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre // Enable extended thinking if thinking_level is set if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" { budget := anthropicThinkingBudget(level) - body["thinking"] = map[string]interface{}{ + body["thinking"] = map[string]any{ "type": "enabled", "budget_tokens": budget, } diff --git a/internal/providers/anthropic_stream.go b/internal/providers/anthropic_stream.go index dc651eed..27c6eb8d 100644 --- a/internal/providers/anthropic_stream.go +++ b/internal/providers/anthropic_stream.go @@ -44,8 +44,8 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC line := scanner.Text() // Track event type - if strings.HasPrefix(line, "event: ") { - currentEvent = strings.TrimPrefix(line, "event: ") + if after, ok := strings.CutPrefix(line, "event: "); ok { + currentEvent = after continue } @@ -77,7 +77,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC result.ToolCalls = append(result.ToolCalls, ToolCall{ ID: ev.ContentBlock.ID, Name: strings.TrimSpace(ev.ContentBlock.Name), - Arguments: make(map[string]interface{}), + Arguments: make(map[string]any), }) } // Store raw content_block for later reconstruction @@ -155,7 +155,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC // Parse accumulated tool call JSON arguments for i, rawJSON := range toolCallJSON { if rawJSON != "" { - args := make(map[string]interface{}) + args := make(map[string]any) _ = json.Unmarshal([]byte(rawJSON), &args) result.ToolCalls[i].Arguments = args } diff --git a/internal/providers/claude_cli_hooks.go b/internal/providers/claude_cli_hooks.go index 83c986fc..004c4192 100644 --- a/internal/providers/claude_cli_hooks.go +++ b/internal/providers/claude_cli_hooks.go @@ -46,30 +46,30 @@ func BuildCLIHooksConfig(workspace string, restrictToWorkspace bool) (string, fu // generateSettingsJSON creates Claude CLI settings with PreToolUse hooks. func generateSettingsJSON(hookPath string) []byte { - settings := map[string]interface{}{ - "hooks": map[string]interface{}{ - "PreToolUse": []map[string]interface{}{ + settings := map[string]any{ + "hooks": map[string]any{ + "PreToolUse": []map[string]any{ { "matcher": "Bash", - "hooks": []map[string]interface{}{ + "hooks": []map[string]any{ {"type": "command", "command": hookPath}, }, }, { "matcher": "Write", - "hooks": []map[string]interface{}{ + "hooks": []map[string]any{ {"type": "command", "command": hookPath}, }, }, { "matcher": "Edit", - "hooks": []map[string]interface{}{ + "hooks": []map[string]any{ {"type": "command", "command": hookPath}, }, }, { "matcher": "Read", - "hooks": []map[string]interface{}{ + "hooks": []map[string]any{ {"type": "command", "command": hookPath}, }, }, diff --git a/internal/providers/claude_cli_mcp.go b/internal/providers/claude_cli_mcp.go index ff8966ea..c9d01d66 100644 --- a/internal/providers/claude_cli_mcp.go +++ b/internal/providers/claude_cli_mcp.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" @@ -33,9 +34,9 @@ type MCPServerLookup func(ctx context.Context, agentID string) []MCPServerEntry // MCPConfigData holds the base MCP server entries built at startup. // Per-session configs are written via WriteMCPConfig with agent context injected. type MCPConfigData struct { - Servers map[string]interface{} // external MCP server entries (stdio/sse/http) - GatewayAddr string - GatewayToken string + Servers map[string]any // external MCP server entries (stdio/sse/http) + GatewayAddr string + GatewayToken string AgentMCPLookup MCPServerLookup // optional: resolves per-agent MCP servers from DB } @@ -43,7 +44,7 @@ type MCPConfigData struct { // Does NOT include the goclaw-bridge entry — that's added per-session // with agent context headers in WriteMCPConfig. func BuildCLIMCPConfigData(servers map[string]*config.MCPServerConfig, gatewayAddr string, gatewayToken ...string) *MCPConfigData { - mcpServers := make(map[string]interface{}, len(servers)) + mcpServers := make(map[string]any, len(servers)) for name, srv := range servers { if !srv.IsEnabled() { @@ -109,10 +110,8 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey, // Shallow-copy the outer map so we can add the bridge entry without mutating the shared base. // Inner server entries are not modified, so shallow copy is sufficient. - servers := make(map[string]interface{}, len(d.Servers)+1) - for k, v := range d.Servers { - servers[k] = v - } + servers := make(map[string]any, len(d.Servers)+1) + maps.Copy(servers, d.Servers) // Inject per-agent MCP servers from DB (if lookup is configured and agentID is set) if d.AgentMCPLookup != nil && agentID != "" { @@ -153,7 +152,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey, headers["X-Bridge-Sig"] = SignBridgeContext(d.GatewayToken, agentID, userID, channel, chatID, peerKind) } - bridgeEntry := map[string]interface{}{ + bridgeEntry := map[string]any{ "url": fmt.Sprintf("http://%s/mcp/bridge", d.GatewayAddr), "type": "http", } @@ -167,7 +166,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey, return "" } - data, err := json.MarshalIndent(map[string]interface{}{"mcpServers": servers}, "", " ") + data, err := json.MarshalIndent(map[string]any{"mcpServers": servers}, "", " ") if err != nil { slog.Warn("claude-cli: failed to marshal mcp config", "error", err) return "" @@ -202,10 +201,9 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey, return path } - // mcpServerEntryToConfig converts an MCPServerEntry to the CLI MCP config format. -func mcpServerEntryToConfig(srv MCPServerEntry) map[string]interface{} { - entry := make(map[string]interface{}) +func mcpServerEntryToConfig(srv MCPServerEntry) map[string]any { + entry := make(map[string]any) switch srv.Transport { case "stdio": if srv.Command != "" { diff --git a/internal/providers/claude_cli_parse.go b/internal/providers/claude_cli_parse.go index 9caa07e1..37fa97cd 100644 --- a/internal/providers/claude_cli_parse.go +++ b/internal/providers/claude_cli_parse.go @@ -15,7 +15,7 @@ func parseJSONResponse(data []byte) (*ChatResponse, error) { } // Fallback: CLI may output one JSON object per line. - for _, line := range bytes.Split(data, []byte("\n")) { + for line := range bytes.SplitSeq(data, []byte("\n")) { line = bytes.TrimSpace(line) if len(line) == 0 { continue diff --git a/internal/providers/claude_cli_session.go b/internal/providers/claude_cli_session.go index ecd8fcfb..9db91532 100644 --- a/internal/providers/claude_cli_session.go +++ b/internal/providers/claude_cli_session.go @@ -131,7 +131,7 @@ func extractFromMessages(msgs []Message) (systemPrompt, userMsg string, images [ } // extractStringOpt gets a string value from Options map by key. -func extractStringOpt(opts map[string]interface{}, key string) string { +func extractStringOpt(opts map[string]any, key string) string { if opts == nil { return "" } @@ -144,7 +144,7 @@ func extractStringOpt(opts map[string]interface{}, key string) string { } // extractBoolOpt gets a bool value from Options map by key. -func extractBoolOpt(opts map[string]interface{}, key string) bool { +func extractBoolOpt(opts map[string]any, key string) bool { if opts == nil { return false } @@ -157,7 +157,7 @@ func extractBoolOpt(opts map[string]interface{}, key string) bool { } // bridgeContextFromOpts builds a BridgeContext from the Options map. -func bridgeContextFromOpts(opts map[string]interface{}) BridgeContext { +func bridgeContextFromOpts(opts map[string]any) BridgeContext { return BridgeContext{ AgentID: extractStringOpt(opts, OptAgentID), UserID: extractStringOpt(opts, OptUserID), @@ -209,12 +209,12 @@ func sessionFileExists(workDir string, sessionID uuid.UUID) bool { // buildStreamJSONInput creates stream-json stdin for vision (images + text). func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader { - var contentBlocks []map[string]interface{} + var contentBlocks []map[string]any for _, img := range images { - contentBlocks = append(contentBlocks, map[string]interface{}{ + contentBlocks = append(contentBlocks, map[string]any{ "type": "image", - "source": map[string]interface{}{ + "source": map[string]any{ "type": "base64", "media_type": img.MimeType, "data": img.Data, @@ -223,15 +223,15 @@ func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader { } if text != "" { - contentBlocks = append(contentBlocks, map[string]interface{}{ + contentBlocks = append(contentBlocks, map[string]any{ "type": "text", "text": text, }) } - msg := map[string]interface{}{ + msg := map[string]any{ "type": "user", - "message": map[string]interface{}{ + "message": map[string]any{ "role": "user", "content": contentBlocks, }, @@ -246,8 +246,8 @@ func filterCLIEnv(environ []string) []string { var filtered []string for _, e := range environ { key := e - if idx := strings.IndexByte(e, '='); idx >= 0 { - key = e[:idx] + if before, _, ok := strings.Cut(e, "="); ok { + key = before } // Filter out variables that could cause nested CLI conflicts if strings.HasPrefix(key, "CLAUDE") { diff --git a/internal/providers/codex.go b/internal/providers/codex.go index 744a6c76..9df7e43e 100644 --- a/internal/providers/codex.go +++ b/internal/providers/codex.go @@ -44,7 +44,7 @@ func NewCodexProvider(name string, tokenSource TokenSource, apiBase, defaultMode } } -func (p *CodexProvider) Name() string { return p.name } +func (p *CodexProvider) Name() string { return p.name } func (p *CodexProvider) DefaultModel() string { return p.defaultModel } func (p *CodexProvider) SupportsThinking() bool { return true } @@ -97,7 +97,7 @@ func (p *CodexProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk if acc.name == "" { continue } - args := make(map[string]interface{}) + args := make(map[string]any) _ = json.Unmarshal([]byte(acc.rawArgs), &args) result.ToolCalls = append(result.ToolCalls, ToolCall{ ID: acc.callID, diff --git a/internal/providers/codex_build.go b/internal/providers/codex_build.go index b415129f..94eeb950 100644 --- a/internal/providers/codex_build.go +++ b/internal/providers/codex_build.go @@ -11,14 +11,14 @@ import ( ) // buildRequestBody converts internal ChatRequest to Responses API format. -func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]interface{} { +func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]any { model := req.Model if model == "" { model = p.defaultModel } var instructions string - var input []interface{} + var input []any for _, m := range req.Messages { switch m.Role { @@ -31,25 +31,25 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin case "user": if len(m.Images) > 0 { - var parts []map[string]interface{} + var parts []map[string]any for _, img := range m.Images { - parts = append(parts, map[string]interface{}{ + parts = append(parts, map[string]any{ "type": "input_image", "image_url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data), }) } if m.Content != "" { - parts = append(parts, map[string]interface{}{ + parts = append(parts, map[string]any{ "type": "input_text", "text": m.Content, }) } - input = append(input, map[string]interface{}{ + input = append(input, map[string]any{ "role": "user", "content": parts, }) } else { - input = append(input, map[string]interface{}{ + input = append(input, map[string]any{ "role": "user", "content": m.Content, }) @@ -60,7 +60,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin for _, tc := range m.ToolCalls { argsJSON, _ := json.Marshal(tc.Arguments) callID := toFcID(tc.ID) - input = append(input, map[string]interface{}{ + input = append(input, map[string]any{ "type": "function_call", "id": callID, "call_id": callID, @@ -70,10 +70,10 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin } } if m.Content != "" { - item := map[string]interface{}{ + item := map[string]any{ "type": "message", "role": "assistant", - "content": []map[string]interface{}{ + "content": []map[string]any{ {"type": "output_text", "text": m.Content}, }, } @@ -84,7 +84,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin } case "tool": - input = append(input, map[string]interface{}{ + input = append(input, map[string]any{ "type": "function_call_output", "call_id": toFcID(m.ToolCallID), "output": m.Content, @@ -92,7 +92,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin } } - body := map[string]interface{}{ + body := map[string]any{ "model": model, "input": input, "stream": stream, @@ -105,9 +105,9 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin body["instructions"] = instructions if len(req.Tools) > 0 { - var tools []map[string]interface{} + var tools []map[string]any for _, t := range req.Tools { - tools = append(tools, map[string]interface{}{ + tools = append(tools, map[string]any{ "type": "function", "name": t.Function.Name, "description": t.Function.Description, @@ -118,13 +118,13 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin } if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" { - body["reasoning"] = map[string]interface{}{"effort": level} + body["reasoning"] = map[string]any{"effort": level} } return body } -func (p *CodexProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) { +func (p *CodexProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) { data, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("%s: marshal request: %w", p.name, err) diff --git a/internal/providers/codex_test.go b/internal/providers/codex_test.go index b7c1d60d..0dbd3e09 100644 --- a/internal/providers/codex_test.go +++ b/internal/providers/codex_test.go @@ -20,7 +20,7 @@ func (s *staticTokenSource) Token() (string, error) { } // mustJSON marshals v to JSON string; panics on error. -func mustJSON(v interface{}) string { +func mustJSON(v any) string { b, err := json.Marshal(v) if err != nil { panic(err) @@ -68,7 +68,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) { {Role: "assistant", Content: "Hi there!"}, {Role: "user", Content: "How are you?"}, }, - Options: map[string]interface{}{ + Options: map[string]any{ OptMaxTokens: 1024, OptTemperature: 0.7, }, @@ -97,7 +97,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) { } // Check input items (should exclude system messages) - input, ok := body["input"].([]interface{}) + input, ok := body["input"].([]any) if !ok { t.Fatalf("input is not []interface{}: %T", body["input"]) } @@ -124,10 +124,10 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) { Function: ToolFunctionSchema{ Name: "get_weather", Description: "Get current weather", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "city": map[string]interface{}{"type": "string"}, + "properties": map[string]any{ + "city": map[string]any{"type": "string"}, }, "required": []string{"city"}, }, @@ -138,7 +138,7 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) { body := p.buildRequestBody(req, true) - tools, ok := body["tools"].([]map[string]interface{}) + tools, ok := body["tools"].([]map[string]any) if !ok { t.Fatalf("tools is not []map[string]interface{}: %T", body["tools"]) } @@ -167,7 +167,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) { { Role: "assistant", ToolCalls: []ToolCall{ - {ID: "call_123", Name: "get_weather", Arguments: map[string]interface{}{"city": "London"}}, + {ID: "call_123", Name: "get_weather", Arguments: map[string]any{"city": "London"}}, }, }, {Role: "tool", ToolCallID: "call_123", Content: `{"temp": 15}`}, @@ -176,7 +176,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) { body := p.buildRequestBody(req, false) - input, ok := body["input"].([]interface{}) + input, ok := body["input"].([]any) if !ok { t.Fatalf("input is not []interface{}: %T", body["input"]) } @@ -185,7 +185,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) { } // Second item should be function_call - fc, ok := input[1].(map[string]interface{}) + fc, ok := input[1].(map[string]any) if !ok { t.Fatalf("input[1] is not map: %T", input[1]) } @@ -197,7 +197,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) { } // Third item should be function_call_output - fco, ok := input[2].(map[string]interface{}) + fco, ok := input[2].(map[string]any) if !ok { t.Fatalf("input[2] is not map: %T", input[2]) } @@ -214,12 +214,12 @@ func TestCodexProviderBuildRequestBodyThinking(t *testing.T) { req := ChatRequest{ Messages: []Message{{Role: "user", Content: "Think about this"}}, - Options: map[string]interface{}{OptThinkingLevel: "high"}, + Options: map[string]any{OptThinkingLevel: "high"}, } body := p.buildRequestBody(req, false) - reasoning, ok := body["reasoning"].(map[string]interface{}) + reasoning, ok := body["reasoning"].(map[string]any) if !ok { t.Fatalf("reasoning is not map: %T", body["reasoning"]) } @@ -245,7 +245,7 @@ func TestCodexProviderChat(t *testing.T) { } // Verify request body - var body map[string]interface{} + var body map[string]any json.NewDecoder(r.Body).Decode(&body) if body["model"] != "gpt-4o" { t.Errorf("request model = %v, want gpt-4o", body["model"]) @@ -599,17 +599,17 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) { body := p.buildRequestBody(req, false) - input, ok := body["input"].([]interface{}) + input, ok := body["input"].([]any) if !ok || len(input) != 1 { t.Fatalf("expected 1 input item, got %v", body["input"]) } - item, ok := input[0].(map[string]interface{}) + item, ok := input[0].(map[string]any) if !ok { t.Fatalf("input[0] is not map: %T", input[0]) } - content, ok := item["content"].([]map[string]interface{}) + content, ok := item["content"].([]map[string]any) if !ok { t.Fatalf("content is not []map: %T", item["content"]) } @@ -625,4 +625,3 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) { t.Errorf("content[1] type = %v, want input_text", content[1]["type"]) } } - diff --git a/internal/providers/dashscope.go b/internal/providers/dashscope.go index 68203a3b..f9e00568 100644 --- a/internal/providers/dashscope.go +++ b/internal/providers/dashscope.go @@ -3,6 +3,7 @@ package providers import ( "context" "log/slog" + "maps" ) const ( @@ -29,7 +30,7 @@ func NewDashScopeProvider(apiKey, apiBase, defaultModel string) *DashScopeProvid } } -func (p *DashScopeProvider) Name() string { return "dashscope" } +func (p *DashScopeProvider) Name() string { return "dashscope" } func (p *DashScopeProvider) SupportsThinking() bool { return true } // ChatStream handles DashScope's limitation: tools + streaming cannot coexist. @@ -39,10 +40,8 @@ func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onC // Map thinking_level to DashScope-specific params before passing to OpenAI base if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" { // Clone Options to avoid mutating caller's map - opts := make(map[string]interface{}, len(req.Options)+2) - for k, v := range req.Options { - opts[k] = v - } + opts := make(map[string]any, len(req.Options)+2) + maps.Copy(opts, req.Options) opts[OptEnableThinking] = true opts[OptThinkingBudget] = dashscopeThinkingBudget(level) delete(opts, OptThinkingLevel) // don't pass generic key to OpenAI buildRequestBody diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 82343f87..f1b42ce5 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -47,11 +47,11 @@ func (p *OpenAIProvider) WithChatPath(path string) *OpenAIProvider { return p } -func (p *OpenAIProvider) Name() string { return p.name } -func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel } -func (p *OpenAIProvider) SupportsThinking() bool { return true } -func (p *OpenAIProvider) APIKey() string { return p.apiKey } -func (p *OpenAIProvider) APIBase() string { return p.apiBase } +func (p *OpenAIProvider) Name() string { return p.name } +func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel } +func (p *OpenAIProvider) SupportsThinking() bool { return true } +func (p *OpenAIProvider) APIKey() string { return p.apiKey } +func (p *OpenAIProvider) APIBase() string { return p.apiBase } // resolveModel returns the model ID to use for a request. // For OpenRouter, model IDs require a provider prefix (e.g. "anthropic/claude-sonnet-4-5-20250929"). @@ -189,7 +189,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun // Parse accumulated tool call arguments for i := 0; i < len(accumulators); i++ { acc := accumulators[i] - args := make(map[string]interface{}) + args := make(map[string]any) _ = json.Unmarshal([]byte(acc.rawArgs), &args) acc.Arguments = args if acc.thoughtSig != "" { @@ -209,7 +209,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun return result, nil } -func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} { +func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any { // Gemini 2.5+: collapse tool_call cycles missing thought_signature. // Gemini requires thought_signature echoed back on every tool_call; models that // don't return it (e.g. gemini-3-flash) will cause HTTP 400 if sent as-is. @@ -223,9 +223,9 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream // This is necessary because our internal Message/ToolCall structs don't match // the OpenAI API format (tool_calls need type+function wrapper, arguments as JSON string). // Also omits empty content on assistant messages with tool_calls (Gemini compatibility). - msgs := make([]map[string]interface{}, 0, len(inputMessages)) + msgs := make([]map[string]any, 0, len(inputMessages)) for _, m := range inputMessages { - msg := map[string]interface{}{ + msg := map[string]any{ "role": m.Role, } @@ -237,17 +237,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream // Include content; omit empty content for assistant messages with tool_calls // (Gemini rejects empty content → "must include at least one parts field"). if m.Role == "user" && len(m.Images) > 0 { - var parts []map[string]interface{} + var parts []map[string]any for _, img := range m.Images { - parts = append(parts, map[string]interface{}{ + parts = append(parts, map[string]any{ "type": "image_url", - "image_url": map[string]interface{}{ + "image_url": map[string]any{ "url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data), }, }) } if m.Content != "" { - parts = append(parts, map[string]interface{}{ + parts = append(parts, map[string]any{ "type": "text", "text": m.Content, }) @@ -260,17 +260,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream // Convert tool_calls to OpenAI wire format: // {id, type: "function", function: {name, arguments: ""}} if len(m.ToolCalls) > 0 { - toolCalls := make([]map[string]interface{}, len(m.ToolCalls)) + toolCalls := make([]map[string]any, len(m.ToolCalls)) for i, tc := range m.ToolCalls { argsJSON, _ := json.Marshal(tc.Arguments) - fn := map[string]interface{}{ + fn := map[string]any{ "name": tc.Name, "arguments": string(argsJSON), } if sig := tc.Metadata["thought_signature"]; sig != "" { fn["thought_signature"] = sig } - toolCalls[i] = map[string]interface{}{ + toolCalls[i] = map[string]any{ "id": tc.ID, "type": "function", "function": fn, @@ -286,7 +286,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream msgs = append(msgs, msg) } - body := map[string]interface{}{ + body := map[string]any{ "model": model, "messages": msgs, "stream": stream, @@ -298,7 +298,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream } if stream { - body["stream_options"] = map[string]interface{}{ + body["stream_options"] = map[string]any{ "include_usage": true, } } @@ -327,7 +327,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream return body } -func (p *OpenAIProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) { +func (p *OpenAIProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) { data, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("%s: marshal request: %w", p.name, err) @@ -370,7 +370,7 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse { result.FinishReason = resp.Choices[0].FinishReason for _, tc := range msg.ToolCalls { - args := make(map[string]interface{}) + args := make(map[string]any) _ = json.Unmarshal([]byte(tc.Function.Arguments), &args) call := ToolCall{ ID: tc.ID, @@ -404,4 +404,3 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse { return result } - diff --git a/internal/providers/schema_cleaner.go b/internal/providers/schema_cleaner.go index b033ab5d..9b3743e2 100644 --- a/internal/providers/schema_cleaner.go +++ b/internal/providers/schema_cleaner.go @@ -1,5 +1,7 @@ package providers +import "slices" + import "strings" // Unsupported schema keys by provider. @@ -34,7 +36,7 @@ func CleanToolSchemas(providerName string, tools []ToolDefinition) []ToolDefinit } // CleanSchemaForProvider cleans a single parameters map for a provider. -func CleanSchemaForProvider(providerName string, params map[string]interface{}) map[string]interface{} { +func CleanSchemaForProvider(providerName string, params map[string]any) map[string]any { removeKeys := unsupportedKeysForProvider(providerName) if removeKeys == nil { return params @@ -54,21 +56,21 @@ func unsupportedKeysForProvider(name string) []string { } // cleanSchema recursively removes unsupported keys from a JSON Schema map. -func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string]interface{} { +func cleanSchema(schema map[string]any, removeKeys []string) map[string]any { if schema == nil { return nil } - result := make(map[string]interface{}, len(schema)) + result := make(map[string]any, len(schema)) for k, v := range schema { if shouldRemoveKey(k, removeKeys) { continue } switch val := v.(type) { - case map[string]interface{}: + case map[string]any: result[k] = cleanSchema(val, removeKeys) - case []interface{}: + case []any: result[k] = cleanSchemaSlice(val, removeKeys) default: result[k] = v @@ -78,10 +80,10 @@ func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string] } // cleanSchemaSlice recurses into arrays (e.g. "anyOf", "oneOf", "allOf"). -func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} { - result := make([]interface{}, len(items)) +func cleanSchemaSlice(items []any, removeKeys []string) []any { + result := make([]any, len(items)) for i, item := range items { - if m, ok := item.(map[string]interface{}); ok { + if m, ok := item.(map[string]any); ok { result[i] = cleanSchema(m, removeKeys) } else { result[i] = item @@ -91,10 +93,5 @@ func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} { } func shouldRemoveKey(key string, removeKeys []string) bool { - for _, rk := range removeKeys { - if key == rk { - return true - } - } - return false + return slices.Contains(removeKeys, key) } diff --git a/internal/providers/schema_cleaner_test.go b/internal/providers/schema_cleaner_test.go index ea163e5e..534a2a3c 100644 --- a/internal/providers/schema_cleaner_test.go +++ b/internal/providers/schema_cleaner_test.go @@ -10,17 +10,17 @@ func TestCleanToolSchemas_Gemini(t *testing.T) { Function: ToolFunctionSchema{ Name: "test", Description: "desc", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ + "properties": map[string]any{ + "name": map[string]any{ "type": "string", "default": "world", }, }, - "$defs": map[string]interface{}{"Foo": "bar"}, + "$defs": map[string]any{"Foo": "bar"}, "additionalProperties": false, - "examples": []interface{}{"a"}, + "examples": []any{"a"}, }, }, }} @@ -43,8 +43,8 @@ func TestCleanToolSchemas_Gemini(t *testing.T) { } // Nested "default" should be removed - props := params["properties"].(map[string]interface{}) - nameSchema := props["name"].(map[string]interface{}) + props := params["properties"].(map[string]any) + nameSchema := props["name"].(map[string]any) if _, ok := nameSchema["default"]; ok { t.Error("expected nested 'default' to be removed for gemini") } @@ -54,15 +54,15 @@ func TestCleanToolSchemas_Gemini(t *testing.T) { } func TestCleanToolSchemas_Anthropic(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "url": map[string]interface{}{ + "properties": map[string]any{ + "url": map[string]any{ "type": "string", "$ref": "#/$defs/URL", }, }, - "$defs": map[string]interface{}{"URL": "..."}, + "$defs": map[string]any{"URL": "..."}, "additionalProperties": false, "default": "x", } @@ -73,8 +73,8 @@ func TestCleanToolSchemas_Anthropic(t *testing.T) { if _, ok := cleaned["$defs"]; ok { t.Error("expected $defs removed for anthropic") } - props := cleaned["properties"].(map[string]interface{}) - urlSchema := props["url"].(map[string]interface{}) + props := cleaned["properties"].(map[string]any) + urlSchema := props["url"].(map[string]any) if _, ok := urlSchema["$ref"]; ok { t.Error("expected nested $ref removed for anthropic") } @@ -93,7 +93,7 @@ func TestCleanToolSchemas_Unknown(t *testing.T) { Type: "function", Function: ToolFunctionSchema{ Name: "test", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "$ref": "something", "default": "val", }, @@ -122,13 +122,13 @@ func TestCleanSchema_NilParams(t *testing.T) { } func TestCleanSchema_NestedArray(t *testing.T) { - params := map[string]interface{}{ - "anyOf": []interface{}{ - map[string]interface{}{ + params := map[string]any{ + "anyOf": []any{ + map[string]any{ "type": "string", "default": "x", }, - map[string]interface{}{ + map[string]any{ "type": "number", "$ref": "#/defs/Num", "default": 42, @@ -137,12 +137,12 @@ func TestCleanSchema_NestedArray(t *testing.T) { } cleaned := CleanSchemaForProvider("gemini", params) - anyOf := cleaned["anyOf"].([]interface{}) + anyOf := cleaned["anyOf"].([]any) if len(anyOf) != 2 { t.Fatalf("expected 2 items, got %d", len(anyOf)) } - first := anyOf[0].(map[string]interface{}) + first := anyOf[0].(map[string]any) if _, ok := first["default"]; ok { t.Error("expected 'default' removed in array item") } @@ -150,7 +150,7 @@ func TestCleanSchema_NestedArray(t *testing.T) { t.Error("expected 'type' to remain in array item") } - second := anyOf[1].(map[string]interface{}) + second := anyOf[1].(map[string]any) if _, ok := second["$ref"]; ok { t.Error("expected '$ref' removed in array item") } @@ -160,13 +160,13 @@ func TestCleanSchema_NestedArray(t *testing.T) { } func TestCleanSchema_DeepNesting(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "config": map[string]interface{}{ + "properties": map[string]any{ + "config": map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "nested": map[string]interface{}{ + "properties": map[string]any{ + "nested": map[string]any{ "type": "string", "default": "deep", "$ref": "#/deep", @@ -177,10 +177,10 @@ func TestCleanSchema_DeepNesting(t *testing.T) { } cleaned := CleanSchemaForProvider("gemini", params) - props := cleaned["properties"].(map[string]interface{}) - config := props["config"].(map[string]interface{}) - innerProps := config["properties"].(map[string]interface{}) - nested := innerProps["nested"].(map[string]interface{}) + props := cleaned["properties"].(map[string]any) + config := props["config"].(map[string]any) + innerProps := config["properties"].(map[string]any) + nested := innerProps["nested"].(map[string]any) if _, ok := nested["default"]; ok { t.Error("expected deeply nested 'default' removed") diff --git a/internal/providers/types.go b/internal/providers/types.go index 054bccaa..42d59b56 100644 --- a/internal/providers/types.go +++ b/internal/providers/types.go @@ -7,12 +7,12 @@ import ( // Options keys used in ChatRequest.Options across providers. const ( - OptMaxTokens = "max_tokens" - OptTemperature = "temperature" - OptThinkingLevel = "thinking_level" + OptMaxTokens = "max_tokens" + OptTemperature = "temperature" + OptThinkingLevel = "thinking_level" OptReasoningEffort = "reasoning_effort" - OptEnableThinking = "enable_thinking" - OptThinkingBudget = "thinking_budget" + OptEnableThinking = "enable_thinking" + OptThinkingBudget = "thinking_budget" ) // TokenSource provides an OAuth access token (with auto-refresh). @@ -45,10 +45,10 @@ type ThinkingCapable interface { // ChatRequest contains the input for a Chat/ChatStream call. type ChatRequest struct { - Messages []Message `json:"messages"` - Tools []ToolDefinition `json:"tools,omitempty"` - Model string `json:"model,omitempty"` - Options map[string]interface{} `json:"options,omitempty"` + Messages []Message `json:"messages"` + Tools []ToolDefinition `json:"tools,omitempty"` + Model string `json:"model,omitempty"` + Options map[string]any `json:"options,omitempty"` } // ChatResponse is the result from an LLM call. @@ -70,9 +70,9 @@ type ChatResponse struct { // StreamChunk is a piece of a streaming response. type StreamChunk struct { - Content string `json:"content,omitempty"` - Thinking string `json:"thinking,omitempty"` - Done bool `json:"done,omitempty"` + Content string `json:"content,omitempty"` + Thinking string `json:"thinking,omitempty"` + Done bool `json:"done,omitempty"` } // ImageContent represents a base64-encoded image for vision-capable models. @@ -92,11 +92,11 @@ type MediaRef struct { // Message represents a conversation message. type Message struct { - Role string `json:"role"` // "system", "user", "assistant", "tool" + Role string `json:"role"` // "system", "user", "assistant", "tool" Content string `json:"content"` - Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.) - Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted) - MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references + Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.) + Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted) + MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` // for role="tool" responses @@ -113,10 +113,10 @@ type Message struct { // ToolCall represents a tool invocation requested by the LLM. type ToolCall struct { - ID string `json:"id"` - Name string `json:"name"` - Arguments map[string]interface{} `json:"arguments"` - Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature) + ID string `json:"id"` + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature) } // ToolDefinition describes a tool available to the LLM. @@ -127,9 +127,9 @@ type ToolDefinition struct { // ToolFunctionSchema is the schema for a function tool. type ToolFunctionSchema struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters"` + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]any `json:"parameters"` } // Usage tracks token consumption. diff --git a/internal/sandbox/docker.go b/internal/sandbox/docker.go index 64c8ac4c..f3f5c64e 100644 --- a/internal/sandbox/docker.go +++ b/internal/sandbox/docker.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "log/slog" + "maps" "os/exec" "strings" "sync" @@ -282,9 +283,7 @@ func (m *DockerManager) Release(ctx context.Context, key string) error { func (m *DockerManager) ReleaseAll(ctx context.Context) error { m.mu.Lock() sbs := make(map[string]*DockerSandbox, len(m.sandboxes)) - for k, v := range m.sandboxes { - sbs[k] = v - } + maps.Copy(sbs, m.sandboxes) m.sandboxes = make(map[string]*DockerSandbox) m.mu.Unlock() @@ -297,7 +296,7 @@ func (m *DockerManager) ReleaseAll(ctx context.Context) error { } // Stats returns information about active sandboxes. -func (m *DockerManager) Stats() map[string]interface{} { +func (m *DockerManager) Stats() map[string]any { m.mu.RLock() defer m.mu.RUnlock() @@ -306,7 +305,7 @@ func (m *DockerManager) Stats() map[string]interface{} { containers[key] = sb.containerID } - return map[string]interface{}{ + return map[string]any{ "mode": m.config.Mode, "image": m.config.Image, "active": len(m.sandboxes), diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go index bd5b7612..db9d4189 100644 --- a/internal/sandbox/sandbox.go +++ b/internal/sandbox/sandbox.go @@ -67,36 +67,36 @@ type Config struct { // Security hardening (matching TS buildSandboxCreateArgs) ReadOnlyRoot bool `json:"read_only_root"` CapDrop []string `json:"cap_drop,omitempty"` - Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m" - TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default) + Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m" + TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default) PidsLimit int `json:"pids_limit,omitempty"` - User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody") + User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody") MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec stdout+stderr capture (default 1MB, 0 = unlimited) SetupCommand string `json:"setup_command,omitempty"` ContainerPrefix string `json:"container_prefix,omitempty"` Workdir string `json:"workdir,omitempty"` // container workdir (default "/workspace") // Pruning (matching TS SandboxPruneSettings) - IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24) - MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7) + IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24) + MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7) PruneIntervalMin int `json:"prune_interval_min,omitempty"` // check interval in minutes (default 5) } // DefaultConfig returns sensible defaults matching TS sandbox defaults. func DefaultConfig() Config { return Config{ - Mode: ModeOff, - Image: "goclaw-sandbox:bookworm-slim", - WorkspaceAccess: AccessRW, - Scope: ScopeSession, - MemoryMB: 512, - CPUs: 1.0, - TimeoutSec: 300, - NetworkEnabled: false, - ReadOnlyRoot: true, - CapDrop: []string{"ALL"}, - Tmpfs: []string{"/tmp", "/var/tmp", "/run"}, - MaxOutputBytes: 1 << 20, // 1MB + Mode: ModeOff, + Image: "goclaw-sandbox:bookworm-slim", + WorkspaceAccess: AccessRW, + Scope: ScopeSession, + MemoryMB: 512, + CPUs: 1.0, + TimeoutSec: 300, + NetworkEnabled: false, + ReadOnlyRoot: true, + CapDrop: []string{"ALL"}, + Tmpfs: []string{"/tmp", "/var/tmp", "/run"}, + MaxOutputBytes: 1 << 20, // 1MB ContainerPrefix: "goclaw-sbx-", Workdir: "/workspace", IdleHours: 24, @@ -183,7 +183,7 @@ type Manager interface { Stop() // Stats returns info about active sandboxes. - Stats() map[string]interface{} + Stats() map[string]any } // ErrSandboxDisabled is returned when sandbox mode is "off". diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go index 07ccf418..e190da9d 100644 --- a/internal/scheduler/scheduler_test.go +++ b/internal/scheduler/scheduler_test.go @@ -18,7 +18,7 @@ func TestLane_ConcurrencyLimit(t *testing.T) { var maxActive atomic.Int32 var wg sync.WaitGroup - for i := 0; i < 6; i++ { + for range 6 { wg.Add(1) err := lane.Submit(context.Background(), func() { defer wg.Done() @@ -139,7 +139,7 @@ func TestScheduler_SessionSerialization(t *testing.T) { sessionKey := "agent:default:test-session" var outcomes []<-chan RunOutcome - for i := 0; i < 3; i++ { + for i := range 3 { ch := sched.Schedule(ctx, "main", agent.RunRequest{ SessionKey: sessionKey, Message: "hello", diff --git a/internal/skills/loader.go b/internal/skills/loader.go index 5095d1b8..db4711b5 100644 --- a/internal/skills/loader.go +++ b/internal/skills/loader.go @@ -463,7 +463,7 @@ func stripFrontmatter(content string) string { func parseSimpleYAML(content string) map[string]string { result := make(map[string]string) - for _, line := range strings.Split(content, "\n") { + for line := range strings.SplitSeq(content, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue diff --git a/internal/store/cron_store.go b/internal/store/cron_store.go index 0cb14868..29a3abda 100644 --- a/internal/store/cron_store.go +++ b/internal/store/cron_store.go @@ -19,7 +19,7 @@ type CronJob struct { // CronSchedule defines when a job should run. type CronSchedule struct { - Kind string `json:"kind"` // "at", "every", "cron" + Kind string `json:"kind"` // "at", "every", "cron" AtMS *int64 `json:"atMs,omitempty"` EveryMS *int64 `json:"everyMs,omitempty"` Expr string `json:"expr,omitempty"` @@ -79,10 +79,10 @@ type CronJobPatch struct { // CronEvent represents a job lifecycle event sent to subscribers. type CronEvent struct { - Action string `json:"action"` // "running", "completed", "error" + Action string `json:"action"` // "running", "completed", "error" JobID string `json:"jobId"` JobName string `json:"jobName,omitempty"` - Status string `json:"status,omitempty"` // final status for completed/error + Status string `json:"status,omitempty"` // final status for completed/error Error string `json:"error,omitempty"` } @@ -95,7 +95,7 @@ type CronStore interface { UpdateJob(jobID string, patch CronJobPatch) (*CronJob, error) EnableJob(jobID string, enabled bool) error GetRunLog(jobID string, limit, offset int) ([]CronRunLogEntry, int) - Status() map[string]interface{} + Status() map[string]any // Lifecycle Start() error diff --git a/internal/store/pg/agents.go b/internal/store/pg/agents.go index 1319516e..72a05a4e 100644 --- a/internal/store/pg/agents.go +++ b/internal/store/pg/agents.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "log/slog" + "strings" "time" "github.com/google/uuid" @@ -318,7 +319,7 @@ func (s *PGAgentStore) ListAccessible(ctx context.Context, userID string) ([]sto // --- Scan helpers --- type agentRowScanner interface { - Scan(dest ...interface{}) error + Scan(dest ...any) error } func scanAgentRow(row agentRowScanner) (*store.AgentData, error) { @@ -380,7 +381,7 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m return nil } var setClauses []string - var args []interface{} + var args []any i := 1 for col, val := range updates { setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i)) @@ -405,25 +406,25 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m } func joinStrings(s []string, sep string) string { - result := "" + var result strings.Builder for i, v := range s { if i > 0 { - result += sep + result.WriteString(sep) } - result += v + result.WriteString(v) } - return result + return result.String() } func replaceIDX(s, replacement string) string { - result := "" + var result strings.Builder for i := 0; i < len(s); i++ { if i+4 <= len(s) && s[i:i+4] == "$IDX" { - result += replacement + result.WriteString(replacement) i += 3 } else { - result += string(s[i]) + result.WriteString(string(s[i])) } } - return result + return result.String() } diff --git a/internal/store/pg/builtin_tools.go b/internal/store/pg/builtin_tools.go index b969e1ed..8e6a7aef 100644 --- a/internal/store/pg/builtin_tools.go +++ b/internal/store/pg/builtin_tools.go @@ -66,7 +66,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma switch sv := v.(type) { case json.RawMessage: allowed["settings"] = []byte(sv) - case map[string]interface{}: + case map[string]any: b, err := json.Marshal(sv) if err != nil { return fmt.Errorf("marshal settings: %w", err) @@ -85,7 +85,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma // Build UPDATE with name as PK (not UUID) var setClauses []string - var args []interface{} + var args []any i := 1 for col, val := range allowed { setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i)) diff --git a/internal/store/pg/channel_instances.go b/internal/store/pg/channel_instances.go index 55e1891a..236d2278 100644 --- a/internal/store/pg/channel_instances.go +++ b/internal/store/pg/channel_instances.go @@ -192,9 +192,9 @@ func (s *PGChannelInstanceStore) ListAll(ctx context.Context) ([]store.ChannelIn return s.scanInstances(rows) } -func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []interface{}) { +func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []any) { var conditions []string - var args []interface{} + var args []any argIdx := 1 if opts.Search != "" { diff --git a/internal/store/pg/cron_crud.go b/internal/store/pg/cron_crud.go index 79a9b611..34a44336 100644 --- a/internal/store/pg/cron_crud.go +++ b/internal/store/pg/cron_crud.go @@ -94,7 +94,7 @@ func (s *PGCronStore) ListJobs(includeDisabled bool, agentID, userID string) []s interval_ms, payload, delete_after_run, next_run_at, last_run_at, last_status, last_error, created_at, updated_at FROM cron_jobs WHERE 1=1` - var args []interface{} + var args []any argIdx := 1 if !includeDisabled { diff --git a/internal/store/pg/cron_exec.go b/internal/store/pg/cron_exec.go index 47aa7568..9a7bec99 100644 --- a/internal/store/pg/cron_exec.go +++ b/internal/store/pg/cron_exec.go @@ -99,12 +99,12 @@ func (s *PGCronStore) GetRunLog(jobID string, limit, offset int) ([]store.CronRu return result, total } -func (s *PGCronStore) Status() map[string]interface{} { +func (s *PGCronStore) Status() map[string]any { s.mu.Lock() defer s.mu.Unlock() var count int64 s.db.QueryRow("SELECT COUNT(*) FROM cron_jobs WHERE enabled = true").Scan(&count) - return map[string]interface{}{ + return map[string]any{ "enabled": s.running, "jobs": count, } diff --git a/internal/store/pg/cron_scan.go b/internal/store/pg/cron_scan.go index d2c39af8..1cd36cdb 100644 --- a/internal/store/pg/cron_scan.go +++ b/internal/store/pg/cron_scan.go @@ -22,7 +22,7 @@ func (s *PGCronStore) scanJob(id uuid.UUID) (*store.CronJob, error) { // --- Scan helpers --- type cronRowScanner interface { - Scan(dest ...interface{}) error + Scan(dest ...any) error } func scanCronRow(row cronRowScanner) (*store.CronJob, error) { diff --git a/internal/store/pg/cron_update.go b/internal/store/pg/cron_update.go index ac6b69d0..525f765d 100644 --- a/internal/store/pg/cron_update.go +++ b/internal/store/pg/cron_update.go @@ -18,7 +18,7 @@ func (s *PGCronStore) UpdateJob(jobID string, patch store.CronJobPatch) (*store. return nil, fmt.Errorf("invalid job ID: %s", jobID) } - updates := make(map[string]interface{}) + updates := make(map[string]any) if patch.Name != "" { updates["name"] = patch.Name } diff --git a/internal/store/pg/custom_tools.go b/internal/store/pg/custom_tools.go index 6bc10cef..7384d739 100644 --- a/internal/store/pg/custom_tools.go +++ b/internal/store/pg/custom_tools.go @@ -193,9 +193,9 @@ func (s *PGCustomToolStore) ListAll(ctx context.Context) ([]store.CustomToolDef, return s.scanTools(rows) } -func buildCustomToolWhere(opts store.CustomToolListOpts) (string, []interface{}) { +func buildCustomToolWhere(opts store.CustomToolListOpts) (string, []any) { conditions := []string{"enabled = true"} - var args []interface{} + var args []any argIdx := 1 if opts.AgentID != nil { diff --git a/internal/store/pg/helpers.go b/internal/store/pg/helpers.go index d57988c2..df70cd71 100644 --- a/internal/store/pg/helpers.go +++ b/internal/store/pg/helpers.go @@ -71,7 +71,7 @@ func jsonOrEmpty(data []byte) []byte { return data } -func jsonOrNull(data json.RawMessage) interface{} { +func jsonOrNull(data json.RawMessage) any { if data == nil { return nil } @@ -81,7 +81,7 @@ func jsonOrNull(data json.RawMessage) interface{} { // --- PostgreSQL array helpers --- // pqStringArray converts a Go string slice to a PostgreSQL text[] literal. -func pqStringArray(arr []string) interface{} { +func pqStringArray(arr []string) any { if arr == nil { return nil } @@ -110,7 +110,7 @@ func execMapUpdate(ctx context.Context, db *sql.DB, table string, id uuid.UUID, return nil } var setClauses []string - var args []interface{} + var args []any i := 1 for col, val := range updates { setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i)) diff --git a/internal/store/pg/memory_admin.go b/internal/store/pg/memory_admin.go index b178ea08..09de58c0 100644 --- a/internal/store/pg/memory_admin.go +++ b/internal/store/pg/memory_admin.go @@ -79,7 +79,7 @@ func (s *PGMemoryStore) GetDocumentDetail(ctx context.Context, agentID, userID, aid := mustParseUUID(agentID) var q string - var args []interface{} + var args []any if userID == "" { q = `SELECT d.path, d.content, d.hash, d.user_id, d.created_at, d.updated_at, COUNT(c.id) AS chunk_count, @@ -88,7 +88,7 @@ func (s *PGMemoryStore) GetDocumentDetail(ctx context.Context, agentID, userID, LEFT JOIN memory_chunks c ON c.document_id = d.id WHERE d.agent_id = $1 AND d.path = $2 AND d.user_id IS NULL GROUP BY d.id` - args = []interface{}{aid, path} + args = []any{aid, path} } else { q = `SELECT d.path, d.content, d.hash, d.user_id, d.created_at, d.updated_at, COUNT(c.id) AS chunk_count, @@ -97,7 +97,7 @@ func (s *PGMemoryStore) GetDocumentDetail(ctx context.Context, agentID, userID, LEFT JOIN memory_chunks c ON c.document_id = d.id WHERE d.agent_id = $1 AND d.path = $2 AND d.user_id = $3 GROUP BY d.id` - args = []interface{}{aid, path, userID} + args = []any{aid, path, userID} } var detail store.DocumentDetail @@ -124,7 +124,7 @@ func (s *PGMemoryStore) ListChunks(ctx context.Context, agentID, userID, path st aid := mustParseUUID(agentID) var q string - var args []interface{} + var args []any if userID == "" { q = `SELECT c.id, c.start_line, c.end_line, LEFT(c.text, 200) AS text_preview, @@ -133,7 +133,7 @@ func (s *PGMemoryStore) ListChunks(ctx context.Context, agentID, userID, path st JOIN memory_documents d ON c.document_id = d.id WHERE d.agent_id = $1 AND d.path = $2 AND d.user_id IS NULL ORDER BY c.start_line` - args = []interface{}{aid, path} + args = []any{aid, path} } else { q = `SELECT c.id, c.start_line, c.end_line, LEFT(c.text, 200) AS text_preview, @@ -142,7 +142,7 @@ func (s *PGMemoryStore) ListChunks(ctx context.Context, agentID, userID, path st JOIN memory_documents d ON c.document_id = d.id WHERE d.agent_id = $1 AND d.path = $2 AND d.user_id = $3 ORDER BY c.start_line` - args = []interface{}{aid, path, userID} + args = []any{aid, path, userID} } rows, err := s.db.QueryContext(ctx, q, args...) diff --git a/internal/store/pg/memory_docs.go b/internal/store/pg/memory_docs.go index 487971d7..6243cb8c 100644 --- a/internal/store/pg/memory_docs.go +++ b/internal/store/pg/memory_docs.go @@ -314,7 +314,7 @@ func vectorToString(v []float32) string { if i > 0 { buf = append(buf, ',') } - buf = append(buf, []byte(fmt.Sprintf("%g", f))...) + buf = append(buf, fmt.Appendf(nil, "%g", f)...) } buf = append(buf, ']') return string(buf) diff --git a/internal/store/pg/memory_search.go b/internal/store/pg/memory_search.go index ca98427e..2067d599 100644 --- a/internal/store/pg/memory_search.go +++ b/internal/store/pg/memory_search.go @@ -70,9 +70,9 @@ type scoredChunk struct { UserID *string } -func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID interface{}, userID string, limit int) ([]scoredChunk, error) { +func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID any, userID string, limit int) ([]scoredChunk, error) { var q string - var args []interface{} + var args []any if userID != "" { q = `SELECT path, start_line, end_line, text, user_id, @@ -81,7 +81,7 @@ func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID int WHERE agent_id = $2 AND tsv @@ plainto_tsquery('simple', $3) AND (user_id IS NULL OR user_id = $4) ORDER BY score DESC LIMIT $5` - args = []interface{}{query, agentID, query, userID, limit} + args = []any{query, agentID, query, userID, limit} } else { q = `SELECT path, start_line, end_line, text, user_id, ts_rank(tsv, plainto_tsquery('simple', $1)) AS score @@ -89,7 +89,7 @@ func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID int WHERE agent_id = $2 AND tsv @@ plainto_tsquery('simple', $3) AND user_id IS NULL ORDER BY score DESC LIMIT $4` - args = []interface{}{query, agentID, query, limit} + args = []any{query, agentID, query, limit} } rows, err := s.db.QueryContext(ctx, q, args...) @@ -107,11 +107,11 @@ func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID int return results, nil } -func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, agentID interface{}, userID string, limit int) ([]scoredChunk, error) { +func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, agentID any, userID string, limit int) ([]scoredChunk, error) { vecStr := vectorToString(embedding) var q string - var args []interface{} + var args []any if userID != "" { q = `SELECT path, start_line, end_line, text, user_id, @@ -120,7 +120,7 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a WHERE agent_id = $2 AND embedding IS NOT NULL AND (user_id IS NULL OR user_id = $3) ORDER BY embedding <=> $4::vector LIMIT $5` - args = []interface{}{vecStr, agentID, userID, vecStr, limit} + args = []any{vecStr, agentID, userID, vecStr, limit} } else { q = `SELECT path, start_line, end_line, text, user_id, 1 - (embedding <=> $1::vector) AS score @@ -128,7 +128,7 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a WHERE agent_id = $2 AND embedding IS NOT NULL AND user_id IS NULL ORDER BY embedding <=> $3::vector LIMIT $4` - args = []interface{}{vecStr, agentID, vecStr, limit} + args = []any{vecStr, agentID, vecStr, limit} } rows, err := s.db.QueryContext(ctx, q, args...) @@ -145,6 +145,7 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a } return results, nil } + // hybridMerge combines FTS and vector results with weighted scoring. // Per-user results get a 1.2x boost. Deduplication: user copy wins over global. func hybridMerge(fts, vec []scoredChunk, textWeight, vectorWeight float64, currentUserID string) []store.MemorySearchResult { diff --git a/internal/store/pg/sessions.go b/internal/store/pg/sessions.go index 88794bf1..7c361b4b 100644 --- a/internal/store/pg/sessions.go +++ b/internal/store/pg/sessions.go @@ -3,6 +3,7 @@ package pg import ( "database/sql" "encoding/json" + "maps" "sync" "time" @@ -138,9 +139,7 @@ func (s *PGSessionStore) SetSessionMetadata(key string, metadata map[string]stri if data.Metadata == nil { data.Metadata = make(map[string]string) } - for k, v := range metadata { - data.Metadata[k] = v - } + maps.Copy(data.Metadata, metadata) data.Updated = time.Now() } diff --git a/internal/store/pg/sessions_list.go b/internal/store/pg/sessions_list.go index 8dc63cdd..0fe3a3f5 100644 --- a/internal/store/pg/sessions_list.go +++ b/internal/store/pg/sessions_list.go @@ -63,13 +63,10 @@ func (s *PGSessionStore) ListPaged(opts store.SessionListOpts) store.SessionList if limit <= 0 { limit = 20 } - offset := opts.Offset - if offset < 0 { - offset = 0 - } + offset := max(opts.Offset, 0) var where string - var whereArgs []interface{} + var whereArgs []any if opts.AgentID != "" { where = " WHERE session_key LIKE $1" @@ -85,16 +82,16 @@ func (s *PGSessionStore) ListPaged(opts store.SessionListOpts) store.SessionList // Fetch page using jsonb_array_length to avoid loading full messages var selectQ string - var selectArgs []interface{} + var selectArgs []any if opts.AgentID != "" { selectQ = `SELECT session_key, jsonb_array_length(messages), created_at, updated_at, label, channel, user_id, COALESCE(metadata, '{}') FROM sessions WHERE session_key LIKE $1 ORDER BY updated_at DESC LIMIT $2 OFFSET $3` - selectArgs = []interface{}{whereArgs[0], limit, offset} + selectArgs = []any{whereArgs[0], limit, offset} } else { selectQ = `SELECT session_key, jsonb_array_length(messages), created_at, updated_at, label, channel, user_id, COALESCE(metadata, '{}') FROM sessions ORDER BY updated_at DESC LIMIT $1 OFFSET $2` - selectArgs = []interface{}{limit, offset} + selectArgs = []any{limit, offset} } rows, err := s.db.Query(selectQ, selectArgs...) @@ -140,13 +137,10 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session if limit <= 0 { limit = 20 } - offset := opts.Offset - if offset < 0 { - offset = 0 - } + offset := max(opts.Offset, 0) var where string - var whereArgs []interface{} + var whereArgs []any if opts.AgentID != "" { where = " WHERE s.session_key LIKE $1" @@ -162,7 +156,7 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session // Fetch page with agent name via LEFT JOIN var selectQ string - var selectArgs []interface{} + var selectArgs []any const richCols = `s.session_key, jsonb_array_length(s.messages), s.created_at, s.updated_at, s.label, s.channel, s.user_id, COALESCE(s.metadata, '{}'), @@ -173,12 +167,12 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session selectQ = `SELECT ` + richCols + ` FROM sessions s LEFT JOIN agents a ON s.agent_id = a.id WHERE s.session_key LIKE $1 ORDER BY s.updated_at DESC LIMIT $2 OFFSET $3` - selectArgs = []interface{}{whereArgs[0], limit, offset} + selectArgs = []any{whereArgs[0], limit, offset} } else { selectQ = `SELECT ` + richCols + ` FROM sessions s LEFT JOIN agents a ON s.agent_id = a.id ORDER BY s.updated_at DESC LIMIT $1 OFFSET $2` - selectArgs = []interface{}{limit, offset} + selectArgs = []any{limit, offset} } rows, err := s.db.Query(selectQ, selectArgs...) diff --git a/internal/store/pg/skills.go b/internal/store/pg/skills.go index 77c9344c..404aecdf 100644 --- a/internal/store/pg/skills.go +++ b/internal/store/pg/skills.go @@ -127,14 +127,15 @@ func (s *PGSkillStore) LoadForContext(allowList []string) string { if len(parts) == 0 { return "" } - result := "## Available Skills\n\n" + var result strings.Builder + result.WriteString("## Available Skills\n\n") for i, p := range parts { if i > 0 { - result += "\n\n---\n\n" + result.WriteString("\n\n---\n\n") } - result += p + result.WriteString(p) } - return result + return result.String() } func (s *PGSkillStore) BuildSummary(allowList []string) string { @@ -142,16 +143,17 @@ func (s *PGSkillStore) BuildSummary(allowList []string) string { if len(skills) == 0 { return "" } - result := "\n" + var result strings.Builder + result.WriteString("\n") for _, sk := range skills { - result += " \n" - result += fmt.Sprintf(" %s\n", sk.Name) - result += fmt.Sprintf(" %s\n", sk.Description) - result += fmt.Sprintf(" %s\n", sk.Path) - result += " \n" + result.WriteString(" \n") + result.WriteString(fmt.Sprintf(" %s\n", sk.Name)) + result.WriteString(fmt.Sprintf(" %s\n", sk.Description)) + result.WriteString(fmt.Sprintf(" %s\n", sk.Path)) + result.WriteString(" \n") } - result += "" - return result + result.WriteString("") + return result.String() } func (s *PGSkillStore) GetSkill(name string) (*store.SkillInfo, bool) { @@ -212,7 +214,7 @@ func (s *PGSkillStore) CreateSkill(name, slug string, description *string, owner return err } -func (s *PGSkillStore) UpdateSkill(id uuid.UUID, updates map[string]interface{}) error { +func (s *PGSkillStore) UpdateSkill(id uuid.UUID, updates map[string]any) error { if err := execMapUpdate(context.Background(), s.db, "skills", id, updates); err != nil { return err } diff --git a/internal/store/pg/teams_delegation.go b/internal/store/pg/teams_delegation.go index e0ba2eb1..8d64aa10 100644 --- a/internal/store/pg/teams_delegation.go +++ b/internal/store/pg/teams_delegation.go @@ -79,10 +79,7 @@ func (s *PGTeamStore) ListDelegationHistory(ctx context.Context, opts store.Dele if limit <= 0 || limit > 200 { limit = 50 } - offset := opts.Offset - if offset < 0 { - offset = 0 - } + offset := max(opts.Offset, 0) query := fmt.Sprintf( `SELECT d.id, d.source_agent_id, d.target_agent_id, d.team_id, d.team_task_id, diff --git a/internal/store/pg/tracing.go b/internal/store/pg/tracing.go index c4556d1f..461b9487 100644 --- a/internal/store/pg/tracing.go +++ b/internal/store/pg/tracing.go @@ -90,9 +90,9 @@ func (s *PGTracingStore) GetTrace(ctx context.Context, traceID uuid.UUID) (*stor return &d, nil } -func buildTraceWhere(opts store.TraceListOpts) (string, []interface{}) { +func buildTraceWhere(opts store.TraceListOpts) (string, []any) { var conditions []string - var args []interface{} + var args []any argIdx := 1 if opts.AgentID != nil { @@ -289,7 +289,7 @@ func (s *PGTracingStore) BatchCreateSpans(ctx context.Context, spans []store.Spa // Build multi-row INSERT const cols = 24 valueGroups := make([]string, len(spans)) - args := make([]interface{}, 0, len(spans)*cols) + args := make([]any, 0, len(spans)*cols) for i, span := range spans { if span.ID == uuid.Nil { @@ -298,7 +298,7 @@ func (s *PGTracingStore) BatchCreateSpans(ctx context.Context, spans []store.Spa } base := i * cols placeholders := make([]string, cols) - for j := 0; j < cols; j++ { + for j := range cols { placeholders[j] = fmt.Sprintf("$%d", base+j+1) } valueGroups[i] = "(" + strings.Join(placeholders, ", ") + ")" @@ -356,4 +356,3 @@ func (s *PGTracingStore) BatchUpdateTraceAggregates(ctx context.Context, traceID WHERE id = $1`, traceID) return err } - diff --git a/internal/store/team_store.go b/internal/store/team_store.go index b486f4f4..1272e3bd 100644 --- a/internal/store/team_store.go +++ b/internal/store/team_store.go @@ -72,17 +72,17 @@ type TeamMemberData struct { // TeamTaskData represents a task in the team's shared task list. type TeamTaskData struct { BaseModel - TeamID uuid.UUID `json:"team_id"` - Subject string `json:"subject"` - Description string `json:"description,omitempty"` - Status string `json:"status"` - OwnerAgentID *uuid.UUID `json:"owner_agent_id,omitempty"` - BlockedBy []uuid.UUID `json:"blocked_by,omitempty"` - Priority int `json:"priority"` - Result *string `json:"result,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - UserID string `json:"user_id,omitempty"` - Channel string `json:"channel,omitempty"` + TeamID uuid.UUID `json:"team_id"` + Subject string `json:"subject"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + OwnerAgentID *uuid.UUID `json:"owner_agent_id,omitempty"` + BlockedBy []uuid.UUID `json:"blocked_by,omitempty"` + Priority int `json:"priority"` + Result *string `json:"result,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + UserID string `json:"user_id,omitempty"` + Channel string `json:"channel,omitempty"` // Joined fields OwnerAgentKey string `json:"owner_agent_key,omitempty"` @@ -91,21 +91,21 @@ type TeamTaskData struct { // DelegationHistoryData represents a persisted delegation record. type DelegationHistoryData struct { BaseModel - SourceAgentID uuid.UUID `json:"source_agent_id"` - TargetAgentID uuid.UUID `json:"target_agent_id"` - TeamID *uuid.UUID `json:"team_id,omitempty"` - TeamTaskID *uuid.UUID `json:"team_task_id,omitempty"` - UserID string `json:"user_id,omitempty"` - Task string `json:"task"` - Mode string `json:"mode"` - Status string `json:"status"` - Result *string `json:"result,omitempty"` - Error *string `json:"error,omitempty"` - Iterations int `json:"iterations"` - TraceID *uuid.UUID `json:"trace_id,omitempty"` - DurationMS int `json:"duration_ms"` - CompletedAt *time.Time `json:"completed_at,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + SourceAgentID uuid.UUID `json:"source_agent_id"` + TargetAgentID uuid.UUID `json:"target_agent_id"` + TeamID *uuid.UUID `json:"team_id,omitempty"` + TeamTaskID *uuid.UUID `json:"team_task_id,omitempty"` + UserID string `json:"user_id,omitempty"` + Task string `json:"task"` + Mode string `json:"mode"` + Status string `json:"status"` + Result *string `json:"result,omitempty"` + Error *string `json:"error,omitempty"` + Iterations int `json:"iterations"` + TraceID *uuid.UUID `json:"trace_id,omitempty"` + DurationMS int `json:"duration_ms"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` // Joined fields SourceAgentKey string `json:"source_agent_key,omitempty"` @@ -125,29 +125,29 @@ type DelegationHistoryListOpts struct { // HandoffRouteData represents an active routing override for agent handoff. type HandoffRouteData struct { - ID uuid.UUID `json:"id"` - Channel string `json:"channel"` - ChatID string `json:"chat_id"` - FromAgentKey string `json:"from_agent_key"` - ToAgentKey string `json:"to_agent_key"` - Reason string `json:"reason,omitempty"` - CreatedBy string `json:"created_by"` - CreatedAt time.Time `json:"created_at"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + ID uuid.UUID `json:"id"` + Channel string `json:"channel"` + ChatID string `json:"chat_id"` + FromAgentKey string `json:"from_agent_key"` + ToAgentKey string `json:"to_agent_key"` + Reason string `json:"reason,omitempty"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + Metadata map[string]any `json:"metadata,omitempty"` } // TeamMessageData represents a message in the team mailbox. type TeamMessageData struct { - ID uuid.UUID `json:"id"` - TeamID uuid.UUID `json:"team_id"` - FromAgentID uuid.UUID `json:"from_agent_id"` - ToAgentID *uuid.UUID `json:"to_agent_id,omitempty"` - Content string `json:"content"` - MessageType string `json:"message_type"` - Read bool `json:"read"` - TaskID *uuid.UUID `json:"task_id,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - CreatedAt time.Time `json:"created_at"` + ID uuid.UUID `json:"id"` + TeamID uuid.UUID `json:"team_id"` + FromAgentID uuid.UUID `json:"from_agent_id"` + ToAgentID *uuid.UUID `json:"to_agent_id,omitempty"` + Content string `json:"content"` + MessageType string `json:"message_type"` + Read bool `json:"read"` + TaskID *uuid.UUID `json:"task_id,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` // Joined fields FromAgentKey string `json:"from_agent_key,omitempty"` diff --git a/internal/tools/create_audio.go b/internal/tools/create_audio.go index 56380508..2a11187a 100644 --- a/internal/tools/create_audio.go +++ b/internal/tools/create_audio.go @@ -22,8 +22,8 @@ var audioGenModelDefaults = map[string]string{ // CreateAudioTool generates music or sound effects using AI audio generation APIs. type CreateAudioTool struct { - registry *providers.Registry - elevenlabsAPIKey string + registry *providers.Registry + elevenlabsAPIKey string elevenlabsBaseURL string } @@ -31,8 +31,8 @@ type CreateAudioTool struct { // elevenlabsKey and elevenlabsBase are used for ElevenLabs sound effects generation. func NewCreateAudioTool(registry *providers.Registry, elevenlabsKey, elevenlabsBase string) *CreateAudioTool { return &CreateAudioTool{ - registry: registry, - elevenlabsAPIKey: elevenlabsKey, + registry: registry, + elevenlabsAPIKey: elevenlabsKey, elevenlabsBaseURL: elevenlabsBase, } } @@ -43,31 +43,31 @@ func (t *CreateAudioTool) Description() string { return "Generate music, sound effects, or ambient audio from a text description. Returns a MEDIA: path to the generated audio file. Note: for music, duration is determined by lyrics length — provide longer/shorter lyrics to control length. The 'duration' parameter only applies to sound effects." } -func (t *CreateAudioTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *CreateAudioTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "Description of audio to generate.", }, - "type": map[string]interface{}{ + "type": map[string]any{ "type": "string", "description": "Type of audio: 'music' (default) or 'sound_effect'.", }, - "duration": map[string]interface{}{ + "duration": map[string]any{ "type": "integer", "description": "Duration in seconds (only for sound effects). For music, duration is controlled by lyrics length.", }, - "lyrics": map[string]interface{}{ + "lyrics": map[string]any{ "type": "string", "description": "Lyrics for music generation. Use [Verse], [Chorus] tags.", }, - "instrumental": map[string]interface{}{ + "instrumental": map[string]any{ "type": "boolean", "description": "No vocals (default: false).", }, - "provider": map[string]interface{}{ + "provider": map[string]any{ "type": "string", "description": "Force a specific provider (e.g. 'minimax').", }, @@ -76,7 +76,7 @@ func (t *CreateAudioTool) Parameters() map[string]interface{} { } } -func (t *CreateAudioTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *CreateAudioTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { return ErrorResult("prompt is required") diff --git a/internal/tools/create_audio_elevenlabs.go b/internal/tools/create_audio_elevenlabs.go index 2ec41c8b..7b2a0537 100644 --- a/internal/tools/create_audio_elevenlabs.go +++ b/internal/tools/create_audio_elevenlabs.go @@ -26,7 +26,7 @@ func (t *CreateAudioTool) callElevenLabsSoundEffect(ctx context.Context, prompt durationSeconds = 30 } - body := map[string]interface{}{ + body := map[string]any{ "text": prompt, "output_format": "mp3_44100_128", "prompt_influence": 0.3, diff --git a/internal/tools/create_audio_minimax.go b/internal/tools/create_audio_minimax.go index be35cd80..15f2fe8a 100644 --- a/internal/tools/create_audio_minimax.go +++ b/internal/tools/create_audio_minimax.go @@ -30,13 +30,13 @@ func callMinimaxMusicGen(ctx context.Context, apiKey, apiBase, model, prompt str // NOTE: MiniMax music API does NOT support a duration parameter. // Audio length is determined by lyrics length (vocal) or the model's default (~3-4 min for instrumental). - body := map[string]interface{}{ + body := map[string]any{ "model": model, "prompt": prompt, "is_instrumental": instrumental, "lyrics_optimizer": lyricsOptimizer, "output_format": "url", - "audio_setting": map[string]interface{}{ + "audio_setting": map[string]any{ "sample_rate": sampleRate, "bitrate": bitrate, "format": "mp3", diff --git a/internal/tools/create_image.go b/internal/tools/create_image.go index d90c5935..fbf34c45 100644 --- a/internal/tools/create_image.go +++ b/internal/tools/create_image.go @@ -51,15 +51,15 @@ func (t *CreateImageTool) Description() string { return "Generate an image from a text description using an image generation model. Returns a MEDIA: path to the generated image file." } -func (t *CreateImageTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *CreateImageTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "Text description of the image to generate.", }, - "aspect_ratio": map[string]interface{}{ + "aspect_ratio": map[string]any{ "type": "string", "description": "Aspect ratio: '1:1' (default), '3:4', '4:3', '9:16', '16:9'.", }, @@ -68,7 +68,7 @@ func (t *CreateImageTool) Parameters() map[string]interface{} { } } -func (t *CreateImageTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *CreateImageTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { return ErrorResult("prompt is required") @@ -152,15 +152,15 @@ func (t *CreateImageTool) callProvider(ctx context.Context, cp credentialProvide // callImageGenAPI calls the OpenAI-compatible chat completions endpoint with image modalities. // Works with OpenRouter (modalities: ["image","text"]). func (t *CreateImageTool) callImageGenAPI(ctx context.Context, apiKey, apiBase, model, prompt, aspectRatio string, params map[string]any) ([]byte, *providers.Usage, error) { - body := map[string]interface{}{ + body := map[string]any{ "model": model, - "messages": []map[string]interface{}{ + "messages": []map[string]any{ {"role": "user", "content": prompt}, }, "modalities": []string{"image", "text"}, } if aspectRatio != "" && aspectRatio != "1:1" { - body["image_config"] = map[string]interface{}{ + body["image_config"] = map[string]any{ "aspect_ratio": aspectRatio, } } @@ -198,7 +198,7 @@ func (t *CreateImageTool) callImageGenAPI(ctx context.Context, apiKey, apiBase, // callStandardImageGenAPI uses the /images/generations endpoint (OpenAI and compatible providers). func (t *CreateImageTool) callStandardImageGenAPI(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) { - body := map[string]interface{}{ + body := map[string]any{ "model": model, "prompt": prompt, "n": 1, @@ -262,11 +262,11 @@ func (t *CreateImageTool) callGeminiNativeImageGen(ctx context.Context, apiKey, url := fmt.Sprintf("%s/models/%s:generateContent?key=%s", nativeBase, model, apiKey) - body := map[string]interface{}{ - "contents": []map[string]interface{}{ - {"parts": []map[string]interface{}{{"text": prompt}}}, + body := map[string]any{ + "contents": []map[string]any{ + {"parts": []map[string]any{{"text": prompt}}}, }, - "generationConfig": map[string]interface{}{ + "generationConfig": map[string]any{ "responseModalities": []string{"TEXT", "IMAGE"}, }, } @@ -350,7 +350,7 @@ func (t *CreateImageTool) parseImageResponse(respBody []byte) ([]byte, *provider var resp struct { Choices []struct { Message struct { - Content interface{} `json:"content"` + Content any `json:"content"` Images []struct { ImageURL struct { URL string `json:"url"` @@ -382,11 +382,11 @@ func (t *CreateImageTool) parseImageResponse(respBody []byte) ([]byte, *provider } // Try multipart content array (some providers return content as array of parts) - if parts, ok := msg.Content.([]interface{}); ok { + if parts, ok := msg.Content.([]any); ok { for _, part := range parts { - if m, ok := part.(map[string]interface{}); ok { + if m, ok := part.(map[string]any); ok { if m["type"] == "image_url" { - if imgURL, ok := m["image_url"].(map[string]interface{}); ok { + if imgURL, ok := m["image_url"].(map[string]any); ok { if url, ok := imgURL["url"].(string); ok { if imageBytes, err := decodeDataURL(url); err == nil { return imageBytes, convertUsage(resp.Usage), nil @@ -403,11 +403,11 @@ func (t *CreateImageTool) parseImageResponse(respBody []byte) ([]byte, *provider // decodeDataURL decodes a data:image/...;base64,... URL into raw bytes. func decodeDataURL(dataURL string) ([]byte, error) { - idx := strings.Index(dataURL, ";base64,") - if idx < 0 { + _, after, ok := strings.Cut(dataURL, ";base64,") + if !ok { return nil, fmt.Errorf("not a base64 data URL") } - b64 := dataURL[idx+8:] + b64 := after return base64.StdEncoding.DecodeString(b64) } diff --git a/internal/tools/create_image_dashscope.go b/internal/tools/create_image_dashscope.go index 9d8434cb..d38b54d7 100644 --- a/internal/tools/create_image_dashscope.go +++ b/internal/tools/create_image_dashscope.go @@ -29,8 +29,8 @@ func dashScopeImageEndpoint(apiBase string) string { "/openai", "/v1", } { - if strings.HasSuffix(base, suffix) { - base = strings.TrimSuffix(base, suffix) + if before, ok := strings.CutSuffix(base, suffix); ok { + base = before break } } @@ -48,8 +48,8 @@ func dashScopeTaskEndpoint(apiBase, taskID string) string { "/openai", "/v1", } { - if strings.HasSuffix(base, suffix) { - base = strings.TrimSuffix(base, suffix) + if before, ok := strings.CutSuffix(base, suffix); ok { + base = before break } } @@ -82,14 +82,14 @@ func callDashScopeImageGen(ctx context.Context, apiKey, apiBase, model, prompt s endpoint := dashScopeImageEndpoint(apiBase) - body := map[string]interface{}{ + body := map[string]any{ "model": model, - "input": map[string]interface{}{ - "messages": []map[string]interface{}{ + "input": map[string]any{ + "messages": []map[string]any{ {"role": "user", "content": prompt}, }, }, - "parameters": map[string]interface{}{ + "parameters": map[string]any{ "n": 1, "size": size, "prompt_extend": promptExtend, @@ -162,7 +162,7 @@ func dashScopePollTask(ctx context.Context, apiKey, apiBase, taskID string, clie const maxPolls = 30 const pollInterval = 10 * time.Second - for i := 0; i < maxPolls; i++ { + for i := range maxPolls { select { case <-ctx.Done(): return nil, nil, ctx.Err() diff --git a/internal/tools/create_image_minimax.go b/internal/tools/create_image_minimax.go index a2da73bd..ab58839d 100644 --- a/internal/tools/create_image_minimax.go +++ b/internal/tools/create_image_minimax.go @@ -42,7 +42,7 @@ func callMinimaxImageGen(ctx context.Context, apiKey, apiBase, model, prompt str size := aspectRatioToMinimaxSize(params) promptOptimizer := GetParamBool(params, "prompt_optimizer", true) - body := map[string]interface{}{ + body := map[string]any{ "model": model, "prompt": prompt, "size": size, diff --git a/internal/tools/create_video.go b/internal/tools/create_video.go index 32066e88..51f3d2a5 100644 --- a/internal/tools/create_video.go +++ b/internal/tools/create_video.go @@ -43,19 +43,19 @@ func (t *CreateVideoTool) Description() string { return "Generate a video from a text description using a video generation model. Returns a MEDIA: path to the generated video file." } -func (t *CreateVideoTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *CreateVideoTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "Text description of the video to generate.", }, - "duration": map[string]interface{}{ + "duration": map[string]any{ "type": "integer", "description": "Video duration in seconds: 4, 6, or 8 (default 8).", }, - "aspect_ratio": map[string]interface{}{ + "aspect_ratio": map[string]any{ "type": "string", "description": "Aspect ratio: '16:9' (default) or '9:16'.", }, @@ -64,7 +64,7 @@ func (t *CreateVideoTool) Parameters() map[string]interface{} { } } -func (t *CreateVideoTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *CreateVideoTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { return ErrorResult("prompt is required") @@ -166,11 +166,11 @@ func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBas // 1. Start long-running video generation. predictURL := fmt.Sprintf("%s/models/%s:predictLongRunning", nativeBase, model) - body := map[string]interface{}{ - "instances": []map[string]interface{}{ + body := map[string]any{ + "instances": []map[string]any{ {"prompt": prompt}, }, - "parameters": map[string]interface{}{ + "parameters": map[string]any{ "aspectRatio": aspectRatio, "durationSeconds": duration, "personGeneration": GetParamString(params, "person_generation", "allow_all"), @@ -223,7 +223,7 @@ func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBas const pollInterval = 10 * time.Second var doneBody []byte - for i := 0; i < maxPolls; i++ { + for i := range maxPolls { select { case <-ctx.Done(): return nil, nil, ctx.Err() @@ -326,9 +326,9 @@ func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBas // callChatVideoGen tries OpenAI-compatible chat completions with video modality. func (t *CreateVideoTool) callChatVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string) ([]byte, *providers.Usage, error) { - body := map[string]interface{}{ + body := map[string]any{ "model": model, - "messages": []map[string]interface{}{ + "messages": []map[string]any{ {"role": "user", "content": prompt}, }, "modalities": []string{"video", "text"}, @@ -368,7 +368,7 @@ func (t *CreateVideoTool) callChatVideoGen(ctx context.Context, apiKey, apiBase, var chatResp struct { Choices []struct { Message struct { - Content interface{} `json:"content"` + Content any `json:"content"` } `json:"message"` } `json:"choices"` Usage *struct { @@ -386,11 +386,11 @@ func (t *CreateVideoTool) callChatVideoGen(ctx context.Context, apiKey, apiBase, } // Look for video data URL in multipart content. - if parts, ok := chatResp.Choices[0].Message.Content.([]interface{}); ok { + if parts, ok := chatResp.Choices[0].Message.Content.([]any); ok { for _, part := range parts { - if m, ok := part.(map[string]interface{}); ok { + if m, ok := part.(map[string]any); ok { if m["type"] == "video_url" || m["type"] == "image_url" { - if vidURL, ok := m["video_url"].(map[string]interface{}); ok { + if vidURL, ok := m["video_url"].(map[string]any); ok { if urlStr, ok := vidURL["url"].(string); ok { if videoBytes, err := decodeDataURL(urlStr); err == nil { return videoBytes, convertUsage(chatResp.Usage), nil diff --git a/internal/tools/create_video_minimax.go b/internal/tools/create_video_minimax.go index eee22d79..2b1ca923 100644 --- a/internal/tools/create_video_minimax.go +++ b/internal/tools/create_video_minimax.go @@ -26,7 +26,7 @@ func callMinimaxVideoGen(ctx context.Context, apiKey, apiBase, model string, par base := strings.TrimRight(apiBase, "/") // 1. Submit video generation task. - submitBody := map[string]interface{}{ + submitBody := map[string]any{ "model": model, "prompt": prompt, "duration": duration, @@ -91,7 +91,7 @@ func callMinimaxVideoGen(ctx context.Context, apiKey, apiBase, model string, par const pollInterval = 10 * time.Second var fileID string - for i := 0; i < maxPolls; i++ { + for i := range maxPolls { select { case <-ctx.Done(): return nil, nil, ctx.Err() diff --git a/internal/tools/cron.go b/internal/tools/cron.go index 7e60aa3f..3d84d99a 100644 --- a/internal/tools/cron.go +++ b/internal/tools/cron.go @@ -68,38 +68,38 @@ CRITICAL CONSTRAINTS: Use jobId as the canonical identifier; id is accepted for compatibility.` } -func (t *CronTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *CronTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "description": "The cron action to perform", "enum": []string{"status", "list", "add", "update", "remove", "run", "runs"}, }, - "includeDisabled": map[string]interface{}{ + "includeDisabled": map[string]any{ "type": "boolean", "description": "Include disabled jobs in list (default false)", }, - "job": map[string]interface{}{ - "type": "object", - "description": "Job definition for add action (name, schedule, message, deliver, channel, to, agentId, deleteAfterRun)", + "job": map[string]any{ + "type": "object", + "description": "Job definition for add action (name, schedule, message, deliver, channel, to, agentId, deleteAfterRun)", "additionalProperties": true, }, - "jobId": map[string]interface{}{ + "jobId": map[string]any{ "type": "string", "description": "Job ID for update/remove/run/runs actions", }, - "id": map[string]interface{}{ + "id": map[string]any{ "type": "string", "description": "Backward compatibility alias for jobId", }, - "patch": map[string]interface{}{ - "type": "object", - "description": "Patch object for update action", + "patch": map[string]any{ + "type": "object", + "description": "Patch object for update action", "additionalProperties": true, }, - "runMode": map[string]interface{}{ + "runMode": map[string]any{ "type": "string", "description": "Run mode: 'due' (only if due) or 'force' (immediate)", "enum": []string{"due", "force"}, @@ -109,7 +109,7 @@ func (t *CronTool) Parameters() map[string]interface{} { } } -func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *CronTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) if action == "" { return ErrorResult("action parameter is required") @@ -151,11 +151,11 @@ func (t *CronTool) handleStatus() *Result { return NewResult(string(data)) } -func (t *CronTool) handleList(args map[string]interface{}, agentID, userID string) *Result { +func (t *CronTool) handleList(args map[string]any, agentID, userID string) *Result { includeDisabled, _ := args["includeDisabled"].(bool) jobs := t.cronStore.ListJobs(includeDisabled, agentID, userID) - result := map[string]interface{}{ + result := map[string]any{ "jobs": jobs, "count": len(jobs), } @@ -163,8 +163,8 @@ func (t *CronTool) handleList(args map[string]interface{}, agentID, userID strin return NewResult(string(data)) } -func (t *CronTool) handleAdd(ctx context.Context, args map[string]interface{}, agentID, userID string) *Result { - jobObj, ok := args["job"].(map[string]interface{}) +func (t *CronTool) handleAdd(ctx context.Context, args map[string]any, agentID, userID string) *Result { + jobObj, ok := args["job"].(map[string]any) if !ok { return ErrorResult("job object is required for add action") } @@ -174,7 +174,7 @@ func (t *CronTool) handleAdd(ctx context.Context, args map[string]interface{}, a return ErrorResult("job.name is required") } - scheduleObj, ok := jobObj["schedule"].(map[string]interface{}) + scheduleObj, ok := jobObj["schedule"].(map[string]any) if !ok { return ErrorResult("job.schedule is required") } @@ -252,7 +252,7 @@ func (t *CronTool) handleAdd(ctx context.Context, args map[string]interface{}, a return ErrorResult(fmt.Sprintf("failed to create cron job: %v", err)) } - data, _ := json.MarshalIndent(map[string]interface{}{"job": job}, "", " ") + data, _ := json.MarshalIndent(map[string]any{"job": job}, "", " ") return NewResult(string(data)) } @@ -275,7 +275,7 @@ func (t *CronTool) checkJobOwnership(jobID, agentID, userID string) (*store.Cron return job, nil } -func (t *CronTool) handleUpdate(args map[string]interface{}, agentID, userID string) *Result { +func (t *CronTool) handleUpdate(args map[string]any, agentID, userID string) *Result { jobID := resolveJobID(args) if jobID == "" { return ErrorResult("jobId is required for update action") @@ -285,7 +285,7 @@ func (t *CronTool) handleUpdate(args map[string]interface{}, agentID, userID str return errResult } - patchObj, ok := args["patch"].(map[string]interface{}) + patchObj, ok := args["patch"].(map[string]any) if !ok { return ErrorResult("patch object is required for update action") } @@ -300,11 +300,11 @@ func (t *CronTool) handleUpdate(args map[string]interface{}, agentID, userID str return ErrorResult(fmt.Sprintf("failed to update cron job: %v", err)) } - data, _ := json.MarshalIndent(map[string]interface{}{"job": job}, "", " ") + data, _ := json.MarshalIndent(map[string]any{"job": job}, "", " ") return NewResult(string(data)) } -func (t *CronTool) handleRemove(args map[string]interface{}, agentID, userID string) *Result { +func (t *CronTool) handleRemove(args map[string]any, agentID, userID string) *Result { jobID := resolveJobID(args) if jobID == "" { return ErrorResult("jobId is required for remove action") @@ -318,11 +318,11 @@ func (t *CronTool) handleRemove(args map[string]interface{}, agentID, userID str return ErrorResult(fmt.Sprintf("failed to remove cron job: %v", err)) } - data, _ := json.MarshalIndent(map[string]interface{}{"deleted": true, "jobId": jobID}, "", " ") + data, _ := json.MarshalIndent(map[string]any{"deleted": true, "jobId": jobID}, "", " ") return NewResult(string(data)) } -func (t *CronTool) handleRun(args map[string]interface{}, agentID, userID string) *Result { +func (t *CronTool) handleRun(args map[string]any, agentID, userID string) *Result { jobID := resolveJobID(args) if jobID == "" { return ErrorResult("jobId is required for run action") @@ -340,7 +340,7 @@ func (t *CronTool) handleRun(args map[string]interface{}, agentID, userID string return ErrorResult(fmt.Sprintf("failed to run cron job: %v", err)) } - result := map[string]interface{}{ + result := map[string]any{ "ran": ran, "jobId": jobID, } @@ -351,7 +351,7 @@ func (t *CronTool) handleRun(args map[string]interface{}, agentID, userID string return NewResult(string(data)) } -func (t *CronTool) handleRuns(args map[string]interface{}, agentID, userID string) *Result { +func (t *CronTool) handleRuns(args map[string]any, agentID, userID string) *Result { jobID := resolveJobID(args) // Validate ownership if a specific job is requested @@ -368,7 +368,7 @@ func (t *CronTool) handleRuns(args map[string]interface{}, agentID, userID strin entries, total := t.cronStore.GetRunLog(jobID, limit, 0) - result := map[string]interface{}{ + result := map[string]any{ "entries": entries, "count": len(entries), "total": total, @@ -379,7 +379,7 @@ func (t *CronTool) handleRuns(args map[string]interface{}, agentID, userID strin // --- helpers --- -func resolveJobID(args map[string]interface{}) string { +func resolveJobID(args map[string]any) string { if id, ok := args["jobId"].(string); ok && id != "" { return id } @@ -389,12 +389,12 @@ func resolveJobID(args map[string]interface{}) string { return "" } -func stringFromMap(m map[string]interface{}, key string) string { +func stringFromMap(m map[string]any, key string) string { v, _ := m[key].(string) return v } -func numberFromMap(m map[string]interface{}, key string) (float64, bool) { +func numberFromMap(m map[string]any, key string) (float64, bool) { v, ok := m[key].(float64) return v, ok } diff --git a/internal/tools/delegate_events.go b/internal/tools/delegate_events.go index 1abcf028..37702a18 100644 --- a/internal/tools/delegate_events.go +++ b/internal/tools/delegate_events.go @@ -2,6 +2,7 @@ package tools import ( "fmt" + "strings" "time" "github.com/google/uuid" @@ -80,46 +81,47 @@ func formatDelegateAnnounce(task *DelegationTask, artifacts *DelegateArtifacts, task.TargetAgentKey, err.Error(), elapsed.Round(time.Millisecond)) } - msg := "[System Message] All team delegations completed.\n\n" + var msg strings.Builder + msg.WriteString("[System Message] All team delegations completed.\n\n") // List auto-completed task IDs so LLM doesn't reuse them if len(artifacts.CompletedTaskIDs) > 0 { - msg += "Auto-completed team tasks: " + msg.WriteString("Auto-completed team tasks: ") for i, tid := range artifacts.CompletedTaskIDs { if i > 0 { - msg += ", " + msg.WriteString(", ") } - msg += tid + msg.WriteString(tid) } - msg += "\nFor follow-up delegations, create NEW tasks (do not reuse completed task IDs).\n\n" + msg.WriteString("\nFor follow-up delegations, create NEW tasks (do not reuse completed task IDs).\n\n") } // Render each delegation result for i, r := range artifacts.Results { - msg += fmt.Sprintf("--- Result from %q ---\n%s\n", r.AgentKey, r.Content) + msg.WriteString(fmt.Sprintf("--- Result from %q ---\n%s\n", r.AgentKey, r.Content)) if len(r.Deliverables) > 0 { for _, d := range r.Deliverables { preview := d if runes := []rune(preview); len(runes) > 4000 { preview = string(runes[:4000]) + "\n[...truncated, full content in team_tasks]" } - msg += fmt.Sprintf("\n[Deliverable]\n%s\n", preview) + msg.WriteString(fmt.Sprintf("\n[Deliverable]\n%s\n", preview)) } } if r.HasMedia { - msg += "[media file(s) attached — will be delivered automatically. Do NOT recreate or call create_image.]\n" + msg.WriteString("[media file(s) attached — will be delivered automatically. Do NOT recreate or call create_image.]\n") } if i < len(artifacts.Results)-1 { - msg += "\n" + msg.WriteString("\n") } } - msg += fmt.Sprintf("\nStats: total elapsed %s\n\n", elapsed.Round(time.Millisecond)) - msg += "Review the results above. You may:\n" + + msg.WriteString(fmt.Sprintf("\nStats: total elapsed %s\n\n", elapsed.Round(time.Millisecond))) + msg.WriteString("Review the results above. You may:\n" + "- Present a comprehensive summary to the user (if the task is fully done)\n" + "- Delegate follow-up tasks to refine, combine, or extend these results\n" + "- Ask a member to revise based on another member's output\n" + - "Any media files attached will be delivered automatically — do NOT recreate them." + "Any media files attached will be delivered automatically — do NOT recreate them.") - return msg + return msg.String() } diff --git a/internal/tools/delegate_policy.go b/internal/tools/delegate_policy.go index 4c5eba86..5ba98738 100644 --- a/internal/tools/delegate_policy.go +++ b/internal/tools/delegate_policy.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log/slog" + "slices" "github.com/google/uuid" @@ -21,16 +22,12 @@ func checkUserPermission(settings json.RawMessage, userID string) error { if json.Unmarshal(settings, &s) != nil { return nil // malformed = fail open } - for _, denied := range s.UserDeny { - if denied == userID { - return fmt.Errorf("you are not authorized to use this delegation link") - } + if slices.Contains(s.UserDeny, userID) { + return fmt.Errorf("you are not authorized to use this delegation link") } if len(s.UserAllow) > 0 { - for _, allowed := range s.UserAllow { - if allowed == userID { - return nil - } + if slices.Contains(s.UserAllow, userID) { + return nil } return fmt.Errorf("you are not authorized to use this delegation link") } @@ -67,19 +64,11 @@ func checkTeamAccess(settings json.RawMessage, userID, channel string) error { // User check: deny > allow if userID != "" { - for _, denied := range s.DenyUserIDs { - if denied == userID { - return fmt.Errorf("user not authorized for this team") - } + if slices.Contains(s.DenyUserIDs, userID) { + return fmt.Errorf("user not authorized for this team") } if len(s.AllowUserIDs) > 0 { - found := false - for _, allowed := range s.AllowUserIDs { - if allowed == userID { - found = true - break - } - } + found := slices.Contains(s.AllowUserIDs, userID) if !found { return fmt.Errorf("user not authorized for this team") } @@ -88,19 +77,11 @@ func checkTeamAccess(settings json.RawMessage, userID, channel string) error { // Channel check: deny > allow if channel != "" { - for _, denied := range s.DenyChannels { - if denied == channel { - return fmt.Errorf("channel %q not authorized for this team", channel) - } + if slices.Contains(s.DenyChannels, channel) { + return fmt.Errorf("channel %q not authorized for this team", channel) } if len(s.AllowChannels) > 0 { - found := false - for _, allowed := range s.AllowChannels { - if allowed == channel { - found = true - break - } - } + found := slices.Contains(s.AllowChannels, channel) if !found { return fmt.Errorf("channel %q not authorized for this team", channel) } @@ -233,12 +214,22 @@ func (dm *DelegateManager) applyQualityGates( UserID: task.UserID, Channel: task.OriginChannel, ChatID: task.OriginChatID, - TeamID: func() string { if task.TeamID != uuid.Nil { return task.TeamID.String() }; return "" }(), - TeamTaskID: func() string { if task.TeamTaskID != uuid.Nil { return task.TeamTaskID.String() }; return "" }(), - GateType: string(gate.Type), - Attempt: attempt + 1, - MaxRetries: retries, - Feedback: hookResult.Feedback, + TeamID: func() string { + if task.TeamID != uuid.Nil { + return task.TeamID.String() + } + return "" + }(), + TeamTaskID: func() string { + if task.TeamTaskID != uuid.Nil { + return task.TeamTaskID.String() + } + return "" + }(), + GateType: string(gate.Type), + Attempt: attempt + 1, + MaxRetries: retries, + Feedback: hookResult.Feedback, }, }) } diff --git a/internal/tools/delegate_search_tool.go b/internal/tools/delegate_search_tool.go index c7da080f..76f8bb95 100644 --- a/internal/tools/delegate_search_tool.go +++ b/internal/tools/delegate_search_tool.go @@ -29,15 +29,15 @@ func (t *DelegateSearchTool) Description() string { return "Search for available delegation target agents by keyword or description. Use this to find the right agent to delegate a task to." } -func (t *DelegateSearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *DelegateSearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Search keywords to find relevant agents", }, - "max_results": map[string]interface{}{ + "max_results": map[string]any{ "type": "integer", "description": "Maximum number of results (default 5)", }, @@ -46,7 +46,7 @@ func (t *DelegateSearchTool) Parameters() map[string]interface{} { } } -func (t *DelegateSearchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *DelegateSearchTool) Execute(ctx context.Context, args map[string]any) *Result { query, _ := args["query"].(string) if query == "" { return ErrorResult("query parameter is required") @@ -96,7 +96,7 @@ func (t *DelegateSearchTool) Execute(ctx context.Context, args map[string]interf }) } - data, _ := json.MarshalIndent(map[string]interface{}{ + data, _ := json.MarshalIndent(map[string]any{ "agents": out, "count": len(out), }, "", " ") diff --git a/internal/tools/dynamic_tool.go b/internal/tools/dynamic_tool.go index 8e14741a..f37f0bc9 100644 --- a/internal/tools/dynamic_tool.go +++ b/internal/tools/dynamic_tool.go @@ -17,29 +17,29 @@ import ( type DynamicTool struct { def store.CustomToolDef workspace string - params map[string]interface{} + params map[string]any } // NewDynamicTool creates a Tool from a DB-stored custom tool definition. func NewDynamicTool(def store.CustomToolDef, workspace string) *DynamicTool { - var params map[string]interface{} + var params map[string]any if len(def.Parameters) > 0 { json.Unmarshal(def.Parameters, ¶ms) } if params == nil { - params = map[string]interface{}{ + params = map[string]any{ "type": "object", - "properties": map[string]interface{}{}, + "properties": map[string]any{}, } } return &DynamicTool{def: def, workspace: workspace, params: params} } -func (t *DynamicTool) Name() string { return t.def.Name } -func (t *DynamicTool) Description() string { return t.def.Description } -func (t *DynamicTool) Parameters() map[string]interface{} { return t.params } +func (t *DynamicTool) Name() string { return t.def.Name } +func (t *DynamicTool) Description() string { return t.def.Description } +func (t *DynamicTool) Parameters() map[string]any { return t.params } -func (t *DynamicTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *DynamicTool) Execute(ctx context.Context, args map[string]any) *Result { // Render command template with shell-escaped args command := renderCommand(t.def.Command, args) @@ -117,7 +117,7 @@ func (t *DynamicTool) Execute(ctx context.Context, args map[string]interface{}) // renderCommand replaces {{.key}} placeholders with shell-escaped arg values. // Uses simple string replacement (NOT Go text/template) for security. -func renderCommand(tmpl string, args map[string]interface{}) string { +func renderCommand(tmpl string, args map[string]any) string { result := tmpl for key, val := range args { placeholder := "{{." + key + "}}" diff --git a/internal/tools/edit.go b/internal/tools/edit.go index e25a7779..a41909d9 100644 --- a/internal/tools/edit.go +++ b/internal/tools/edit.go @@ -56,23 +56,23 @@ func (t *EditTool) Description() string { return "Edit a file by replacing exact text matches. Use old_string/new_string for precise edits without rewriting the entire file." } -func (t *EditTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *EditTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ + "properties": map[string]any{ + "path": map[string]any{ "type": "string", "description": "Path to the file to edit", }, - "old_string": map[string]interface{}{ + "old_string": map[string]any{ "type": "string", "description": "Exact text to find (must match uniquely unless replace_all is true)", }, - "new_string": map[string]interface{}{ + "new_string": map[string]any{ "type": "string", "description": "Replacement text", }, - "replace_all": map[string]interface{}{ + "replace_all": map[string]any{ "type": "boolean", "description": "Replace all occurrences (default: false, requires unique match)", }, @@ -81,7 +81,7 @@ func (t *EditTool) Parameters() map[string]interface{} { } } -func (t *EditTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *EditTool) Execute(ctx context.Context, args map[string]any) *Result { path, _ := args["path"].(string) oldStr, _ := args["old_string"].(string) newStr, _ := args["new_string"].(string) diff --git a/internal/tools/evaluate_loop_tool.go b/internal/tools/evaluate_loop_tool.go index fb6482b8..63099665 100644 --- a/internal/tools/evaluate_loop_tool.go +++ b/internal/tools/evaluate_loop_tool.go @@ -33,35 +33,35 @@ func (t *EvaluateLoopTool) Description() string { "generator revises until approved or max rounds reached." } -func (t *EvaluateLoopTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *EvaluateLoopTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "generator": map[string]interface{}{ + "properties": map[string]any{ + "generator": map[string]any{ "type": "string", "description": "Agent key for the content generator", }, - "evaluator": map[string]interface{}{ + "evaluator": map[string]any{ "type": "string", "description": "Agent key for the quality evaluator", }, - "task": map[string]interface{}{ + "task": map[string]any{ "type": "string", "description": "Initial task for the generator", }, - "max_rounds": map[string]interface{}{ + "max_rounds": map[string]any{ "type": "number", "description": "Maximum generate-evaluate rounds (default 3, max 5)", }, - "pass_criteria": map[string]interface{}{ + "pass_criteria": map[string]any{ "type": "string", "description": "Criteria the evaluator uses to approve/reject output", }, - "context": map[string]interface{}{ + "context": map[string]any{ "type": "string", "description": "Optional additional context for both agents", }, - "team_task_id": map[string]interface{}{ + "team_task_id": map[string]any{ "type": "string", "description": "Optional team task ID for auto-completion on success", }, @@ -70,7 +70,7 @@ func (t *EvaluateLoopTool) Parameters() map[string]interface{} { } } -func (t *EvaluateLoopTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *EvaluateLoopTool) Execute(ctx context.Context, args map[string]any) *Result { generatorKey, _ := args["generator"].(string) evaluatorKey, _ := args["evaluator"].(string) task, _ := args["task"].(string) @@ -81,10 +81,7 @@ func (t *EvaluateLoopTool) Execute(ctx context.Context, args map[string]interfac maxRounds := defaultMaxRounds if v, ok := args["max_rounds"].(float64); ok && int(v) > 0 { - maxRounds = int(v) - if maxRounds > maxAllowedRounds { - maxRounds = maxAllowedRounds - } + maxRounds = min(int(v), maxAllowedRounds) } passCriteria, _ := args["pass_criteria"].(string) diff --git a/internal/tools/filesystem.go b/internal/tools/filesystem.go index fc6cd5bf..c5e99b03 100644 --- a/internal/tools/filesystem.go +++ b/internal/tools/filesystem.go @@ -17,8 +17,8 @@ import ( // virtualSystemFiles are files dynamically injected into the system prompt. // They don't exist on disk — if the model tries to read them, return a hint. var virtualSystemFiles = map[string]string{ - bootstrap.TeamFile: "TEAM.md is already loaded in your system prompt. Refer to the TEAM.md section in your context above for team member information.", - bootstrap.DelegationFile: "DELEGATION.md is already loaded in your system prompt. Refer to the DELEGATION.md section in your context above for delegation instructions and available agents.", + bootstrap.TeamFile: "TEAM.md is already loaded in your system prompt. Refer to the TEAM.md section in your context above for team member information.", + bootstrap.DelegationFile: "DELEGATION.md is already loaded in your system prompt. Refer to the DELEGATION.md section in your context above for delegation instructions and available agents.", bootstrap.AvailabilityFile: "AVAILABILITY.md is already loaded in your system prompt. Refer to the AVAILABILITY.md section in your context above for agent availability information.", } @@ -26,12 +26,12 @@ var virtualSystemFiles = map[string]string{ type ReadFileTool struct { workspace string restrict bool - allowedPrefixes []string // extra allowed path prefixes (e.g. skills dirs) - deniedPrefixes []string // path prefixes to deny access to (e.g. .goclaw) - sandboxMgr sandbox.Manager // nil = direct host access + allowedPrefixes []string // extra allowed path prefixes (e.g. skills dirs) + deniedPrefixes []string // path prefixes to deny access to (e.g. .goclaw) + sandboxMgr sandbox.Manager // nil = direct host access contextFileIntc *ContextFileInterceptor // nil = no virtual FS routing memIntc *MemoryInterceptor // nil = no memory routing - groupWriterCache *store.GroupWriterCache // nil = no group read restriction + groupWriterCache *store.GroupWriterCache // nil = no group read restriction } // SetContextFileInterceptor enables virtual FS routing for context files. @@ -73,11 +73,11 @@ func (t *ReadFileTool) SetSandboxKey(key string) {} func (t *ReadFileTool) Name() string { return "read_file" } func (t *ReadFileTool) Description() string { return "Read the contents of a file" } -func (t *ReadFileTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ReadFileTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ + "properties": map[string]any{ + "path": map[string]any{ "type": "string", "description": "Path to the file to read", }, @@ -86,7 +86,7 @@ func (t *ReadFileTool) Parameters() map[string]interface{} { } } -func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *Result { path, _ := args["path"].(string) if path == "" { return ErrorResult("path is required") diff --git a/internal/tools/filesystem_list.go b/internal/tools/filesystem_list.go index aeecbda0..e58ff6b1 100644 --- a/internal/tools/filesystem_list.go +++ b/internal/tools/filesystem_list.go @@ -46,11 +46,11 @@ func (t *ListFilesTool) SetSandboxKey(key string) {} func (t *ListFilesTool) Name() string { return "list_files" } func (t *ListFilesTool) Description() string { return "List files and directories in a path" } -func (t *ListFilesTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ListFilesTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ + "properties": map[string]any{ + "path": map[string]any{ "type": "string", "description": "Directory path to list (default: workspace root)", }, @@ -58,7 +58,7 @@ func (t *ListFilesTool) Parameters() map[string]interface{} { } } -func (t *ListFilesTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ListFilesTool) Execute(ctx context.Context, args map[string]any) *Result { path, _ := args["path"].(string) if path == "" { path = "." diff --git a/internal/tools/filesystem_write.go b/internal/tools/filesystem_write.go index ce70c83f..c7b51e47 100644 --- a/internal/tools/filesystem_write.go +++ b/internal/tools/filesystem_write.go @@ -19,7 +19,7 @@ type WriteFileTool struct { sandboxMgr sandbox.Manager contextFileIntc *ContextFileInterceptor // nil = no virtual FS routing memIntc *MemoryInterceptor // nil = no memory routing - groupWriterCache *store.GroupWriterCache // nil = no group write restriction + groupWriterCache *store.GroupWriterCache // nil = no group write restriction } // DenyPaths adds path prefixes that write_file must reject. @@ -53,21 +53,23 @@ func NewSandboxedWriteFileTool(workspace string, restrict bool, mgr sandbox.Mana // SetSandboxKey is a no-op; sandbox key is now read from ctx (thread-safe). func (t *WriteFileTool) SetSandboxKey(key string) {} -func (t *WriteFileTool) Name() string { return "write_file" } -func (t *WriteFileTool) Description() string { return "Write content to a file, creating directories as needed" } -func (t *WriteFileTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *WriteFileTool) Name() string { return "write_file" } +func (t *WriteFileTool) Description() string { + return "Write content to a file, creating directories as needed" +} +func (t *WriteFileTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ + "properties": map[string]any{ + "path": map[string]any{ "type": "string", "description": "Path to the file to write", }, - "content": map[string]interface{}{ + "content": map[string]any{ "type": "string", "description": "Content to write", }, - "deliver": map[string]interface{}{ + "deliver": map[string]any{ "type": "boolean", "description": "If true, deliver this file to the user as an attachment (image, document, etc.)", }, @@ -76,7 +78,7 @@ func (t *WriteFileTool) Parameters() map[string]interface{} { } } -func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *Result { path, _ := args["path"].(string) content, _ := args["content"].(string) deliver, _ := args["deliver"].(bool) diff --git a/internal/tools/gemini_file_api.go b/internal/tools/gemini_file_api.go index 82e5dcd3..351e516c 100644 --- a/internal/tools/gemini_file_api.go +++ b/internal/tools/gemini_file_api.go @@ -25,7 +25,7 @@ const ( // Returns the file name (e.g. "files/abc123") and file URI for use in generateContent. func geminiFileUpload(ctx context.Context, apiKey, displayName string, data []byte, mime string) (fileName, fileURI string, err error) { // Step 1: Initiate resumable upload. - initBody, _ := json.Marshal(map[string]interface{}{ + initBody, _ := json.Marshal(map[string]any{ "file": map[string]string{"display_name": displayName}, }) initReq, err := http.NewRequestWithContext(ctx, "POST", geminiUploadBase+"?key="+apiKey, bytes.NewReader(initBody)) @@ -97,7 +97,7 @@ func geminiFileUpload(ctx context.Context, apiKey, displayName string, data []by func geminiFilePoll(ctx context.Context, apiKey, fileName string) (fileURI string, err error) { url := fmt.Sprintf("%s/%s?key=%s", geminiFilesBase, fileName, apiKey) - for i := 0; i < geminiFilePollMax; i++ { + for i := range geminiFilePollMax { select { case <-ctx.Done(): return "", ctx.Err() @@ -165,16 +165,16 @@ func geminiFileAPICall(ctx context.Context, apiKey, model, prompt string, data [ slog.Info("gemini file api: file active", "uri", fileURI) // Call generateContent with file_data reference. - body := map[string]interface{}{ - "contents": []map[string]interface{}{ + body := map[string]any{ + "contents": []map[string]any{ { - "parts": []map[string]interface{}{ - {"file_data": map[string]interface{}{"mime_type": mime, "file_uri": fileURI}}, + "parts": []map[string]any{ + {"file_data": map[string]any{"mime_type": mime, "file_uri": fileURI}}, {"text": prompt}, }, }, }, - "generationConfig": map[string]interface{}{ + "generationConfig": map[string]any{ "maxOutputTokens": 16384, "temperature": 0.2, }, diff --git a/internal/tools/handoff_tool.go b/internal/tools/handoff_tool.go index 911ed74f..9f0622af 100644 --- a/internal/tools/handoff_tool.go +++ b/internal/tools/handoff_tool.go @@ -44,23 +44,23 @@ func (t *HandoffTool) Description() string { "Use action=transfer to hand off, action=clear to remove a previous handoff." } -func (t *HandoffTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *HandoffTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "description": "'transfer' (default) or 'clear'", }, - "agent": map[string]interface{}{ + "agent": map[string]any{ "type": "string", "description": "Target agent key (required for action=transfer)", }, - "reason": map[string]interface{}{ + "reason": map[string]any{ "type": "string", "description": "Why the handoff is happening (required for action=transfer)", }, - "transfer_context": map[string]interface{}{ + "transfer_context": map[string]any{ "type": "boolean", "description": "Pass conversation summary to target agent (default true)", }, @@ -69,7 +69,7 @@ func (t *HandoffTool) Parameters() map[string]interface{} { } } -func (t *HandoffTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *HandoffTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) if action == "" { action = "transfer" @@ -85,7 +85,7 @@ func (t *HandoffTool) Execute(ctx context.Context, args map[string]interface{}) } } -func (t *HandoffTool) executeTransfer(ctx context.Context, args map[string]interface{}) *Result { +func (t *HandoffTool) executeTransfer(ctx context.Context, args map[string]any) *Result { targetAgentKey, _ := args["agent"].(string) if targetAgentKey == "" { return ErrorResult("agent is required for handoff") diff --git a/internal/tools/knowledge_graph.go b/internal/tools/knowledge_graph.go index 0a29c48d..5e81660e 100644 --- a/internal/tools/knowledge_graph.go +++ b/internal/tools/knowledge_graph.go @@ -31,23 +31,23 @@ func (t *KnowledgeGraphSearchTool) Description() string { return "Search the knowledge graph for entities and their relationships. Use specific names or keywords (e.g. a person's name, project name) — not generic words like 'entities' or 'all'. Use query='*' to list all entities. Supports graph traversal via entity_id to discover multi-hop relationships." } -func (t *KnowledgeGraphSearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *KnowledgeGraphSearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Search query for entity names or descriptions", }, - "entity_type": map[string]interface{}{ + "entity_type": map[string]any{ "type": "string", "description": "Filter by entity type (person, project, task, event, concept, location, organization)", }, - "entity_id": map[string]interface{}{ + "entity_id": map[string]any{ "type": "string", "description": "Entity ID to traverse from (for relationship discovery)", }, - "max_depth": map[string]interface{}{ + "max_depth": map[string]any{ "type": "number", "description": "Maximum traversal depth (default 2, max 3)", }, @@ -56,7 +56,7 @@ func (t *KnowledgeGraphSearchTool) Parameters() map[string]interface{} { } } -func (t *KnowledgeGraphSearchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *KnowledgeGraphSearchTool) Execute(ctx context.Context, args map[string]any) *Result { if t.kgStore == nil { return NewResult("Knowledge graph is not enabled for this agent.") } @@ -75,10 +75,7 @@ func (t *KnowledgeGraphSearchTool) Execute(ctx context.Context, args map[string] entityID, _ := args["entity_id"].(string) maxDepth := 2 if md, ok := args["max_depth"].(float64); ok && md > 0 { - maxDepth = int(md) - if maxDepth > 3 { - maxDepth = 3 - } + maxDepth = min(int(md), 3) } // Traversal mode: entity_id provided @@ -142,7 +139,7 @@ func (t *KnowledgeGraphSearchTool) executeListAll(ctx context.Context, agentID, return NewResult(sb.String()) } -func (t *KnowledgeGraphSearchTool) executeSearch(ctx context.Context, agentID, userID, query string, args map[string]interface{}) *Result { +func (t *KnowledgeGraphSearchTool) executeSearch(ctx context.Context, agentID, userID, query string, args map[string]any) *Result { entities, err := t.kgStore.SearchEntities(ctx, agentID, userID, query, 10) if err != nil { return ErrorResult(fmt.Sprintf("entity search failed: %v", err)) diff --git a/internal/tools/memory.go b/internal/tools/memory.go index 9a468af4..ae1bfddc 100644 --- a/internal/tools/memory.go +++ b/internal/tools/memory.go @@ -31,19 +31,19 @@ func (t *MemorySearchTool) Description() string { return "Mandatory recall step: semantically search MEMORY.md + memory/*.md before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user. IMPORTANT: Always query in the SAME language as the stored memory content. If the user speaks Vietnamese, search in Vietnamese. If memory was written in English, search in English. Matching the language dramatically improves search accuracy." } -func (t *MemorySearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *MemorySearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Natural language search query. Must be in the same language as the stored memory content (e.g., Vietnamese if memory is in Vietnamese).", }, - "maxResults": map[string]interface{}{ + "maxResults": map[string]any{ "type": "number", "description": "Maximum number of results to return (default: 6)", }, - "minScore": map[string]interface{}{ + "minScore": map[string]any{ "type": "number", "description": "Minimum relevance score threshold (0-1)", }, @@ -52,7 +52,7 @@ func (t *MemorySearchTool) Parameters() map[string]interface{} { } } -func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]any) *Result { query, _ := args["query"].(string) if query == "" { return ErrorResult("query parameter is required") @@ -84,7 +84,7 @@ func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]interfac return NewResult("No memory results found for query: " + query) } - data, _ := json.MarshalIndent(map[string]interface{}{ + data, _ := json.MarshalIndent(map[string]any{ "results": results, "count": len(results), }, "", " ") @@ -111,19 +111,19 @@ func (t *MemoryGetTool) Description() string { return "Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; use after memory_search to pull only the needed lines and keep context small." } -func (t *MemoryGetTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *MemoryGetTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ + "properties": map[string]any{ + "path": map[string]any{ "type": "string", "description": "Relative path to memory file (e.g., 'MEMORY.md' or 'memory/notes.md')", }, - "from": map[string]interface{}{ + "from": map[string]any{ "type": "number", "description": "Start line number (1-indexed). Omit to read from beginning.", }, - "lines": map[string]interface{}{ + "lines": map[string]any{ "type": "number", "description": "Number of lines to read. Omit to read entire file.", }, @@ -132,7 +132,7 @@ func (t *MemoryGetTool) Parameters() map[string]interface{} { } } -func (t *MemoryGetTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *MemoryGetTool) Execute(ctx context.Context, args map[string]any) *Result { path, _ := args["path"].(string) if path == "" { return ErrorResult("path parameter is required") @@ -168,7 +168,7 @@ func (t *MemoryGetTool) Execute(ctx context.Context, args map[string]interface{} return NewResult(fmt.Sprintf("File %s is empty or the specified range has no content.", path)) } - data, _ := json.MarshalIndent(map[string]interface{}{ + data, _ := json.MarshalIndent(map[string]any{ "path": path, "text": text, }, "", " ") diff --git a/internal/tools/message.go b/internal/tools/message.go index 94455bc4..f0369e1c 100644 --- a/internal/tools/message.go +++ b/internal/tools/message.go @@ -21,31 +21,31 @@ type MessageTool struct { func NewMessageTool() *MessageTool { return &MessageTool{} } func (t *MessageTool) SetChannelSender(s ChannelSender) { t.sender = s } -func (t *MessageTool) SetMessageBus(b *bus.MessageBus) { t.msgBus = b } +func (t *MessageTool) SetMessageBus(b *bus.MessageBus) { t.msgBus = b } func (t *MessageTool) Name() string { return "message" } func (t *MessageTool) Description() string { return "Send a message to a channel (Telegram, Discord, Slack, Zalo, Feishu/Lark, WhatsApp, etc.) or the current chat. Channel and target are auto-filled from context." } -func (t *MessageTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *MessageTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "description": "Action to perform: 'send'", "enum": []string{"send"}, }, - "channel": map[string]interface{}{ + "channel": map[string]any{ "type": "string", "description": "Channel name (default: current channel from context)", }, - "target": map[string]interface{}{ + "target": map[string]any{ "type": "string", "description": "Chat ID to send to (default: current chat from context)", }, - "message": map[string]interface{}{ + "message": map[string]any{ "type": "string", "description": "Message content to send", }, @@ -54,7 +54,7 @@ func (t *MessageTool) Parameters() map[string]interface{} { } } -func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) if action != "send" { return ErrorResult(fmt.Sprintf("unsupported action: %s (only 'send' is supported)", action)) diff --git a/internal/tools/openai_compat_call.go b/internal/tools/openai_compat_call.go index 1cd7dda2..3f874ecd 100644 --- a/internal/tools/openai_compat_call.go +++ b/internal/tools/openai_compat_call.go @@ -16,7 +16,7 @@ import ( // callOpenAICompatJSON sends a raw JSON body to an OpenAI-compatible chat/completions endpoint // and parses the response into a ChatResponse. Used by audio/video tools that need custom // message formats not supported by the generic providers.ChatRequest interface. -func callOpenAICompatJSON(ctx context.Context, apiKey, baseURL string, body map[string]interface{}, timeout time.Duration) (*providers.ChatResponse, error) { +func callOpenAICompatJSON(ctx context.Context, apiKey, baseURL string, body map[string]any, timeout time.Duration) (*providers.ChatResponse, error) { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) diff --git a/internal/tools/policy.go b/internal/tools/policy.go index 7a8bfae4..c617f8e6 100644 --- a/internal/tools/policy.go +++ b/internal/tools/policy.go @@ -218,8 +218,8 @@ func (pe *PolicyEngine) applyProfile(allTools []string, profile string) []string func expandSpec(available []string, spec []string) []string { expanded := make(map[string]bool) for _, s := range spec { - if strings.HasPrefix(s, "group:") { - groupName := strings.TrimPrefix(s, "group:") + if after, ok := strings.CutPrefix(s, "group:"); ok { + groupName := after if members, ok := toolGroups[groupName]; ok { for _, m := range members { expanded[m] = true @@ -243,8 +243,8 @@ func expandSpec(available []string, spec []string) []string { func intersectWithSpec(current []string, spec []string) []string { expanded := make(map[string]bool) for _, s := range spec { - if strings.HasPrefix(s, "group:") { - groupName := strings.TrimPrefix(s, "group:") + if after, ok := strings.CutPrefix(s, "group:"); ok { + groupName := after if members, ok := toolGroups[groupName]; ok { for _, m := range members { expanded[m] = true @@ -268,8 +268,8 @@ func intersectWithSpec(current []string, spec []string) []string { func subtractSpec(current []string, spec []string) []string { denied := make(map[string]bool) for _, s := range spec { - if strings.HasPrefix(s, "group:") { - groupName := strings.TrimPrefix(s, "group:") + if after, ok := strings.CutPrefix(s, "group:"); ok { + groupName := after if members, ok := toolGroups[groupName]; ok { for _, m := range members { denied[m] = true diff --git a/internal/tools/rate_limiter_test.go b/internal/tools/rate_limiter_test.go index 306a9856..70cf20cc 100644 --- a/internal/tools/rate_limiter_test.go +++ b/internal/tools/rate_limiter_test.go @@ -21,7 +21,7 @@ func TestNewToolRateLimiter_Negative(t *testing.T) { func TestToolRateLimiter_AllowUnderLimit(t *testing.T) { rl := NewToolRateLimiter(5) - for i := 0; i < 5; i++ { + for i := range 5 { if err := rl.Allow("user1"); err != nil { t.Errorf("action %d should be allowed: %v", i, err) } @@ -31,7 +31,7 @@ func TestToolRateLimiter_AllowUnderLimit(t *testing.T) { func TestToolRateLimiter_BlockOverLimit(t *testing.T) { rl := NewToolRateLimiter(3) - for i := 0; i < 3; i++ { + for i := range 3 { if err := rl.Allow("user1"); err != nil { t.Fatalf("action %d should be allowed: %v", i, err) } diff --git a/internal/tools/read_audio.go b/internal/tools/read_audio.go index 285aed08..e0a92283 100644 --- a/internal/tools/read_audio.go +++ b/internal/tools/read_audio.go @@ -59,15 +59,15 @@ func (t *ReadAudioTool) Description() string { "Specify what you want to extract or analyze." } -func (t *ReadAudioTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ReadAudioTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "What to analyze. E.g. 'Transcribe this audio', 'Summarize the conversation', 'What language is spoken?'", }, - "media_id": map[string]interface{}{ + "media_id": map[string]any{ "type": "string", "description": "Optional: specific media_id from tag. If omitted, uses most recent audio.", }, @@ -76,7 +76,7 @@ func (t *ReadAudioTool) Parameters() map[string]interface{} { } } -func (t *ReadAudioTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ReadAudioTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { prompt = "Analyze this audio and describe its contents." diff --git a/internal/tools/read_audio_resolve.go b/internal/tools/read_audio_resolve.go index ac3e1895..c8405248 100644 --- a/internal/tools/read_audio_resolve.go +++ b/internal/tools/read_audio_resolve.go @@ -96,7 +96,7 @@ func (t *ReadAudioTool) callProvider(ctx context.Context, cp credentialProvider, }, }, Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 16384, "temperature": 0.2, }, @@ -120,12 +120,12 @@ func openaiAudioCall(ctx context.Context, apiKey, baseURL, model, prompt string, b64 := base64.StdEncoding.EncodeToString(data) - body := map[string]interface{}{ + body := map[string]any{ "model": model, - "messages": []map[string]interface{}{ + "messages": []map[string]any{ { "role": "user", - "content": []map[string]interface{}{ + "content": []map[string]any{ {"type": "text", "text": prompt}, {"type": "input_audio", "input_audio": map[string]string{ "data": b64, diff --git a/internal/tools/read_document.go b/internal/tools/read_document.go index d3bbfc17..1480588c 100644 --- a/internal/tools/read_document.go +++ b/internal/tools/read_document.go @@ -78,15 +78,15 @@ func (t *ReadDocumentTool) Description() string { "Specify what you want to extract or analyze." } -func (t *ReadDocumentTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ReadDocumentTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "What to analyze. E.g. 'Extract all tables', 'Summarize key findings', 'What does page 3 say?'", }, - "media_id": map[string]interface{}{ + "media_id": map[string]any{ "type": "string", "description": "Optional: specific media_id from tag. If omitted, uses most recent document.", }, @@ -95,7 +95,7 @@ func (t *ReadDocumentTool) Parameters() map[string]interface{} { } } -func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { prompt = "Analyze this document and describe its contents." diff --git a/internal/tools/read_document_gemini.go b/internal/tools/read_document_gemini.go index ee554379..c086e1c1 100644 --- a/internal/tools/read_document_gemini.go +++ b/internal/tools/read_document_gemini.go @@ -20,14 +20,14 @@ func geminiNativeDocumentCall(ctx context.Context, apiKey, model, prompt string, b64 := base64.StdEncoding.EncodeToString(docData) // Gemini generateContent request body. - body := map[string]interface{}{ - "contents": []map[string]interface{}{ + body := map[string]any{ + "contents": []map[string]any{ { - "parts": []map[string]interface{}{ + "parts": []map[string]any{ { - "inline_data": map[string]interface{}{ + "inline_data": map[string]any{ "mime_type": docMime, - "data": b64, + "data": b64, }, }, { @@ -36,7 +36,7 @@ func geminiNativeDocumentCall(ctx context.Context, apiKey, model, prompt string, }, }, }, - "generationConfig": map[string]interface{}{ + "generationConfig": map[string]any{ "maxOutputTokens": 16384, "temperature": 0.2, }, @@ -117,4 +117,3 @@ func geminiNativeDocumentCall(ctx context.Context, apiKey, model, prompt string, }, }, nil } - diff --git a/internal/tools/read_document_resolve.go b/internal/tools/read_document_resolve.go index 60dd9b80..3ce994b1 100644 --- a/internal/tools/read_document_resolve.go +++ b/internal/tools/read_document_resolve.go @@ -89,7 +89,7 @@ func (t *ReadDocumentTool) callProvider(ctx context.Context, cp credentialProvid }, }, Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 16384, "temperature": 0.2, }, diff --git a/internal/tools/read_image.go b/internal/tools/read_image.go index 712c8758..980efa3e 100644 --- a/internal/tools/read_image.go +++ b/internal/tools/read_image.go @@ -51,11 +51,11 @@ func (t *ReadImageTool) Description() string { return "Analyze images attached to the current message using a vision model. Use this when you see tags but cannot view images directly." } -func (t *ReadImageTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ReadImageTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "What you want to know about the image(s). E.g. 'Describe this image in detail' or 'What text is in this image?'", }, @@ -64,7 +64,7 @@ func (t *ReadImageTool) Parameters() map[string]interface{} { } } -func (t *ReadImageTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ReadImageTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { prompt = "Describe this image in detail." @@ -128,7 +128,7 @@ func (t *ReadImageTool) callProvider(ctx context.Context, cp credentialProvider, }, }, Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 1024, "temperature": 0.3, }, diff --git a/internal/tools/read_video.go b/internal/tools/read_video.go index 23f6ea8a..b6be69fb 100644 --- a/internal/tools/read_video.go +++ b/internal/tools/read_video.go @@ -59,15 +59,15 @@ func (t *ReadVideoTool) Description() string { "Specify what you want to extract or analyze." } -func (t *ReadVideoTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ReadVideoTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "prompt": map[string]interface{}{ + "properties": map[string]any{ + "prompt": map[string]any{ "type": "string", "description": "What to analyze. E.g. 'Describe what happens in this video', 'Summarize the key scenes', 'What text appears on screen?'", }, - "media_id": map[string]interface{}{ + "media_id": map[string]any{ "type": "string", "description": "Optional: specific media_id from tag. If omitted, uses most recent video.", }, @@ -76,7 +76,7 @@ func (t *ReadVideoTool) Parameters() map[string]interface{} { } } -func (t *ReadVideoTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ReadVideoTool) Execute(ctx context.Context, args map[string]any) *Result { prompt, _ := args["prompt"].(string) if prompt == "" { prompt = "Analyze this video and describe its contents." diff --git a/internal/tools/read_video_resolve.go b/internal/tools/read_video_resolve.go index f6a6b13e..bd4f9831 100644 --- a/internal/tools/read_video_resolve.go +++ b/internal/tools/read_video_resolve.go @@ -85,7 +85,7 @@ func (t *ReadVideoTool) callProvider(ctx context.Context, cp credentialProvider, }, }, Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 16384, "temperature": 0.2, }, diff --git a/internal/tools/registry.go b/internal/tools/registry.go index 09f4bc10..5fb54a36 100644 --- a/internal/tools/registry.go +++ b/internal/tools/registry.go @@ -3,6 +3,7 @@ package tools import ( "context" "log/slog" + "maps" "sync" "time" @@ -57,7 +58,7 @@ func (r *Registry) Unregister(name string) { } // Execute runs a tool by name with the given arguments. -func (r *Registry) Execute(ctx context.Context, name string, args map[string]interface{}) *Result { +func (r *Registry) Execute(ctx context.Context, name string, args map[string]any) *Result { return r.ExecuteWithContext(ctx, name, args, "", "", "", "", nil) } @@ -67,7 +68,7 @@ func (r *Registry) Execute(ctx context.Context, name string, args map[string]int // // Context values are injected into ctx so tools can read them without mutable fields, // making tool instances thread-safe for concurrent execution. -func (r *Registry) ExecuteWithContext(ctx context.Context, name string, args map[string]interface{}, channel, chatID, peerKind, sessionKey string, asyncCB AsyncCallback) *Result { +func (r *Registry) ExecuteWithContext(ctx context.Context, name string, args map[string]any, channel, chatID, peerKind, sessionKey string, asyncCB AsyncCallback) *Result { r.mu.RLock() tool, ok := r.tools[name] r.mu.RUnlock() @@ -166,8 +167,6 @@ func (r *Registry) Clone() *Registry { rateLimiter: r.rateLimiter, scrubbing: r.scrubbing, } - for name, tool := range r.tools { - clone.tools[name] = tool - } + maps.Copy(clone.tools, r.tools) return clone } diff --git a/internal/tools/registry_test.go b/internal/tools/registry_test.go index ed5ce173..8ce722fb 100644 --- a/internal/tools/registry_test.go +++ b/internal/tools/registry_test.go @@ -8,15 +8,15 @@ import ( // mockTool is a minimal tool for testing the registry. type mockTool struct { name string - execFn func(ctx context.Context, args map[string]interface{}) *Result + execFn func(ctx context.Context, args map[string]any) *Result } func (m *mockTool) Name() string { return m.name } func (m *mockTool) Description() string { return "mock tool" } -func (m *mockTool) Parameters() map[string]interface{} { - return map[string]interface{}{"type": "object", "properties": map[string]interface{}{}} +func (m *mockTool) Parameters() map[string]any { + return map[string]any{"type": "object", "properties": map[string]any{}} } -func (m *mockTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (m *mockTool) Execute(ctx context.Context, args map[string]any) *Result { if m.execFn != nil { return m.execFn(ctx, args) } @@ -79,7 +79,7 @@ func TestRegistry_ExecuteWithContext_InjectsContextValues(t *testing.T) { reg.Register(&mockTool{ name: "ctx_tool", - execFn: func(ctx context.Context, args map[string]interface{}) *Result { + execFn: func(ctx context.Context, args map[string]any) *Result { gotChannel = ToolChannelFromCtx(ctx) gotChatID = ToolChatIDFromCtx(ctx) gotPeerKind = ToolPeerKindFromCtx(ctx) @@ -120,7 +120,7 @@ func TestRegistry_ExecuteWithContext_ScrubsCredentials(t *testing.T) { reg := NewRegistry() reg.Register(&mockTool{ name: "leaky_tool", - execFn: func(ctx context.Context, args map[string]interface{}) *Result { + execFn: func(ctx context.Context, args map[string]any) *Result { return &Result{ ForLLM: "key is sk-abcdefghijklmnopqrstuvwxyz1234567890", ForUser: "token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij", @@ -144,7 +144,7 @@ func TestRegistry_ExecuteWithContext_RateLimiting(t *testing.T) { reg.Register(&mockTool{name: "rl_tool"}) // First 2 calls allowed - for i := 0; i < 2; i++ { + for i := range 2 { result := reg.ExecuteWithContext(context.Background(), "rl_tool", nil, "", "", "", "session-1", nil) if result.IsError { @@ -173,7 +173,7 @@ func TestRegistry_ExecuteWithContext_NoRateLimitWithoutSessionKey(t *testing.T) reg.Register(&mockTool{name: "tool"}) // Without sessionKey, rate limiting is skipped - for i := 0; i < 5; i++ { + for i := range 5 { result := reg.ExecuteWithContext(context.Background(), "tool", nil, "", "", "", "", nil) if result.IsError { @@ -188,7 +188,7 @@ func TestRegistry_ExecuteWithContext_EmptyContextValues(t *testing.T) { var gotChannel, gotSandboxKey string reg.Register(&mockTool{ name: "empty_ctx", - execFn: func(ctx context.Context, args map[string]interface{}) *Result { + execFn: func(ctx context.Context, args map[string]any) *Result { gotChannel = ToolChannelFromCtx(ctx) gotSandboxKey = ToolSandboxKeyFromCtx(ctx) return NewResult("ok") diff --git a/internal/tools/sessions.go b/internal/tools/sessions.go index 1b1b870b..6c93fe91 100644 --- a/internal/tools/sessions.go +++ b/internal/tools/sessions.go @@ -27,15 +27,15 @@ func (t *SessionsListTool) Description() string { return "List sessions for this agent with optional filters." } -func (t *SessionsListTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *SessionsListTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "limit": map[string]interface{}{ + "properties": map[string]any{ + "limit": map[string]any{ "type": "number", "description": "Max sessions to return (default 20)", }, - "active_minutes": map[string]interface{}{ + "active_minutes": map[string]any{ "type": "number", "description": "Only show sessions active in the last N minutes", }, @@ -43,7 +43,7 @@ func (t *SessionsListTool) Parameters() map[string]interface{} { } } -func (t *SessionsListTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SessionsListTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } @@ -93,7 +93,7 @@ func (t *SessionsListTool) Execute(ctx context.Context, args map[string]interfac }) } - out, _ := json.Marshal(map[string]interface{}{ + out, _ := json.Marshal(map[string]any{ "count": len(entries), "sessions": entries, }) @@ -117,11 +117,11 @@ func (t *SessionStatusTool) Description() string { return "Show session status: model, tokens, compaction count, channel, last update." } -func (t *SessionStatusTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *SessionStatusTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "session_key": map[string]interface{}{ + "properties": map[string]any{ + "session_key": map[string]any{ "type": "string", "description": "Session key to inspect (default: current session)", }, @@ -129,7 +129,7 @@ func (t *SessionStatusTool) Parameters() map[string]interface{} { } } -func (t *SessionStatusTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SessionStatusTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } diff --git a/internal/tools/sessions_history.go b/internal/tools/sessions_history.go index c09ab6c8..5ae5823f 100644 --- a/internal/tools/sessions_history.go +++ b/internal/tools/sessions_history.go @@ -32,19 +32,19 @@ func (t *SessionsHistoryTool) Description() string { return "Fetch message history for a session." } -func (t *SessionsHistoryTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *SessionsHistoryTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "session_key": map[string]interface{}{ + "properties": map[string]any{ + "session_key": map[string]any{ "type": "string", "description": "Session key to fetch history from", }, - "limit": map[string]interface{}{ + "limit": map[string]any{ "type": "number", "description": "Max messages to return (default 20)", }, - "include_tools": map[string]interface{}{ + "include_tools": map[string]any{ "type": "boolean", "description": "Include tool call/result messages (default false)", }, @@ -53,7 +53,7 @@ func (t *SessionsHistoryTool) Parameters() map[string]interface{} { } } -func (t *SessionsHistoryTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SessionsHistoryTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } @@ -111,7 +111,7 @@ func (t *SessionsHistoryTool) Execute(ctx context.Context, args map[string]inter entries = entries[len(entries)-limit:] } - out, _ := json.Marshal(map[string]interface{}{ + out, _ := json.Marshal(map[string]any{ "session_key": sessionKey, "messages": entries, "count": len(entries), diff --git a/internal/tools/sessions_send.go b/internal/tools/sessions_send.go index 10f938c2..c9134843 100644 --- a/internal/tools/sessions_send.go +++ b/internal/tools/sessions_send.go @@ -28,19 +28,19 @@ func (t *SessionsSendTool) Description() string { return "Send a message into another session. Use session_key or label to identify the target." } -func (t *SessionsSendTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *SessionsSendTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "session_key": map[string]interface{}{ + "properties": map[string]any{ + "session_key": map[string]any{ "type": "string", "description": "Target session key", }, - "label": map[string]interface{}{ + "label": map[string]any{ "type": "string", "description": "Target session label (alternative to session_key)", }, - "message": map[string]interface{}{ + "message": map[string]any{ "type": "string", "description": "Message to send", }, @@ -49,7 +49,7 @@ func (t *SessionsSendTool) Parameters() map[string]interface{} { } } -func (t *SessionsSendTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SessionsSendTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } diff --git a/internal/tools/shell.go b/internal/tools/shell.go index d563aa8b..06c4c468 100644 --- a/internal/tools/shell.go +++ b/internal/tools/shell.go @@ -34,11 +34,11 @@ var defaultDenyPatterns = []*regexp.Regexp{ regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), // fork bomb // ── Data exfiltration ── - regexp.MustCompile(`\bcurl\b.*\|\s*(ba)?sh\b`), // curl | sh + regexp.MustCompile(`\bcurl\b.*\|\s*(ba)?sh\b`), // curl | sh regexp.MustCompile(`\bcurl\b.*(-d\b|-F\b|--data|--upload|--form|-T\b|-X\s*P(UT|OST|ATCH))`), // curl POST/PUT - regexp.MustCompile(`\bwget\b.*-O\s*-\s*\|\s*(ba)?sh\b`), // wget | sh - regexp.MustCompile(`\bwget\b.*--post-(data|file)`), // wget POST - regexp.MustCompile(`\b(nslookup|dig|host)\b`), // DNS exfiltration + regexp.MustCompile(`\bwget\b.*-O\s*-\s*\|\s*(ba)?sh\b`), // wget | sh + regexp.MustCompile(`\bwget\b.*--post-(data|file)`), // wget POST + regexp.MustCompile(`\b(nslookup|dig|host)\b`), // DNS exfiltration regexp.MustCompile(`/dev/tcp/`), // bash tcp redirect // ── Reverse shells ── @@ -50,8 +50,8 @@ var defaultDenyPatterns = []*regexp.Regexp{ regexp.MustCompile(`\bperl\b.*-e\s*.*\b[Ss]ocket\b`), regexp.MustCompile(`\bruby\b.*-e\s*.*\b(TCPSocket|Socket)\b`), regexp.MustCompile(`\bnode\b.*-e\s*.*\b(net\.connect|child_process)\b`), - regexp.MustCompile(`\bawk\b.*/inet/`), // awk built-in networking - regexp.MustCompile(`\bmkfifo\b`), // named pipes for shell redirection + regexp.MustCompile(`\bawk\b.*/inet/`), // awk built-in networking + regexp.MustCompile(`\bmkfifo\b`), // named pipes for shell redirection // ── Dangerous eval / code injection ── regexp.MustCompile(`\beval\s*\$`), @@ -68,7 +68,7 @@ var defaultDenyPatterns = []*regexp.Regexp{ // ── Dangerous path operations ── regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\s+/`), regexp.MustCompile(`\bchown\b.*\s+/`), - regexp.MustCompile(`\bchmod\b.*\+x.*/tmp/`), // make tmpfs executable + regexp.MustCompile(`\bchmod\b.*\+x.*/tmp/`), // make tmpfs executable regexp.MustCompile(`\bchmod\b.*\+x.*/var/tmp/`), regexp.MustCompile(`\bchmod\b.*\+x.*/dev/shm/`), @@ -77,14 +77,14 @@ var defaultDenyPatterns = []*regexp.Regexp{ regexp.MustCompile(`\bDYLD_INSERT_LIBRARIES\s*=`), regexp.MustCompile(`\bLD_LIBRARY_PATH\s*=`), regexp.MustCompile(`/etc/ld\.so\.preload`), - regexp.MustCompile(`\bGIT_EXTERNAL_DIFF\s*=`), // git diff arbitrary code exec - regexp.MustCompile(`\bGIT_DIFF_OPTS\s*=`), // git diff behavior injection - regexp.MustCompile(`\bBASH_ENV\s*=`), // shell init injection - regexp.MustCompile(`\bENV\s*=.*\bsh\b`), // sh init injection + regexp.MustCompile(`\bGIT_EXTERNAL_DIFF\s*=`), // git diff arbitrary code exec + regexp.MustCompile(`\bGIT_DIFF_OPTS\s*=`), // git diff behavior injection + regexp.MustCompile(`\bBASH_ENV\s*=`), // shell init injection + regexp.MustCompile(`\bENV\s*=.*\bsh\b`), // sh init injection // ── Container escape ── regexp.MustCompile(`/var/run/docker\.sock|docker\.(sock|socket)`), - regexp.MustCompile(`/proc/sys/(kernel|fs|net)/`), // proc writes + regexp.MustCompile(`/proc/sys/(kernel|fs|net)/`), // proc writes regexp.MustCompile(`/sys/(kernel|fs|class|devices)/`), // sysfs manipulation // ── Crypto mining ── @@ -92,17 +92,17 @@ var defaultDenyPatterns = []*regexp.Regexp{ regexp.MustCompile(`stratum\+tcp://|stratum\+ssl://`), // ── Filter bypass (Claude Code CVE-2025-66032) ── - regexp.MustCompile(`\bsed\b.*['"]/e\b`), // sed /e command execution - regexp.MustCompile(`\bsort\b.*--compress-program`), // sort arbitrary exec + regexp.MustCompile(`\bsed\b.*['"]/e\b`), // sed /e command execution + regexp.MustCompile(`\bsort\b.*--compress-program`), // sort arbitrary exec regexp.MustCompile(`\bgit\b.*(--upload-pack|--receive-pack|--exec)=`), // git exec flags - regexp.MustCompile(`\b(rg|grep)\b.*--pre=`), // preprocessor execution - regexp.MustCompile(`\bman\b.*--html=`), // man command injection - regexp.MustCompile(`\bhistory\b.*-[saw]\b`), // history file injection - regexp.MustCompile(`\$\{[^}]*@[PpEeAaKk]\}`), // ${var@P} parameter expansion + regexp.MustCompile(`\b(rg|grep)\b.*--pre=`), // preprocessor execution + regexp.MustCompile(`\bman\b.*--html=`), // man command injection + regexp.MustCompile(`\bhistory\b.*-[saw]\b`), // history file injection + regexp.MustCompile(`\$\{[^}]*@[PpEeAaKk]\}`), // ${var@P} parameter expansion // ── Network abuse / reconnaissance ── regexp.MustCompile(`\b(nmap|masscan|zmap|rustscan)\b`), - regexp.MustCompile(`\b(ssh|scp|sftp)\b.*@`), // outbound SSH + regexp.MustCompile(`\b(ssh|scp|sftp)\b.*@`), // outbound SSH regexp.MustCompile(`\b(chisel|frp|ngrok|cloudflared|bore|localtunnel)\b`), // tunneling tools // ── Persistence ── @@ -117,15 +117,15 @@ var defaultDenyPatterns = []*regexp.Regexp{ // ── Environment variable dumping ── // Bare env/printenv/set/export dumps all vars including secrets (API keys, DSN, encryption keys). // 'env VAR=val cmd' (env with assignment before command) is still allowed. - regexp.MustCompile(`^\s*env\s*$`), // bare 'env' - regexp.MustCompile(`^\s*env\s*\|`), // 'env | ...' (piped) - regexp.MustCompile(`^\s*env\s*>\s`), // 'env > file' - regexp.MustCompile(`\bprintenv\b`), // any printenv usage - regexp.MustCompile(`^\s*(set|export\s+-p|declare\s+-x)\s*($|\|)`), // shell var dumps - regexp.MustCompile(`\bcompgen\s+-e\b`), // bash env completion dump - regexp.MustCompile(`/proc/[^/]+/environ`), // /proc/PID/environ (leaks all env vars) - regexp.MustCompile(`/proc/self/environ`), // /proc/self/environ - regexp.MustCompile(`(?i)\bstrings\b.*/proc/`), // strings on /proc files (binary env dump) + regexp.MustCompile(`^\s*env\s*$`), // bare 'env' + regexp.MustCompile(`^\s*env\s*\|`), // 'env | ...' (piped) + regexp.MustCompile(`^\s*env\s*>\s`), // 'env > file' + regexp.MustCompile(`\bprintenv\b`), // any printenv usage + regexp.MustCompile(`^\s*(set|export\s+-p|declare\s+-x)\s*($|\|)`), // shell var dumps + regexp.MustCompile(`\bcompgen\s+-e\b`), // bash env completion dump + regexp.MustCompile(`/proc/[^/]+/environ`), // /proc/PID/environ (leaks all env vars) + regexp.MustCompile(`/proc/self/environ`), // /proc/self/environ + regexp.MustCompile(`(?i)\bstrings\b.*/proc/`), // strings on /proc files (binary env dump) } // ExecTool executes shell commands, optionally inside a sandbox container. @@ -135,9 +135,9 @@ type ExecTool struct { denyPatterns []*regexp.Regexp denyExemptions []string // substrings that exempt a command from deny (e.g. ".goclaw/skills-store/") restrict bool - sandboxMgr sandbox.Manager // nil = no sandbox, execute on host - approvalMgr *ExecApprovalManager // nil = no approval needed - agentID string // for approval request context + sandboxMgr sandbox.Manager // nil = no sandbox, execute on host + approvalMgr *ExecApprovalManager // nil = no approval needed + agentID string // for approval request context } // NewExecTool creates an exec tool that runs commands directly on the host. @@ -188,15 +188,15 @@ func (t *ExecTool) SetApprovalManager(mgr *ExecApprovalManager, agentID string) func (t *ExecTool) Name() string { return "exec" } func (t *ExecTool) Description() string { return "Execute a shell command and return its output" } -func (t *ExecTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *ExecTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ + "properties": map[string]any{ + "command": map[string]any{ "type": "string", "description": "The shell command to execute", }, - "working_dir": map[string]interface{}{ + "working_dir": map[string]any{ "type": "string", "description": "Optional working directory for the command", }, @@ -205,7 +205,7 @@ func (t *ExecTool) Parameters() map[string]interface{} { } } -func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result { command, _ := args["command"].(string) if command == "" { return ErrorResult("command is required") diff --git a/internal/tools/skill_search.go b/internal/tools/skill_search.go index b50a8190..37f0f636 100644 --- a/internal/tools/skill_search.go +++ b/internal/tools/skill_search.go @@ -70,15 +70,15 @@ func (t *SkillSearchTool) Description() string { return "Search for available skills by keyword. Returns matching skills with name, description, and SKILL.md location for reading with read_file." } -func (t *SkillSearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *SkillSearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Search keywords to find relevant skills (use English keywords)", }, - "max_results": map[string]interface{}{ + "max_results": map[string]any{ "type": "integer", "description": "Maximum number of results to return (default: 5)", }, @@ -87,7 +87,7 @@ func (t *SkillSearchTool) Parameters() map[string]interface{} { } } -func (t *SkillSearchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SkillSearchTool) Execute(ctx context.Context, args map[string]any) *Result { query, _ := args["query"].(string) if query == "" { return ErrorResult("query parameter is required") @@ -127,7 +127,7 @@ func (t *SkillSearchTool) Execute(ctx context.Context, args map[string]interface return NewResult(fmt.Sprintf("No skills found matching: %s", query)) } - data, _ := json.MarshalIndent(map[string]interface{}{ + data, _ := json.MarshalIndent(map[string]any{ "results": results, "count": len(results), }, "", " ") diff --git a/internal/tools/subagent_exec.go b/internal/tools/subagent_exec.go index f5842b23..7bbea394 100644 --- a/internal/tools/subagent_exec.go +++ b/internal/tools/subagent_exec.go @@ -184,7 +184,7 @@ func (sm *SubagentManager) executeTask(ctx context.Context, task *SubagentTask) Messages: messages, Tools: toolsReg.ProviderDefs(), Model: model, - Options: map[string]interface{}{ + Options: map[string]any{ "max_tokens": 4096, "temperature": 0.5, }, diff --git a/internal/tools/subagent_spawn_tool.go b/internal/tools/subagent_spawn_tool.go index 0ca9ad1a..7af5924d 100644 --- a/internal/tools/subagent_spawn_tool.go +++ b/internal/tools/subagent_spawn_tool.go @@ -46,33 +46,33 @@ func (t *SpawnTool) Description() string { return "Spawn a subagent to handle a task in the background. The subagent runs independently and reports back when done." } -func (t *SpawnTool) Parameters() map[string]interface{} { - props := map[string]interface{}{ - "action": map[string]interface{}{ +func (t *SpawnTool) Parameters() map[string]any { + props := map[string]any{ + "action": map[string]any{ "type": "string", "description": "'spawn' (default), 'list', 'cancel', or 'steer'", }, - "task": map[string]interface{}{ + "task": map[string]any{ "type": "string", "description": "The task to complete (required for action=spawn)", }, - "mode": map[string]interface{}{ + "mode": map[string]any{ "type": "string", "description": "'async' (default, returns immediately) or 'sync' (blocks until done)", }, - "label": map[string]interface{}{ + "label": map[string]any{ "type": "string", "description": "Short label for the task (for display)", }, - "model": map[string]interface{}{ + "model": map[string]any{ "type": "string", "description": "Optional model override (e.g. 'anthropic/claude-sonnet-4-5-20250929')", }, - "id": map[string]interface{}{ + "id": map[string]any{ "type": "string", "description": "Task ID for cancel/steer. For cancel: use 'all' to cancel all or 'last' for most recent", }, - "message": map[string]interface{}{ + "message": map[string]any{ "type": "string", "description": "New instructions (required for action=steer)", }, @@ -80,32 +80,32 @@ func (t *SpawnTool) Parameters() map[string]interface{} { // Add delegation-specific params when delegate manager is available if t.delegateMgr != nil { - props["agent"] = map[string]interface{}{ + props["agent"] = map[string]any{ "type": "string", "description": "Target agent key. Omit to clone yourself, specify to delegate to another agent", } - props["context"] = map[string]interface{}{ + props["context"] = map[string]any{ "type": "string", "description": "Optional additional context for the target agent (used with agent param)", } - props["team_task_id"] = map[string]interface{}{ + props["team_task_id"] = map[string]any{ "type": "string", "description": "Team task ID to auto-complete when task finishes (for team workflows)", } - props["estimated_duration"] = map[string]interface{}{ + props["estimated_duration"] = map[string]any{ "type": "number", "description": "Estimated task duration in seconds. A progress notification is sent to the user if the task takes longer than this. Default 90.", } } - return map[string]interface{}{ + return map[string]any{ "type": "object", "properties": props, "required": []string{"task"}, } } -func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) if action == "" { action = "spawn" @@ -124,7 +124,7 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *R } // executeSpawn routes to subagent (self-clone) or delegation (different agent). -func (t *SpawnTool) executeSpawn(ctx context.Context, args map[string]interface{}) *Result { +func (t *SpawnTool) executeSpawn(ctx context.Context, args map[string]any) *Result { task, _ := args["task"].(string) if task == "" { return ErrorResult("task parameter is required") @@ -150,7 +150,7 @@ func (t *SpawnTool) executeSpawn(ctx context.Context, args map[string]interface{ } // executeSubagentAsync spawns an async self-clone (old SpawnTool behavior). -func (t *SpawnTool) executeSubagentAsync(ctx context.Context, args map[string]interface{}, task string) *Result { +func (t *SpawnTool) executeSubagentAsync(ctx context.Context, args map[string]any, task string) *Result { label, _ := args["label"].(string) modelOverride, _ := args["model"].(string) @@ -178,7 +178,7 @@ After all spawn tool calls in this turn are complete, briefly tell the user what } // executeSubagentSync runs a sync self-clone (old SubagentTool action=run behavior). -func (t *SpawnTool) executeSubagentSync(ctx context.Context, args map[string]interface{}, task string) *Result { +func (t *SpawnTool) executeSubagentSync(ctx context.Context, args map[string]any, task string) *Result { label, _ := args["label"].(string) if label == "" { label = truncate(task, 50) @@ -212,7 +212,7 @@ func (t *SpawnTool) executeSubagentSync(ctx context.Context, args map[string]int } // executeDelegation delegates to a different agent (old DelegateTool behavior). -func (t *SpawnTool) executeDelegation(ctx context.Context, args map[string]interface{}, agentKey, task string) *Result { +func (t *SpawnTool) executeDelegation(ctx context.Context, args map[string]any, agentKey, task string) *Result { extraContext, _ := args["context"].(string) mode, _ := args["mode"].(string) if mode == "" { @@ -317,7 +317,7 @@ func (t *SpawnTool) executeList(ctx context.Context) *Result { sourceAgentID := store.AgentIDFromContext(ctx) delegations := t.delegateMgr.ListActive(sourceAgentID) if len(delegations) > 0 { - out, _ := json.Marshal(map[string]interface{}{ + out, _ := json.Marshal(map[string]any{ "delegations": delegations, "count": len(delegations), }) @@ -332,7 +332,7 @@ func (t *SpawnTool) executeList(ctx context.Context) *Result { } // executeCancel cancels a subagent or delegation by ID. -func (t *SpawnTool) executeCancel(ctx context.Context, args map[string]interface{}) *Result { +func (t *SpawnTool) executeCancel(ctx context.Context, args map[string]any) *Result { id, _ := args["id"].(string) if id == "" { return ErrorResult("id is required for action=cancel") @@ -352,7 +352,7 @@ func (t *SpawnTool) executeCancel(ctx context.Context, args map[string]interface } // executeSteer redirects a running subagent with new instructions. -func (t *SpawnTool) executeSteer(ctx context.Context, args map[string]interface{}) *Result { +func (t *SpawnTool) executeSteer(ctx context.Context, args map[string]any) *Result { id, _ := args["id"].(string) if id == "" { return ErrorResult("id is required for action=steer") diff --git a/internal/tools/team_message_tool.go b/internal/tools/team_message_tool.go index cf955196..51765057 100644 --- a/internal/tools/team_message_tool.go +++ b/internal/tools/team_message_tool.go @@ -26,19 +26,19 @@ func (t *TeamMessageTool) Description() string { return "Send and receive messages within your team. Actions: send (direct message to a teammate), broadcast (message all teammates), read (check unread messages). See TEAM.md for your teammates." } -func (t *TeamMessageTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *TeamMessageTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "description": "'send', 'broadcast', or 'read'", }, - "to": map[string]interface{}{ + "to": map[string]any{ "type": "string", "description": "Target agent key (required for action=send)", }, - "text": map[string]interface{}{ + "text": map[string]any{ "type": "string", "description": "Message content (required for action=send and action=broadcast)", }, @@ -47,7 +47,7 @@ func (t *TeamMessageTool) Parameters() map[string]interface{} { } } -func (t *TeamMessageTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamMessageTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) switch action { @@ -62,7 +62,7 @@ func (t *TeamMessageTool) Execute(ctx context.Context, args map[string]interface } } -func (t *TeamMessageTool) executeSend(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamMessageTool) executeSend(ctx context.Context, args map[string]any) *Result { team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -134,7 +134,7 @@ func (t *TeamMessageTool) executeSend(ctx context.Context, args map[string]inter return NewResult(fmt.Sprintf("Message sent to %s.", toKey)) } -func (t *TeamMessageTool) executeBroadcast(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamMessageTool) executeBroadcast(ctx context.Context, args map[string]any) *Result { team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -207,7 +207,7 @@ func (t *TeamMessageTool) executeRead(ctx context.Context) *Result { _ = t.manager.teamStore.MarkRead(ctx, msg.ID) } - out, _ := json.Marshal(map[string]interface{}{ + out, _ := json.Marshal(map[string]any{ "messages": messages, "count": len(messages), }) diff --git a/internal/tools/team_tasks_tool.go b/internal/tools/team_tasks_tool.go index 2937fbc6..2edbe1f0 100644 --- a/internal/tools/team_tasks_tool.go +++ b/internal/tools/team_tasks_tool.go @@ -28,48 +28,48 @@ func (t *TeamTasksTool) Description() string { return "Manage the shared team task list. Actions: list (active tasks overview), get (full task detail with result), create, claim, complete, cancel, search. See TEAM.md for your team context." } -func (t *TeamTasksTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *TeamTasksTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "description": "'list', 'get', 'create', 'claim', 'complete', 'cancel', or 'search'", }, - "status": map[string]interface{}{ + "status": map[string]any{ "type": "string", "description": "Filter for action=list: '' (active only, default), 'completed', 'all'", }, - "query": map[string]interface{}{ + "query": map[string]any{ "type": "string", "description": "Search query for action=search (searches subject and description)", }, - "subject": map[string]interface{}{ + "subject": map[string]any{ "type": "string", "description": "Task subject (required for action=create)", }, - "description": map[string]interface{}{ + "description": map[string]any{ "type": "string", "description": "Task description (optional, for action=create)", }, - "priority": map[string]interface{}{ + "priority": map[string]any{ "type": "number", "description": "Task priority, higher = more important (optional, for action=create, default 0)", }, - "blocked_by": map[string]interface{}{ + "blocked_by": map[string]any{ "type": "array", - "items": map[string]interface{}{"type": "string"}, + "items": map[string]any{"type": "string"}, "description": "Task IDs that must complete before this task can be claimed (optional, for action=create)", }, - "task_id": map[string]interface{}{ + "task_id": map[string]any{ "type": "string", "description": "Task ID (required for action=get, claim, complete, cancel)", }, - "result": map[string]interface{}{ + "result": map[string]any{ "type": "string", "description": "Task result summary (required for action=complete)", }, - "reason": map[string]interface{}{ + "reason": map[string]any{ "type": "string", "description": "Cancellation reason (optional for action=cancel)", }, @@ -78,7 +78,7 @@ func (t *TeamTasksTool) Parameters() map[string]interface{} { } } -func (t *TeamTasksTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) switch action { @@ -103,7 +103,7 @@ func (t *TeamTasksTool) Execute(ctx context.Context, args map[string]interface{} const listTasksLimit = 20 -func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]any) *Result { team, _, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -133,7 +133,7 @@ func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]interfa tasks = tasks[:listTasksLimit] } - resp := map[string]interface{}{ + resp := map[string]any{ "tasks": tasks, "count": len(tasks), } @@ -146,7 +146,7 @@ func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]interfa return SilentResult(string(out)) } -func (t *TeamTasksTool) executeGet(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeGet(ctx context.Context, args map[string]any) *Result { team, _, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -183,7 +183,7 @@ func (t *TeamTasksTool) executeGet(ctx context.Context, args map[string]interfac return SilentResult(string(out)) } -func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]any) *Result { team, _, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -218,14 +218,14 @@ func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]inter } } - out, _ := json.Marshal(map[string]interface{}{ + out, _ := json.Marshal(map[string]any{ "tasks": tasks, "count": len(tasks), }) return SilentResult(string(out)) } -func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]any) *Result { team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -246,7 +246,7 @@ func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]inter } var blockedBy []uuid.UUID - if raw, ok := args["blocked_by"].([]interface{}); ok { + if raw, ok := args["blocked_by"].([]any); ok { for _, v := range raw { if s, ok := v.(string); ok { if id, err := uuid.Parse(s); err == nil { @@ -290,7 +290,7 @@ func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]inter return NewResult(fmt.Sprintf("Task created: %s (id=%s, status=%s)", subject, task.ID, status)) } -func (t *TeamTasksTool) executeClaim(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeClaim(ctx context.Context, args map[string]any) *Result { team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) @@ -325,7 +325,7 @@ func (t *TeamTasksTool) executeClaim(ctx context.Context, args map[string]interf return NewResult(fmt.Sprintf("Task %s claimed successfully. It is now in progress.", taskIDStr)) } -func (t *TeamTasksTool) executeComplete(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeComplete(ctx context.Context, args map[string]any) *Result { // Delegate agents cannot complete tasks — autoCompleteTeamTask handles it. if ToolChannelFromCtx(ctx) == "delegate" { return ErrorResult("delegate agents cannot complete team tasks directly — results are auto-completed when delegation finishes") @@ -375,7 +375,7 @@ func (t *TeamTasksTool) executeComplete(ctx context.Context, args map[string]int return NewResult(fmt.Sprintf("Task %s completed. Dependent tasks have been unblocked.", taskIDStr)) } -func (t *TeamTasksTool) executeCancel(ctx context.Context, args map[string]interface{}) *Result { +func (t *TeamTasksTool) executeCancel(ctx context.Context, args map[string]any) *Result { // Delegate agents cannot cancel tasks — only lead/user-facing agents can. if ToolChannelFromCtx(ctx) == "delegate" { return ErrorResult("delegate agents cannot cancel team tasks directly") diff --git a/internal/tools/team_tool_manager.go b/internal/tools/team_tool_manager.go index f0e3ef20..fdcbe263 100644 --- a/internal/tools/team_tool_manager.go +++ b/internal/tools/team_tool_manager.go @@ -126,7 +126,7 @@ func (m *TeamToolManager) agentKeyFromID(ctx context.Context, id uuid.UUID) stri } // broadcastTeamEvent sends a real-time event via the message bus for team activity visibility. -func (m *TeamToolManager) broadcastTeamEvent(name string, payload interface{}) { +func (m *TeamToolManager) broadcastTeamEvent(name string, payload any) { if m.msgBus == nil { return } diff --git a/internal/tools/telegram_forum.go b/internal/tools/telegram_forum.go index 96b642e8..a781d15b 100644 --- a/internal/tools/telegram_forum.go +++ b/internal/tools/telegram_forum.go @@ -33,23 +33,23 @@ func (t *CreateForumTopicTool) Description() string { "Returns the topic's message_thread_id which can be used for routing messages to the topic." } -func (t *CreateForumTopicTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *CreateForumTopicTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "chat_id": map[string]interface{}{ + "properties": map[string]any{ + "chat_id": map[string]any{ "type": "string", "description": "The Telegram chat ID of the supergroup (e.g. \"-1001234567890\").", }, - "name": map[string]interface{}{ + "name": map[string]any{ "type": "string", "description": "Topic name (1-128 characters).", }, - "icon_color": map[string]interface{}{ + "icon_color": map[string]any{ "type": "integer", "description": "Optional icon color as RGB integer. Allowed values: 7322096, 16766590, 13338331, 9367192, 16749490, 16478047.", }, - "icon_custom_emoji_id": map[string]interface{}{ + "icon_custom_emoji_id": map[string]any{ "type": "string", "description": "Optional custom emoji ID for the topic icon (requires Telegram Premium).", }, @@ -58,7 +58,7 @@ func (t *CreateForumTopicTool) Parameters() map[string]interface{} { } } -func (t *CreateForumTopicTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *CreateForumTopicTool) Execute(ctx context.Context, args map[string]any) *Result { creator := t.provider() if creator == nil { return &Result{ForLLM: "Error: no Telegram channel available", IsError: true} @@ -90,7 +90,7 @@ func (t *CreateForumTopicTool) Execute(ctx context.Context, args map[string]inte return &Result{ForLLM: fmt.Sprintf("Error creating forum topic: %v", err), IsError: true} } - result := map[string]interface{}{ + result := map[string]any{ "message_thread_id": threadID, "name": topicName, "chat_id": chatIDStr, diff --git a/internal/tools/tts.go b/internal/tools/tts.go index 0ed088b2..44bbcd67 100644 --- a/internal/tools/tts.go +++ b/internal/tools/tts.go @@ -29,19 +29,19 @@ func (t *TtsTool) Description() string { return "Convert text to speech audio. Returns a MEDIA: path to the generated audio file." } -func (t *TtsTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *TtsTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "text": map[string]interface{}{ + "properties": map[string]any{ + "text": map[string]any{ "type": "string", "description": "The text to convert to speech", }, - "voice": map[string]interface{}{ + "voice": map[string]any{ "type": "string", "description": "Voice ID (provider-specific). Optional — uses default if omitted.", }, - "provider": map[string]interface{}{ + "provider": map[string]any{ "type": "string", "description": "TTS provider: openai, elevenlabs, edge, minimax. Optional — uses primary if omitted.", }, @@ -53,7 +53,7 @@ func (t *TtsTool) Parameters() map[string]interface{} { // SetContext is a no-op; channel is now read from ctx (thread-safe). func (t *TtsTool) SetContext(channel, _ string) {} -func (t *TtsTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *TtsTool) Execute(ctx context.Context, args map[string]any) *Result { text, _ := args["text"].(string) if text == "" { return &Result{ForLLM: "error: text is required", IsError: true} diff --git a/internal/tools/types.go b/internal/tools/types.go index 4a2d947f..08d49e01 100644 --- a/internal/tools/types.go +++ b/internal/tools/types.go @@ -12,8 +12,8 @@ import ( type Tool interface { Name() string Description() string - Parameters() map[string]interface{} - Execute(ctx context.Context, args map[string]interface{}) *Result + Parameters() map[string]any + Execute(ctx context.Context, args map[string]any) *Result } // ContextualTool receives channel/chat context before execution. diff --git a/internal/tools/use_skill.go b/internal/tools/use_skill.go index fd6ce4cf..cc92ee5a 100644 --- a/internal/tools/use_skill.go +++ b/internal/tools/use_skill.go @@ -20,15 +20,15 @@ func (t *UseSkillTool) Description() string { return "Activate a skill. Call this before read_file to signal skill usage for tracing and observability." } -func (t *UseSkillTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *UseSkillTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ + "properties": map[string]any{ + "name": map[string]any{ "type": "string", "description": "Skill name or slug to activate", }, - "params": map[string]interface{}{ + "params": map[string]any{ "type": "object", "description": "Optional skill-specific parameters", }, @@ -37,7 +37,7 @@ func (t *UseSkillTool) Parameters() map[string]interface{} { } } -func (t *UseSkillTool) Execute(_ context.Context, args map[string]interface{}) *Result { +func (t *UseSkillTool) Execute(_ context.Context, args map[string]any) *Result { name, _ := args["name"].(string) if name == "" { return ErrorResult("name parameter is required") diff --git a/internal/tools/web_fetch.go b/internal/tools/web_fetch.go index 8a301e15..831c6f1f 100644 --- a/internal/tools/web_fetch.go +++ b/internal/tools/web_fetch.go @@ -117,20 +117,20 @@ func (t *WebFetchTool) Description() string { return "Fetch a URL and extract its content. Supports HTML (converted to markdown/text), JSON, and plain text. Includes SSRF protection." } -func (t *WebFetchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *WebFetchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "url": map[string]interface{}{ + "properties": map[string]any{ + "url": map[string]any{ "type": "string", "description": "HTTP or HTTPS URL to fetch.", }, - "extractMode": map[string]interface{}{ + "extractMode": map[string]any{ "type": "string", "description": `Extraction mode ("markdown" or "text"). Default: "markdown".`, "enum": []string{"markdown", "text"}, }, - "maxChars": map[string]interface{}{ + "maxChars": map[string]any{ "type": "number", "description": "Maximum characters to return (truncates when exceeded).", "minimum": 100.0, @@ -140,7 +140,7 @@ func (t *WebFetchTool) Parameters() map[string]interface{} { } } -func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *Result { rawURL, _ := args["url"].(string) if rawURL == "" { return ErrorResult("url is required") diff --git a/internal/tools/web_fetch_convert.go b/internal/tools/web_fetch_convert.go index 0b3a4501..e20968ac 100644 --- a/internal/tools/web_fetch_convert.go +++ b/internal/tools/web_fetch_convert.go @@ -10,7 +10,7 @@ import ( // extractJSON pretty-prints JSON content. func extractJSON(body []byte) (string, string) { - var data interface{} + var data any if err := json.Unmarshal(body, &data); err == nil { formatted, _ := json.MarshalIndent(data, "", " ") return string(formatted), "json" @@ -55,7 +55,7 @@ var skipElements = map[atom.Atom]bool{ atom.Nav: true, atom.Footer: true, atom.Picture: true, - atom.Source: true, + atom.Source: true, } // Additional elements to skip in text mode only. diff --git a/internal/tools/web_fetch_convert_handlers.go b/internal/tools/web_fetch_convert_handlers.go index 2962a8af..f2000c54 100644 --- a/internal/tools/web_fetch_convert_handlers.go +++ b/internal/tools/web_fetch_convert_handlers.go @@ -29,7 +29,7 @@ func (c *converter) handleHeading(n *html.Node) { c.ensureDoubleNewline() if c.mode == modeMarkdown && len(n.Data) == 2 && n.Data[0] == 'h' { level := int(n.Data[1] - '0') - for i := 0; i < level; i++ { + for range level { c.buf.WriteByte('#') } c.buf.WriteByte(' ') @@ -82,7 +82,7 @@ func (c *converter) handlePre(n *html.Node) { lang := "" if code := findChild(n, atom.Code); code != nil { cls := getAttr(code, "class") - for _, part := range strings.Fields(cls) { + for part := range strings.FieldsSeq(cls) { if rest, ok := strings.CutPrefix(part, "language-"); ok { lang = rest break diff --git a/internal/tools/web_fetch_hidden.go b/internal/tools/web_fetch_hidden.go index 074c157d..c96be117 100644 --- a/internal/tools/web_fetch_hidden.go +++ b/internal/tools/web_fetch_hidden.go @@ -67,7 +67,7 @@ func hasHiddenClass(n *html.Node) bool { if classAttr == "" { return false } - for _, cls := range strings.Fields(classAttr) { + for cls := range strings.FieldsSeq(classAttr) { if hiddenClasses[strings.ToLower(cls)] { return true } diff --git a/internal/tools/web_search.go b/internal/tools/web_search.go index 8e391ebf..c50a5d9e 100644 --- a/internal/tools/web_search.go +++ b/internal/tools/web_search.go @@ -74,12 +74,12 @@ type WebSearchTool struct { // WebSearchConfig holds configuration for the web search tool. type WebSearchConfig struct { - BraveAPIKey string - BraveEnabled bool + BraveAPIKey string + BraveEnabled bool BraveMaxResults int - DDGEnabled bool - DDGMaxResults int - CacheTTL time.Duration + DDGEnabled bool + DDGMaxResults int + CacheTTL time.Duration } func NewWebSearchTool(cfg WebSearchConfig) *WebSearchTool { @@ -114,33 +114,33 @@ func (t *WebSearchTool) Description() string { return "Search the web for current information. Returns titles, URLs, and snippets from search results." } -func (t *WebSearchTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *WebSearchTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Search query string.", }, - "count": map[string]interface{}{ + "count": map[string]any{ "type": "number", "description": "Number of results to return (1-10).", "minimum": 1.0, "maximum": float64(maxSearchCount), }, - "country": map[string]interface{}{ + "country": map[string]any{ "type": "string", "description": "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.", }, - "search_lang": map[string]interface{}{ + "search_lang": map[string]any{ "type": "string", "description": "ISO language code for search results (e.g., 'de', 'en', 'fr').", }, - "ui_lang": map[string]interface{}{ + "ui_lang": map[string]any{ "type": "string", "description": "ISO language code for UI elements.", }, - "freshness": map[string]interface{}{ + "freshness": map[string]any{ "type": "string", "description": "Filter results by discovery time. Supports 'pd' (past day), 'pw' (past week), 'pm' (past month), 'py' (past year), and date range 'YYYY-MM-DDtoYYYY-MM-DD'.", }, @@ -149,7 +149,7 @@ func (t *WebSearchTool) Parameters() map[string]interface{} { } } -func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *Result { +func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *Result { query, _ := args["query"].(string) if query == "" { return ErrorResult("query is required") diff --git a/internal/tools/web_search_ddg.go b/internal/tools/web_search_ddg.go index 46c5b78d..9dc880f7 100644 --- a/internal/tools/web_search_ddg.go +++ b/internal/tools/web_search_ddg.go @@ -70,8 +70,8 @@ func extractDDGResults(html string, count int) ([]searchResult, error) { // DDG wraps URLs with redirect — extract real URL from uddg= param if strings.Contains(rawURL, "uddg=") { if u, err := url.QueryUnescape(rawURL); err == nil { - if idx := strings.Index(u, "uddg="); idx != -1 { - extracted := u[idx+5:] + if _, after, ok := strings.Cut(u, "uddg="); ok { + extracted := after // uddg value may have trailing ¶ms if ampIdx := strings.Index(extracted, "&"); ampIdx != -1 { extracted = extracted[:ampIdx] diff --git a/internal/tracing/otelexport/exporter_test.go b/internal/tracing/otelexport/exporter_test.go index 11b07e13..9a3d60ac 100644 --- a/internal/tracing/otelexport/exporter_test.go +++ b/internal/tracing/otelexport/exporter_test.go @@ -33,7 +33,7 @@ func TestUUIDToSpanID(t *testing.T) { t.Errorf("expected 8 bytes, got %d", len(sid)) } // Verify it uses the last 8 bytes - for i := 0; i < 8; i++ { + for i := range 8 { if sid[i] != id[8+i] { t.Errorf("byte %d: expected %02x, got %02x", i, id[8+i], sid[i]) } diff --git a/internal/tts/elevenlabs.go b/internal/tts/elevenlabs.go index f5db5b7d..e9076d38 100644 --- a/internal/tts/elevenlabs.go +++ b/internal/tts/elevenlabs.go @@ -77,13 +77,13 @@ func (p *ElevenLabsProvider) Synthesize(ctx context.Context, text string, opts O mime = "audio/ogg" } - body := map[string]interface{}{ + body := map[string]any{ "text": text, "model_id": modelID, - "voice_settings": map[string]interface{}{ - "stability": 0.5, - "similarity_boost": 0.75, - "style": 0.0, + "voice_settings": map[string]any{ + "stability": 0.5, + "similarity_boost": 0.75, + "style": 0.0, "use_speaker_boost": true, }, } diff --git a/internal/tts/minimax.go b/internal/tts/minimax.go index d442b9b5..c3cdc87f 100644 --- a/internal/tts/minimax.go +++ b/internal/tts/minimax.go @@ -93,16 +93,16 @@ func (p *MiniMaxProvider) Synthesize(ctx context.Context, text string, opts Opti } } - body := map[string]interface{}{ + body := map[string]any{ "text": text, "model": model, "stream": false, - "voice_setting": map[string]interface{}{ + "voice_setting": map[string]any{ "voice_id": voiceID, "speed": 1.0, "pitch": 0, }, - "audio_setting": map[string]interface{}{ + "audio_setting": map[string]any{ "format": audioFormat, }, } diff --git a/internal/tts/openai.go b/internal/tts/openai.go index 5e1b0661..d330e25e 100644 --- a/internal/tts/openai.go +++ b/internal/tts/openai.go @@ -71,7 +71,7 @@ func (p *OpenAIProvider) Synthesize(ctx context.Context, text string, opts Optio format = "mp3" } - body := map[string]interface{}{ + body := map[string]any{ "model": model, "input": text, "voice": voice, diff --git a/pkg/browser/browser_remote.go b/pkg/browser/browser_remote.go index 394ad4b0..c636f3af 100644 --- a/pkg/browser/browser_remote.go +++ b/pkg/browser/browser_remote.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "net/url" + "strings" "time" "github.com/go-rod/rod" @@ -89,11 +90,11 @@ func (m *Manager) getPage(targetID string) (*rod.Page, error) { // setupConsoleListener attaches a console message listener to a page via Rod's EachEvent. func (m *Manager) setupConsoleListener(page *rod.Page, targetID string) { go page.EachEvent(func(e *proto.RuntimeConsoleAPICalled) { - var text string + var text strings.Builder for _, arg := range e.Args { s := arg.Value.String() if s != "" && s != "null" { - text += s + " " + text.WriteString(s + " ") } } @@ -114,7 +115,7 @@ func (m *Manager) setupConsoleListener(page *rod.Page, targetID string) { } m.console[targetID] = append(msgs, ConsoleMessage{ Level: level, - Text: text, + Text: text.String(), }) m.mu.Unlock() })() diff --git a/pkg/browser/tool.go b/pkg/browser/tool.go index 6a52a31c..c26ee125 100644 --- a/pkg/browser/tool.go +++ b/pkg/browser/tool.go @@ -50,77 +50,77 @@ Act kinds: click, type, press, hover, wait, evaluate Workflow: start → open URL → snapshot (get refs) → act (use refs) → snapshot again` } -func (t *BrowserTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *BrowserTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "enum": []string{"status", "start", "stop", "tabs", "open", "close", "snapshot", "screenshot", "navigate", "console", "act"}, "description": "The browser action to perform", }, - "targetUrl": map[string]interface{}{ + "targetUrl": map[string]any{ "type": "string", "description": "URL for open/navigate actions", }, - "targetId": map[string]interface{}{ + "targetId": map[string]any{ "type": "string", "description": "Tab target ID (omit for current tab)", }, - "maxChars": map[string]interface{}{ + "maxChars": map[string]any{ "type": "number", "description": "Max characters for snapshot (default 8000)", }, - "interactive": map[string]interface{}{ + "interactive": map[string]any{ "type": "boolean", "description": "Only show interactive elements in snapshot", }, - "compact": map[string]interface{}{ + "compact": map[string]any{ "type": "boolean", "description": "Remove empty structural elements from snapshot", }, - "depth": map[string]interface{}{ + "depth": map[string]any{ "type": "number", "description": "Max depth for snapshot tree", }, - "fullPage": map[string]interface{}{ + "fullPage": map[string]any{ "type": "boolean", "description": "Capture full page screenshot", }, - "timeoutMs": map[string]interface{}{ + "timeoutMs": map[string]any{ "type": "number", "description": "Timeout in milliseconds for actions", }, - "request": map[string]interface{}{ + "request": map[string]any{ "type": "object", "description": "Action request for 'act' command", - "properties": map[string]interface{}{ - "kind": map[string]interface{}{ + "properties": map[string]any{ + "kind": map[string]any{ "type": "string", "enum": []string{"click", "type", "press", "hover", "wait", "evaluate"}, "description": "The interaction kind", }, - "ref": map[string]interface{}{ + "ref": map[string]any{ "type": "string", "description": "Element ref from snapshot (e.g. e1, e2)", }, - "text": map[string]interface{}{ + "text": map[string]any{ "type": "string", "description": "Text to type", }, - "key": map[string]interface{}{ + "key": map[string]any{ "type": "string", "description": "Key to press (e.g. Enter, Tab, Escape)", }, - "submit": map[string]interface{}{ + "submit": map[string]any{ "type": "boolean", "description": "Press Enter after typing", }, - "fn": map[string]interface{}{ + "fn": map[string]any{ "type": "string", "description": "JavaScript to evaluate", }, - "timeMs": map[string]interface{}{ + "timeMs": map[string]any{ "type": "number", "description": "Wait time in milliseconds", }, @@ -131,7 +131,7 @@ func (t *BrowserTool) Parameters() map[string]interface{} { } } -func (t *BrowserTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) Execute(ctx context.Context, args map[string]any) *tools.Result { action, _ := args["action"].(string) if action == "" { return tools.ErrorResult("action is required") @@ -200,7 +200,7 @@ func (t *BrowserTool) handleTabs(ctx context.Context) *tools.Result { return jsonResult(tabs) } -func (t *BrowserTool) handleOpen(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleOpen(ctx context.Context, args map[string]any) *tools.Result { url, _ := args["targetUrl"].(string) if url == "" { return tools.ErrorResult("targetUrl is required for open action") @@ -212,7 +212,7 @@ func (t *BrowserTool) handleOpen(ctx context.Context, args map[string]interface{ return jsonResult(tab) } -func (t *BrowserTool) handleClose(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleClose(ctx context.Context, args map[string]any) *tools.Result { targetID, _ := args["targetId"].(string) if err := t.manager.CloseTab(ctx, targetID); err != nil { return tools.ErrorResult(err.Error()) @@ -220,7 +220,7 @@ func (t *BrowserTool) handleClose(ctx context.Context, args map[string]interface return tools.NewResult("Tab closed.") } -func (t *BrowserTool) handleSnapshot(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleSnapshot(ctx context.Context, args map[string]any) *tools.Result { targetID, _ := args["targetId"].(string) opts := DefaultSnapshotOptions() @@ -248,7 +248,7 @@ func (t *BrowserTool) handleSnapshot(ctx context.Context, args map[string]interf return tools.NewResult(header + snap.Snapshot) } -func (t *BrowserTool) handleScreenshot(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleScreenshot(ctx context.Context, args map[string]any) *tools.Result { targetID, _ := args["targetId"].(string) fullPage, _ := args["fullPage"].(bool) @@ -266,7 +266,7 @@ func (t *BrowserTool) handleScreenshot(ctx context.Context, args map[string]inte return &tools.Result{ForLLM: fmt.Sprintf("MEDIA:%s", imagePath)} } -func (t *BrowserTool) handleNavigate(ctx context.Context, args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleNavigate(ctx context.Context, args map[string]any) *tools.Result { targetID, _ := args["targetId"].(string) url, _ := args["targetUrl"].(string) if url == "" { @@ -279,14 +279,14 @@ func (t *BrowserTool) handleNavigate(ctx context.Context, args map[string]interf return tools.NewResult(fmt.Sprintf("Navigated to %s", url)) } -func (t *BrowserTool) handleConsole(args map[string]interface{}) *tools.Result { +func (t *BrowserTool) handleConsole(args map[string]any) *tools.Result { targetID, _ := args["targetId"].(string) msgs := t.manager.ConsoleMessages(targetID) return jsonResult(msgs) } -func (t *BrowserTool) handleAct(ctx context.Context, args map[string]interface{}) *tools.Result { - req, ok := args["request"].(map[string]interface{}) +func (t *BrowserTool) handleAct(ctx context.Context, args map[string]any) *tools.Result { + req, ok := args["request"].(map[string]any) if !ok { return tools.ErrorResult("request object is required for act action") } @@ -392,7 +392,7 @@ func (t *BrowserTool) handleAct(ctx context.Context, args map[string]interface{} } } -func jsonResult(v interface{}) *tools.Result { +func jsonResult(v any) *tools.Result { data, _ := json.MarshalIndent(v, "", " ") return tools.NewResult(string(data)) } diff --git a/pkg/protocol/frames.go b/pkg/protocol/frames.go index a64c80b7..5ca9fb47 100644 --- a/pkg/protocol/frames.go +++ b/pkg/protocol/frames.go @@ -30,27 +30,27 @@ type RequestFrame struct { // ResponseFrame is sent by the server in response to a request. type ResponseFrame struct { - Type string `json:"type"` // always "res" - ID string `json:"id"` // matches request ID - OK bool `json:"ok"` // true if success - Payload interface{} `json:"payload,omitempty"` // response data (when ok=true) + Type string `json:"type"` // always "res" + ID string `json:"id"` // matches request ID + OK bool `json:"ok"` // true if success + Payload any `json:"payload,omitempty"` // response data (when ok=true) Error *ErrorShape `json:"error,omitempty"` // error info (when ok=false) } // ErrorShape describes a protocol error. type ErrorShape struct { - Code string `json:"code"` - Message string `json:"message"` - Details interface{} `json:"details,omitempty"` - Retryable bool `json:"retryable,omitempty"` - RetryAfterMs int `json:"retryAfterMs,omitempty"` + Code string `json:"code"` + Message string `json:"message"` + Details any `json:"details,omitempty"` + Retryable bool `json:"retryable,omitempty"` + RetryAfterMs int `json:"retryAfterMs,omitempty"` } // EventFrame is pushed from server to client without a preceding request. type EventFrame struct { Type string `json:"type"` // always "event" Event string `json:"event"` // event name - Payload interface{} `json:"payload,omitempty"` // event data + Payload any `json:"payload,omitempty"` // event data Seq int64 `json:"seq,omitempty"` // ordering sequence number StateVersion *StateVersion `json:"stateVersion,omitempty"` // version counters for state sync } @@ -62,7 +62,7 @@ type StateVersion struct { } // NewOKResponse creates a success response frame. -func NewOKResponse(id string, payload interface{}) *ResponseFrame { +func NewOKResponse(id string, payload any) *ResponseFrame { return &ResponseFrame{ Type: FrameTypeResponse, ID: id, @@ -85,7 +85,7 @@ func NewErrorResponse(id string, code, message string) *ResponseFrame { } // NewEvent creates an event frame. -func NewEvent(event string, payload interface{}) *EventFrame { +func NewEvent(event string, payload any) *EventFrame { return &EventFrame{ Type: FrameTypeEvent, Event: event,