diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8854b519..042f22a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,7 @@ jobs: - run: go build ./... - run: go test -race ./... - run: go vet ./... + - run: make lint web: runs-on: ubuntu-latest diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..cc8973fc --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,69 @@ +version: "2" +linters: + # enable: + # - asasalint + # - bidichk + # - bodyclose + # - cyclop + # - decorder + # - dupl + # - err113 + # - errname + # - errorlint + # - exhaustive + # - ginkgolinter + # - gocheckcompilerdirectives + # - gocognit + # - goconst + # - gocritic + # - goheader + # - lll + # - misspell + # - revive + # - unparam + exclusions: + generated: lax + rules: + - linters: + - errcheck + - err113 + - revive + - lll + - ginkgolinter + path: _test.go + - text: should not use dot imports + path: _test.go + - text: lines are duplicate of + path: _test.go + - text: seems to be unused + path: _test.go + - text: not checked + path: .go + - linters: + - staticcheck + - unused + - ineffassign + path: .go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +run: + timeout: 5m +issues: + max-issues-per-linter: 50 + max-same-issues: 3 + new: false + fix: true + uniq-by-line: true + whole-files: false \ No newline at end of file diff --git a/Makefile b/Makefile index 13e50c43..c60864ef 100644 --- a/Makefile +++ b/Makefile @@ -55,4 +55,16 @@ setup: go mod download cd ui/web && pnpm install --frozen-lockfile -ci: build test vet check-web +GOPATH ?= $(shell go env GOPATH) +export BIN_DIR := $(GOPATH)/bin +export GOLANGCI_LINT := $(BIN_DIR)/golangci-lint + +$(GOLANGCI_LINT): + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(BIN_DIR) v2.4.0 + +.PHONY: lint +lint: $(GOLANGCI_LINT) + $(GOLANGCI_LINT) --version + $(GOLANGCI_LINT) run -v + +ci: build test lint vet check-web diff --git a/cmd/onboard.go b/cmd/onboard.go index cda3e4cb..57b25268 100644 --- a/cmd/onboard.go +++ b/cmd/onboard.go @@ -477,17 +477,18 @@ func runOnboard() { // --- Apply collected values to config --- // Provider & model - if providerChoice == "claude_cli" { + switch providerChoice { + case "claude_cli": cfg.Agents.Defaults.Provider = "claude-cli" cfg.Agents.Defaults.Model = cliModel cfg.Providers.ClaudeCLI.CLIPath = cliPath cfg.Providers.ClaudeCLI.Model = cliModel - } else if providerChoice == "custom" { + case "custom": cfg.Agents.Defaults.Provider = "openai" cfg.Providers.OpenAI.APIBase = customAPIBase cfg.Providers.OpenAI.APIKey = apiKey cfg.Agents.Defaults.Model = customModel - } else { + default: pi := providerMap[providerChoice] cfg.Agents.Defaults.Provider = pi.name applyProviderAPIKey(cfg, pi.name, apiKey) diff --git a/cmd/onboard_auto.go b/cmd/onboard_auto.go index 3a046074..3dfb19db 100644 --- a/cmd/onboard_auto.go +++ b/cmd/onboard_auto.go @@ -244,14 +244,14 @@ func saveCleanConfig(cfgPath string, cfg *config.Config) error { // Build agents section. agents := map[string]interface{}{ "defaults": map[string]interface{}{ - "workspace": cfg.Agents.Defaults.Workspace, + "workspace": cfg.Agents.Defaults.Workspace, "restrict_to_workspace": cfg.Agents.Defaults.RestrictToWorkspace, - "provider": cfg.Agents.Defaults.Provider, - "model": cfg.Agents.Defaults.Model, - "max_tokens": cfg.Agents.Defaults.MaxTokens, - "temperature": cfg.Agents.Defaults.Temperature, - "max_tool_iterations": cfg.Agents.Defaults.MaxToolIterations, - "context_window": cfg.Agents.Defaults.ContextWindow, + "provider": cfg.Agents.Defaults.Provider, + "model": cfg.Agents.Defaults.Model, + "max_tokens": cfg.Agents.Defaults.MaxTokens, + "temperature": cfg.Agents.Defaults.Temperature, + "max_tool_iterations": cfg.Agents.Defaults.MaxToolIterations, + "context_window": cfg.Agents.Defaults.ContextWindow, }, } @@ -283,16 +283,15 @@ func saveCleanConfig(cfgPath string, cfg *config.Config) error { // Build root config map. root := map[string]interface{}{ - "agents": agents, - "gateway": gateway, - "tools": tools, + "agents": agents, + "gateway": gateway, + "tools": tools, } if len(channels) > 0 { root["channels"] = channels } - data, err := json.MarshalIndent(root, "", " ") if err != nil { return err diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 4a705b93..8616e6a4 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -461,7 +461,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) iteration := 0 totalToolCalls := 0 var finalContent 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) @@ -474,7 +474,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. @@ -669,7 +669,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) @@ -1080,4 +1080,3 @@ func (l *Loop) scanWebToolResult(toolName string, result *tools.Result) { strings.Join(injMatches, ", "), result.ForLLM) } } - diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 1399609b..d40f7935 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -372,9 +372,10 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) { var sb string var mediaKinds []string for _, m := range toSummarize { - if m.Role == "user" { + switch m.Role { + case "user": sb += fmt.Sprintf("user: %s\n", m.Content) - } else if m.Role == "assistant" { + case "assistant": sb += fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content)) } for _, ref := range m.MediaRefs { diff --git a/internal/agent/loop_history_test.go b/internal/agent/loop_history_test.go index b44680f2..279a29ed 100644 --- a/internal/agent/loop_history_test.go +++ b/internal/agent/loop_history_test.go @@ -206,7 +206,7 @@ func TestSanitizeHistory_DropsOrphanedToolMidHistory(t *testing.T) { func TestEstimateTokens(t *testing.T) { msgs := []providers.Message{ - {Role: "user", Content: "Hello world!"}, // 12 chars → ~4 tokens + {Role: "user", Content: "Hello world!"}, // 12 chars → ~4 tokens {Role: "assistant", Content: "Hi there, how are you?"}, // 22 chars → ~7 tokens } got := EstimateTokens(msgs) diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index b1dfc49c..3c9c5cf1 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,7 +113,7 @@ 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" + 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 @@ -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/resolver.go b/internal/agent/resolver.go index a7dcefc5..19d535a7 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -278,11 +278,11 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { toolsReg = deps.Tools.Clone() } var mcpOpts []mcpbridge.ManagerOption - mcpOpts = append(mcpOpts, mcpbridge.WithStore(deps.MCPStore)) - if deps.MCPPool != nil { - mcpOpts = append(mcpOpts, mcpbridge.WithPool(deps.MCPPool)) - } - mcpMgr := mcpbridge.NewManager(toolsReg, mcpOpts...) + mcpOpts = append(mcpOpts, mcpbridge.WithStore(deps.MCPStore)) + if deps.MCPPool != nil { + mcpOpts = append(mcpOpts, mcpbridge.WithPool(deps.MCPPool)) + } + mcpMgr := mcpbridge.NewManager(toolsReg, mcpOpts...) if err := mcpMgr.LoadForAgent(ctx, ag.ID, ""); err != nil { slog.Warn("failed to load MCP servers for agent", "agent", agentKey, "error", err) } else if mcpMgr.IsSearchMode() { @@ -399,4 +399,3 @@ func (r *Router) InvalidateAll() { r.agents = make(map[string]*agentEntry) slog.Debug("invalidated all agent caches") } - diff --git a/internal/agent/router.go b/internal/agent/router.go index b4247a73..5600bbda 100644 --- a/internal/agent/router.go +++ b/internal/agent/router.go @@ -25,7 +25,7 @@ type agentEntry struct { type Router struct { agents map[string]*agentEntry mu sync.RWMutex - activeRuns sync.Map // runID → *ActiveRun + activeRuns sync.Map // runID → *ActiveRun resolver ResolverFunc // optional: lazy creation from DB ttl time.Duration } diff --git a/internal/agent/sanitize.go b/internal/agent/sanitize.go index 25c86a7a..7cd15b22 100644 --- a/internal/agent/sanitize.go +++ b/internal/agent/sanitize.go @@ -12,8 +12,8 @@ // - collapseConsecutiveDuplicateBlocks() // // Additional Go-specific: -// 5. stripEchoedSystemMessages() → strip hallucinated [System Message] blocks -// 6. stripGarbledToolXML() → strip garbled XML from models like DeepSeek +// 5. stripEchoedSystemMessages() → strip hallucinated [System Message] blocks +// 6. stripGarbledToolXML() → strip garbled XML from models like DeepSeek package agent import ( @@ -169,7 +169,9 @@ func stripDowngradedToolCallText(content string) string { // Matches TS stripThinkingTagsFromText() with strict mode. // Strips: ..., ..., ..., -// ... +// +// ... +// // Go regexp doesn't support backreferences, so we use separate patterns. var thinkingTagPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?is).*?`), diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index 5700eed7..fd7c52d1 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -23,25 +23,25 @@ type SystemPromptConfig struct { AgentID string Model string Workspace string - Channel string // runtime channel instance name (e.g. "my-telegram-bot") - ChannelType string // platform type (e.g. "zalo_personal", "telegram") - PeerKind string // "direct" or "group" - OwnerIDs []string // owner sender IDs - Mode PromptMode // full or minimal - ToolNames []string // registered tool names - SkillsSummary string // XML from skills.Loader.BuildSummary() - HasMemory bool // memory_search/memory_get available? - HasSpawn bool // spawn tool available? + Channel string // runtime channel instance name (e.g. "my-telegram-bot") + ChannelType string // platform type (e.g. "zalo_personal", "telegram") + PeerKind string // "direct" or "group" + OwnerIDs []string // owner sender IDs + Mode PromptMode // full or minimal + ToolNames []string // registered tool names + SkillsSummary string // XML from skills.Loader.BuildSummary() + HasMemory bool // memory_search/memory_get available? + HasSpawn bool // spawn tool available? ContextFiles []bootstrap.ContextFile // bootstrap files for # Project Context - ExtraPrompt string // extra system prompt (subagent context, etc.) - AgentType string // "open" or "predefined" — affects context file framing + ExtraPrompt string // extra system prompt (subagent context, etc.) + AgentType string // "open" or "predefined" — affects context file framing HasSkillSearch bool // skill_search tool registered? (for search-mode prompt) HasMCPToolSearch bool // mcp_tool_search tool registered? (MCP search mode) // Sandbox info — matching TS sandboxInfo in system-prompt.ts - SandboxEnabled bool // exec tool runs inside Docker sandbox? - SandboxContainerDir string // container-side workdir (e.g. "/workspace") + SandboxEnabled bool // exec tool runs inside Docker sandbox? + SandboxContainerDir string // container-side workdir (e.g. "/workspace") SandboxWorkspaceAccess string // "none", "ro", "rw" // Self-evolution: predefined agents can update SOUL.md (style/tone) @@ -51,16 +51,16 @@ type SystemPromptConfig struct { // coreToolSummaries maps tool names to one-line descriptions. // Shown in the ## Tooling section of the system prompt. var coreToolSummaries = map[string]string{ - "read_file": "Read file contents", - "write_file": "Create or overwrite files", - "list_files": "List directory contents", - "exec": "Run shell commands", - "memory_search": "Search indexed memory files (MEMORY.md + memory/*.md)", - "memory_get": "Read specific sections of memory files", - "spawn": "Spawn a subagent or delegate to another agent", - "web_search": "Search the web", - "web_fetch": "Fetch and extract content from a URL", - "cron": "Manage scheduled jobs and reminders", + "read_file": "Read file contents", + "write_file": "Create or overwrite files", + "list_files": "List directory contents", + "exec": "Run shell commands", + "memory_search": "Search indexed memory files (MEMORY.md + memory/*.md)", + "memory_get": "Read specific sections of memory files", + "spawn": "Spawn a subagent or delegate to another agent", + "web_search": "Search the web", + "web_fetch": "Fetch and extract content from a URL", + "cron": "Manage scheduled jobs and reminders", "skill_search": "Search available skills by keyword (weather, translate, github, etc.)", "mcp_tool_search": "Search for available MCP external integration tools by keyword", "browser": "Browse web pages interactively", @@ -346,4 +346,3 @@ func buildWorkspaceSection(workspace string, sandboxEnabled bool, containerDir s "", } } - diff --git a/internal/bootstrap/files.go b/internal/bootstrap/files.go index 43457ec5..890f476c 100644 --- a/internal/bootstrap/files.go +++ b/internal/bootstrap/files.go @@ -27,12 +27,12 @@ const ( UserFile = "USER.md" UserPredefinedFile = "USER_PREDEFINED.md" BootstrapFile = "BOOTSTRAP.md" - DelegationFile = "DELEGATION.md" - TeamFile = "TEAM.md" - AvailabilityFile = "AVAILABILITY.md" - MemoryFile = "MEMORY.md" - MemoryAltFile = "memory.md" - MemoryJSONFile = "MEMORY.json" + DelegationFile = "DELEGATION.md" + TeamFile = "TEAM.md" + AvailabilityFile = "AVAILABILITY.md" + MemoryFile = "MEMORY.md" + MemoryAltFile = "memory.md" + MemoryJSONFile = "MEMORY.json" ) // standardFiles is the ordered list of bootstrap files to load. diff --git a/internal/bus/bus.go b/internal/bus/bus.go index d1112838..43dc2e10 100644 --- a/internal/bus/bus.go +++ b/internal/bus/bus.go @@ -12,7 +12,7 @@ type MessageBus struct { outbound chan OutboundMessage // Channel message handlers (channel name → handler) - handlers map[string]MessageHandler + handlers map[string]MessageHandler handlerMu sync.RWMutex // Event subscribers (subscriber ID → handler) diff --git a/internal/channels/channel.go b/internal/channels/channel.go index b4cc99d5..3ff084fc 100644 --- a/internal/channels/channel.go +++ b/internal/channels/channel.go @@ -34,9 +34,9 @@ type DMPolicy string const ( DMPolicyPairing DMPolicy = "pairing" // Require pairing code - DMPolicyAllowlist DMPolicy = "allowlist" // Only whitelisted senders - DMPolicyOpen DMPolicy = "open" // Accept all - DMPolicyDisabled DMPolicy = "disabled" // Reject all DMs + DMPolicyAllowlist DMPolicy = "allowlist" // Only whitelisted senders + DMPolicyOpen DMPolicy = "open" // Accept all + DMPolicyDisabled DMPolicy = "disabled" // Reject all DMs ) // GroupPolicy controls how group messages are handled. @@ -44,8 +44,8 @@ type GroupPolicy string const ( GroupPolicyOpen GroupPolicy = "open" // Accept all groups - GroupPolicyAllowlist GroupPolicy = "allowlist" // Only whitelisted groups - GroupPolicyDisabled GroupPolicy = "disabled" // No group messages + GroupPolicyAllowlist GroupPolicy = "allowlist" // Only whitelisted groups + GroupPolicyDisabled GroupPolicy = "disabled" // No group messages ) // Channel defines the interface that all channel implementations must satisfy. diff --git a/internal/channels/discord/factory.go b/internal/channels/discord/factory.go index 1832b393..36e3e8d4 100644 --- a/internal/channels/discord/factory.go +++ b/internal/channels/discord/factory.go @@ -17,9 +17,9 @@ type discordCreds struct { // discordInstanceConfig maps the non-secret config JSONB from the channel_instances table. type discordInstanceConfig struct { - DMPolicy string `json:"dm_policy,omitempty"` - GroupPolicy string `json:"group_policy,omitempty"` - AllowFrom []string `json:"allow_from,omitempty"` + DMPolicy string `json:"dm_policy,omitempty"` + GroupPolicy string `json:"group_policy,omitempty"` + AllowFrom []string `json:"allow_from,omitempty"` RequireMention *bool `json:"require_mention,omitempty"` HistoryLimit int `json:"history_limit,omitempty"` BlockReply *bool `json:"block_reply,omitempty"` diff --git a/internal/channels/feishu/bot.go b/internal/channels/feishu/bot.go index 96220552..1c9c4dc9 100644 --- a/internal/channels/feishu/bot.go +++ b/internal/channels/feishu/bot.go @@ -13,16 +13,16 @@ import ( // messageContext holds parsed information from a Feishu message event. type messageContext struct { - ChatID string - MessageID string - SenderID string // sender_id.open_id - ChatType string // "p2p" or "group" - Content string - ContentType string // "text", "post", "image", etc. + ChatID string + MessageID string + SenderID string // sender_id.open_id + ChatType string // "p2p" or "group" + Content string + ContentType string // "text", "post", "image", etc. MentionedBot bool - RootID string // thread root message ID - ParentID string // parent message ID - Mentions []mentionInfo + RootID string // thread root message ID + ParentID string // parent message ID + Mentions []mentionInfo } type mentionInfo struct { diff --git a/internal/channels/feishu/factory.go b/internal/channels/feishu/factory.go index 14870645..4824dc7f 100644 --- a/internal/channels/feishu/factory.go +++ b/internal/channels/feishu/factory.go @@ -20,21 +20,21 @@ type feishuCreds struct { // feishuInstanceConfig maps the non-secret config JSONB from the channel_instances table. type feishuInstanceConfig struct { - Domain string `json:"domain,omitempty"` - ConnectionMode string `json:"connection_mode,omitempty"` - WebhookPort int `json:"webhook_port,omitempty"` - WebhookPath string `json:"webhook_path,omitempty"` - AllowFrom []string `json:"allow_from,omitempty"` - DMPolicy string `json:"dm_policy,omitempty"` - GroupPolicy string `json:"group_policy,omitempty"` - GroupAllowFrom []string `json:"group_allow_from,omitempty"` - RequireMention *bool `json:"require_mention,omitempty"` - TopicSessionMode string `json:"topic_session_mode,omitempty"` - TextChunkLimit int `json:"text_chunk_limit,omitempty"` - MediaMaxMB int `json:"media_max_mb,omitempty"` - RenderMode string `json:"render_mode,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - ReactionLevel string `json:"reaction_level,omitempty"` + Domain string `json:"domain,omitempty"` + ConnectionMode string `json:"connection_mode,omitempty"` + WebhookPort int `json:"webhook_port,omitempty"` + WebhookPath string `json:"webhook_path,omitempty"` + AllowFrom []string `json:"allow_from,omitempty"` + DMPolicy string `json:"dm_policy,omitempty"` + GroupPolicy string `json:"group_policy,omitempty"` + GroupAllowFrom []string `json:"group_allow_from,omitempty"` + RequireMention *bool `json:"require_mention,omitempty"` + TopicSessionMode string `json:"topic_session_mode,omitempty"` + TextChunkLimit int `json:"text_chunk_limit,omitempty"` + MediaMaxMB int `json:"media_max_mb,omitempty"` + RenderMode string `json:"render_mode,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + ReactionLevel string `json:"reaction_level,omitempty"` HistoryLimit int `json:"history_limit,omitempty"` BlockReply *bool `json:"block_reply,omitempty"` STTProxyURL string `json:"stt_proxy_url,omitempty"` diff --git a/internal/channels/feishu/feishu.go b/internal/channels/feishu/feishu.go index e7d49e7b..efcedf91 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 diff --git a/internal/channels/feishu/larkevents.go b/internal/channels/feishu/larkevents.go index c786d744..a1fcaf1d 100644 --- a/internal/channels/feishu/larkevents.go +++ b/internal/channels/feishu/larkevents.go @@ -53,8 +53,8 @@ type EventMessage struct { } type EventMention struct { - Key string `json:"key"` - ID struct { + Key string `json:"key"` + ID struct { OpenID string `json:"open_id"` UserID string `json:"user_id"` UnionID string `json:"union_id"` @@ -69,9 +69,9 @@ type EventMention struct { // Schema v1.0 uses flat structure, v2.0 uses header+event. type webhookEvent struct { // v2.0 fields - Schema string `json:"schema"` - Header json.RawMessage `json:"header"` - Event json.RawMessage `json:"event"` + Schema string `json:"schema"` + Header json.RawMessage `json:"header"` + Event json.RawMessage `json:"event"` // v1.0 fields (also used for URL verification challenge) Type string `json:"type"` diff --git a/internal/channels/feishu/larkws.go b/internal/channels/feishu/larkws.go index 5a37ba31..d020f7d2 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. @@ -310,9 +310,7 @@ func (c *WSClient) handleFrame(ctx context.Context, f *wsFrame) { func (c *WSClient) sendResponse(original *wsFrame, headers map[string]string) { respHeaders := make([]wsHeader, 0, len(original.Headers)+1) - for _, h := range original.Headers { - respHeaders = append(respHeaders, h) - } + respHeaders = append(respHeaders, original.Headers...) respHeaders = append(respHeaders, wsHeader{Key: "biz_rt", Value: "0"}) respPayload, _ := json.Marshal(map[string]interface{}{ @@ -415,4 +413,3 @@ func (c *WSClient) reassemble(msgID string, total, seq int, data []byte) []byte delete(c.fragments, msgID) return result } - diff --git a/internal/channels/manager.go b/internal/channels/manager.go index 91187eaa..6b7cfe71 100644 --- a/internal/channels/manager.go +++ b/internal/channels/manager.go @@ -16,15 +16,15 @@ import ( // RunContext tracks an active agent run for streaming/reaction event forwarding. type RunContext struct { - ChannelName string - ChatID string - MessageID string // platform message ID (string to support Feishu "om_xxx", Telegram "12345", etc.) - Metadata map[string]string // outbound routing metadata (thread_id, local_key, group_id) + ChannelName string + ChatID string + MessageID string // platform message ID (string to support Feishu "om_xxx", Telegram "12345", etc.) + Metadata map[string]string // outbound routing metadata (thread_id, local_key, group_id) Streaming bool // whether run uses streaming (to avoid double-delivery of block replies) BlockReplyEnabled bool // whether block.reply delivery is enabled for this run (resolved at RegisterRun time) - mu sync.Mutex - streamBuffer string // accumulated streaming text (chunks are deltas) - inToolPhase bool // true after tool.call, reset on next chunk (new LLM iteration) + mu sync.Mutex + streamBuffer string // accumulated streaming text (chunks are deltas) + inToolPhase bool // true after tool.call, reset on next chunk (new LLM iteration) } // Manager manages all registered channels, handling their lifecycle diff --git a/internal/channels/slack/channel.go b/internal/channels/slack/channel.go index 4ab94bc2..237d6d6c 100644 --- a/internal/channels/slack/channel.go +++ b/internal/channels/slack/channel.go @@ -28,9 +28,9 @@ const ( // Channel connects to Slack via Socket Mode for event-driven messaging. type Channel struct { *channels.BaseChannel - api *slackapi.Client // Bot Token API client (xoxb-) - userAPI *slackapi.Client // User Token API client (xoxp-, optional) - sm *socketmode.Client // Socket Mode client (xapp-) + api *slackapi.Client // Bot Token API client (xoxb-) + userAPI *slackapi.Client // User Token API client (xoxp-, optional) + sm *socketmode.Client // Socket Mode client (xapp-) config config.SlackConfig botUserID string // populated on Start() via auth.test teamID string // populated on Start() via auth.test @@ -56,7 +56,7 @@ type Channel struct { groupHistory *channels.PendingHistory historyLimit int debounceDelay time.Duration - threadTTL time.Duration // thread participation expiry (0 = disabled) + threadTTL time.Duration // thread participation expiry (0 = disabled) wg sync.WaitGroup // tracks goroutines for clean shutdown cancelFn context.CancelFunc } diff --git a/internal/channels/slack/handlers.go b/internal/channels/slack/handlers.go index 180d912b..e516efdb 100644 --- a/internal/channels/slack/handlers.go +++ b/internal/channels/slack/handlers.go @@ -650,11 +650,11 @@ func (c *Channel) uploadFile(channelID, threadTS string, media bus.MediaAttachme } params := slackapi.UploadFileParameters{ - Filename: fileName, - FileSize: len(data), - Reader: bytes.NewReader(data), - Title: fileName, - Channel: channelID, + Filename: fileName, + FileSize: len(data), + Reader: bytes.NewReader(data), + Title: fileName, + Channel: channelID, ThreadTimestamp: threadTS, } diff --git a/internal/channels/telegram/channel.go b/internal/channels/telegram/channel.go index 41a4402a..8a4f0788 100644 --- a/internal/channels/telegram/channel.go +++ b/internal/channels/telegram/channel.go @@ -38,7 +38,7 @@ type Channel struct { historyLimit int requireMention bool pollCancel context.CancelFunc // cancels the long polling context - pollDone chan struct{} // closed when polling goroutine exits + pollDone chan struct{} // closed when polling goroutine exits } type thinkingCancel struct { diff --git a/internal/channels/telegram/commands.go b/internal/channels/telegram/commands.go index 4cbb3aa2..33e9f1c5 100644 --- a/internal/channels/telegram/commands.go +++ b/internal/channels/telegram/commands.go @@ -212,4 +212,3 @@ func (c *Channel) handleBotCommand(ctx context.Context, message *telego.Message, return false } - diff --git a/internal/channels/telegram/context.go b/internal/channels/telegram/context.go index 1b32ce7a..37631485 100644 --- a/internal/channels/telegram/context.go +++ b/internal/channels/telegram/context.go @@ -27,9 +27,9 @@ type ForwardInfo struct { // ReplyInfo contains metadata about the message being replied to. // Ref: TS describeReplyTarget() type ReplyInfo struct { - Sender string // sender name - Body string // quoted message text - IsBotReply bool // true if replying to bot's own message + Sender string // sender name + Body string // quoted message text + IsBotReply bool // true if replying to bot's own message } // LocationInfo contains geographic coordinates. diff --git a/internal/channels/telegram/format.go b/internal/channels/telegram/format.go index 77027a14..e2adf23d 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 { diff --git a/internal/channels/telegram/format_test.go b/internal/channels/telegram/format_test.go index 4952358d..7b0afc19 100644 --- a/internal/channels/telegram/format_test.go +++ b/internal/channels/telegram/format_test.go @@ -11,12 +11,12 @@ func TestDisplayWidth(t *testing.T) { want int }{ {"hello", 5}, - {"Khởi động", 9}, // Vietnamese diacritics = single-width + {"Khởi động", 9}, // Vietnamese diacritics = single-width {"Hardware tối thiểu", 18}, // Vietnamese diacritics = single-width {"Ngôn ngữ", 8}, - {"đ", 1}, // Vietnamese d-stroke = single-width - {"中文", 4}, // CJK = double-width - {"日本語", 6}, // CJK = double-width + {"đ", 1}, // Vietnamese d-stroke = single-width + {"中文", 4}, // CJK = double-width + {"日本語", 6}, // CJK = double-width } for _, tt := range tests { diff --git a/internal/channels/telegram/reactions.go b/internal/channels/telegram/reactions.go index 4f59b9d6..0994d3a3 100644 --- a/internal/channels/telegram/reactions.go +++ b/internal/channels/telegram/reactions.go @@ -81,10 +81,10 @@ type StatusReactionController struct { chatID int64 messageID int - mu sync.Mutex - currentEmoji string - lastStatus string - terminal bool // true once done/error is set + mu sync.Mutex + currentEmoji string + lastStatus string + terminal bool // true once done/error is set debounceTimer *time.Timer stallTimer *time.Timer } diff --git a/internal/channels/typing/controller.go b/internal/channels/typing/controller.go index a32d16c1..9b040296 100644 --- a/internal/channels/typing/controller.go +++ b/internal/channels/typing/controller.go @@ -40,10 +40,10 @@ type Controller struct { mu sync.Mutex // State flags - closed bool // post-close guard: prevents stale startFn calls - runComplete bool // signal 1: agent finished processing + closed bool // post-close guard: prevents stale startFn calls + runComplete bool // signal 1: agent finished processing dispatchIdle bool // signal 2: message delivery finished - stopSent bool // prevents duplicate stopFn calls + stopSent bool // prevents duplicate stopFn calls // Configuration maxDuration time.Duration diff --git a/internal/channels/zalo/personal/channel.go b/internal/channels/zalo/personal/channel.go index f4ab8274..997e5edc 100644 --- a/internal/channels/zalo/personal/channel.go +++ b/internal/channels/zalo/personal/channel.go @@ -22,10 +22,10 @@ import ( ) const ( - maxTextLength = 2000 - maxChannelRestarts = 10 - maxChannelBackoff = 60 * time.Second - code3000InitialDelay = 60 * time.Second + maxTextLength = 2000 + maxChannelRestarts = 10 + maxChannelBackoff = 60 * time.Second + code3000InitialDelay = 60 * time.Second ) // Channel connects to Zalo Personal Chat via the internal protocol port (from zcago, MIT). diff --git a/internal/channels/zalo/personal/factory.go b/internal/channels/zalo/personal/factory.go index 194d545f..0d0e95a1 100644 --- a/internal/channels/zalo/personal/factory.go +++ b/internal/channels/zalo/personal/factory.go @@ -13,10 +13,10 @@ import ( // zaloCreds maps the credentials JSON from the channel_instances table. type zaloCreds struct { - IMEI string `json:"imei"` + IMEI string `json:"imei"` Cookie *protocol.CookieUnion `json:"cookie"` - UserAgent string `json:"userAgent"` - Language *string `json:"language,omitempty"` + UserAgent string `json:"userAgent"` + Language *string `json:"language,omitempty"` } // zaloInstanceConfig maps the config JSONB from the channel_instances table. diff --git a/internal/channels/zalo/personal/protocol/auth.go b/internal/channels/zalo/personal/protocol/auth.go index ba186474..78dc5534 100644 --- a/internal/channels/zalo/personal/protocol/auth.go +++ b/internal/channels/zalo/personal/protocol/auth.go @@ -294,13 +294,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) { diff --git a/internal/channels/zalo/personal/protocol/client.go b/internal/channels/zalo/personal/protocol/client.go index 8dfe8276..38ebf481 100644 --- a/internal/channels/zalo/personal/protocol/client.go +++ b/internal/channels/zalo/personal/protocol/client.go @@ -131,11 +131,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/config.go b/internal/channels/zalo/personal/protocol/config.go index 1dc9e032..b5886dcf 100644 --- a/internal/channels/zalo/personal/protocol/config.go +++ b/internal/channels/zalo/personal/protocol/config.go @@ -180,7 +180,7 @@ func NewHTTPCookies(hc []*http.Cookie) CookieUnion { return CookieUnion{cookies: cookies} } -func (cu *CookieUnion) IsValid() bool { return cu.cookies != nil || cu.j2cookie != nil } +func (cu *CookieUnion) IsValid() bool { return cu.cookies != nil || cu.j2cookie != nil } func (cu *CookieUnion) GetCookies() []Cookie { if cu.cookies != nil { return cu.cookies diff --git a/internal/channels/zalo/personal/protocol/listener.go b/internal/channels/zalo/personal/protocol/listener.go index 79a1cebb..b85f1ae6 100644 --- a/internal/channels/zalo/personal/protocol/listener.go +++ b/internal/channels/zalo/personal/protocol/listener.go @@ -31,8 +31,8 @@ type Listener struct { client *WSClient cipherKey string connectedAt time.Time - stopped bool // prevents reconnect after Stop() - reconnTimer *time.Timer // pending reconnect timer, cancelled on Stop() + stopped bool // prevents reconnect after Stop() + reconnTimer *time.Timer // pending reconnect timer, cancelled on Stop() retryStates map[string]*retryState @@ -93,7 +93,7 @@ func NewListener(sess *Session) (*Listener, error) { } // Channel accessors. -func (ln *Listener) Messages() <-chan Message { return ln.messageCh } +func (ln *Listener) Messages() <-chan Message { return ln.messageCh } func (ln *Listener) Disconnected() <-chan CloseInfo { return ln.disconnectedCh } func (ln *Listener) Closed() <-chan CloseInfo { return ln.closedCh } func (ln *Listener) Errors() <-chan error { return ln.errorCh } @@ -282,4 +282,3 @@ func (ln *Listener) handleCipherKey(ctx context.Context, key *string) { } } } - diff --git a/internal/channels/zalo/personal/protocol/listener_handlers.go b/internal/channels/zalo/personal/protocol/listener_handlers.go index d170a0e4..cddedd64 100644 --- a/internal/channels/zalo/personal/protocol/listener_handlers.go +++ b/internal/channels/zalo/personal/protocol/listener_handlers.go @@ -235,9 +235,9 @@ func (ln *Listener) sendPing(ctx context.Context) { body, _ := json.Marshal(data) buf := make([]byte, 4+len(body)) - buf[0] = 1 // version + buf[0] = 1 // version binary.LittleEndian.PutUint16(buf[1:3], 2) // cmd=2 - buf[3] = 1 // subCmd=1 + buf[3] = 1 // subCmd=1 copy(buf[4:], body) ln.mu.RLock() diff --git a/internal/channels/zalo/personal/protocol/message.go b/internal/channels/zalo/personal/protocol/message.go index 2bb8b43d..ba457399 100644 --- a/internal/channels/zalo/personal/protocol/message.go +++ b/internal/channels/zalo/personal/protocol/message.go @@ -37,7 +37,7 @@ type TGroupMessage struct { // TMention represents an @mention in a group message. type TMention struct { - UID string `json:"uid"` // user ID or "-1" for @all + UID string `json:"uid"` // user ID or "-1" for @all Pos int `json:"pos"` Len int `json:"len"` Type MentionType `json:"type"` // 0=individual, 1=all @@ -47,9 +47,9 @@ type TMention struct { type MentionType int const ( - MentionEach MentionType = 0 - MentionAll MentionType = 1 - MentionAllUID = "-1" + MentionEach MentionType = 0 + MentionAll MentionType = 1 + MentionAllUID = "-1" ) // Content is a union type: can be a plain string or an attachment object. diff --git a/internal/channels/zalo/personal/protocol/send_file.go b/internal/channels/zalo/personal/protocol/send_file.go index 9f83080d..21d08727 100644 --- a/internal/channels/zalo/personal/protocol/send_file.go +++ b/internal/channels/zalo/personal/protocol/send_file.go @@ -17,7 +17,7 @@ import ( type FileUploadResult struct { FileID string `json:"fileId"` FileURL string `json:"fileUrl"` // populated from WS callback - ClientFileID json.Number `json:"clientFileId"` // Zalo may return string or number + ClientFileID json.Number `json:"clientFileId"` // Zalo may return string or number ChunkID int `json:"chunkId"` Finished int `json:"finished"` diff --git a/internal/channels/zalo/personal/protocol/send_image.go b/internal/channels/zalo/personal/protocol/send_image.go index eaad8d65..633730bb 100644 --- a/internal/channels/zalo/personal/protocol/send_image.go +++ b/internal/channels/zalo/personal/protocol/send_image.go @@ -22,9 +22,9 @@ type ImageUploadResult struct { HDUrl string `json:"hdUrl"` ThumbURL string `json:"thumbUrl"` PhotoID json.Number `json:"photoId"` // Zalo may return string or number - ClientFileID json.Number `json:"clientFileId"` // Zalo may return string or number + ClientFileID json.Number `json:"clientFileId"` // Zalo may return string or number ChunkID int `json:"chunkId"` - Finished FlexBool `json:"finished"` // Zalo returns bool or int depending on endpoint + Finished FlexBool `json:"finished"` // Zalo returns bool or int depending on endpoint // Set by caller (not from API response). Width int `json:"-"` diff --git a/internal/channels/zalo/personal/zalomethods/contacts.go b/internal/channels/zalo/personal/zalomethods/contacts.go index c0690c80..ba4964e6 100644 --- a/internal/channels/zalo/personal/zalomethods/contacts.go +++ b/internal/channels/zalo/personal/zalomethods/contacts.go @@ -64,10 +64,10 @@ func (m *ContactsMethods) handleContacts(ctx context.Context, client *gateway.Cl // Parse credentials (same struct as factory.go) var creds struct { - IMEI string `json:"imei"` + IMEI string `json:"imei"` Cookie *protocol.CookieUnion `json:"cookie"` - UserAgent string `json:"userAgent"` - Language *string `json:"language,omitempty"` + UserAgent string `json:"userAgent"` + Language *string `json:"language,omitempty"` } if err := json.Unmarshal(inst.Credentials, &creds); err != nil { client.SendResponse(goclawprotocol.NewErrorResponse(req.ID, goclawprotocol.ErrInternal, "failed to parse credentials")) diff --git a/internal/channels/zalo/zalo.go b/internal/channels/zalo/zalo.go index 724d2e00..46eb08ba 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. diff --git a/internal/config/config.go b/internal/config/config.go index f27a0be1..cfe8ede8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,16 +52,16 @@ type Config struct { Telemetry TelemetryConfig `json:"telemetry,omitempty"` Tailscale TailscaleConfig `json:"tailscale,omitempty"` 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) @@ -133,7 +133,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. @@ -142,20 +142,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. @@ -201,9 +201,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) @@ -351,9 +351,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 16ac4d85..31940cc9 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,20 +169,20 @@ 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"` - 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"` + ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` } // ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key). @@ -302,9 +302,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). diff --git a/internal/config/hotreload.go b/internal/config/hotreload.go index 1d251842..1fd083cb 100644 --- a/internal/config/hotreload.go +++ b/internal/config/hotreload.go @@ -15,12 +15,12 @@ type ChangeHandler func(cfg *Config) // Watcher watches a config file for changes and reloads it. // Changes are debounced (300ms) to avoid rapid reloads. type Watcher struct { - path string - watcher *fsnotify.Watcher - handlers []ChangeHandler - debounce time.Duration - stopChan chan struct{} - mu sync.Mutex + path string + watcher *fsnotify.Watcher + handlers []ChangeHandler + debounce time.Duration + stopChan chan struct{} + mu sync.Mutex } // NewWatcher creates a config file watcher. diff --git a/internal/cron/service.go b/internal/cron/service.go index 2c87edeb..ce534e9c 100644 --- a/internal/cron/service.go +++ b/internal/cron/service.go @@ -104,10 +104,10 @@ func (cs *Service) AddJob(name string, schedule Schedule, message string, delive now := nowMS() job := Job{ - ID: generateID(), - Name: name, - AgentID: agentID, - Enabled: true, + ID: generateID(), + Name: name, + AgentID: agentID, + Enabled: true, Schedule: schedule, Payload: Payload{ Kind: "agent_turn", @@ -269,4 +269,3 @@ func (cs *Service) Status() map[string]interface{} { "nextWakeAtMs": cs.getNextWakeMS(), } } - diff --git a/internal/gateway/client.go b/internal/gateway/client.go index a22a9ab4..2a0d15c8 100644 --- a/internal/gateway/client.go +++ b/internal/gateway/client.go @@ -27,10 +27,10 @@ type Client struct { remoteAddr string // peer IP (extracted from proxy headers or RemoteAddr) // Browser pairing state - pairingCode string // 8-char code if pending approval - pairingPending bool // true while waiting for admin approval - pairedSenderID string // senderID used for browser pairing auth (for revocation lookup) - pairedChannel string // channel used for pairing auth (e.g., "browser") + pairingCode string // 8-char code if pending approval + pairingPending bool // true while waiting for admin approval + pairedSenderID string // senderID used for browser pairing auth (for revocation lookup) + pairedChannel string // channel used for pairing auth (e.g., "browser") } func NewClient(conn *websocket.Conn, server *Server, remoteIP string) *Client { @@ -128,7 +128,7 @@ func (c *Client) handleFrame(ctx context.Context, data []byte) { // First request must be "connect" (except browser.pairing.status for pending clients) if !c.authenticated && req.Method != protocol.MethodConnect { - if !(c.pairingPending && req.Method == protocol.MethodBrowserPairingStatus) { + if !c.pairingPending || req.Method != protocol.MethodBrowserPairingStatus { c.sendError(req.ID, protocol.ErrUnauthorized, "first request must be 'connect'") return } diff --git a/internal/gateway/methods/agents.go b/internal/gateway/methods/agents.go index f2d6c217..091994b8 100644 --- a/internal/gateway/methods/agents.go +++ b/internal/gateway/methods/agents.go @@ -163,8 +163,8 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client, Workspace string `json:"workspace"` Emoji string `json:"emoji"` Avatar string `json:"avatar"` - AgentType string `json:"agent_type"` // "open" (default) or "predefined" - OwnerIDs []string `json:"owner_ids,omitempty"` // first entry used as DB owner_id; falls back to "system" + AgentType string `json:"agent_type"` // "open" (default) or "predefined" + OwnerIDs []string `json:"owner_ids,omitempty"` // first entry used as DB owner_id; falls back to "system" // Per-agent config overrides ToolsConfig json.RawMessage `json:"tools_config,omitempty"` SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"` diff --git a/internal/gateway/methods/channel_instances.go b/internal/gateway/methods/channel_instances.go index 6b64d2aa..98a17a6f 100644 --- a/internal/gateway/methods/channel_instances.go +++ b/internal/gateway/methods/channel_instances.go @@ -209,18 +209,18 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate // 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, + "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 diff --git a/internal/gateway/methods/cron.go b/internal/gateway/methods/cron.go index 3275477f..bc195f94 100644 --- a/internal/gateway/methods/cron.go +++ b/internal/gateway/methods/cron.go @@ -50,13 +50,13 @@ func (m *CronMethods) handleList(_ context.Context, client *gateway.Client, req func (m *CronMethods) handleCreate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { var params struct { - Name string `json:"name"` + Name string `json:"name"` Schedule store.CronSchedule `json:"schedule"` - Message string `json:"message"` - Deliver bool `json:"deliver"` - Channel string `json:"channel"` - To string `json:"to"` - AgentID string `json:"agentId"` + Message string `json:"message"` + Deliver bool `json:"deliver"` + Channel string `json:"channel"` + To string `json:"to"` + AgentID string `json:"agentId"` } if req.Params != nil { json.Unmarshal(req.Params, ¶ms) @@ -140,8 +140,8 @@ func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, re func (m *CronMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) { 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 { diff --git a/internal/gateway/methods/exec_approval.go b/internal/gateway/methods/exec_approval.go index c89971b8..ddc76cfe 100644 --- a/internal/gateway/methods/exec_approval.go +++ b/internal/gateway/methods/exec_approval.go @@ -62,8 +62,8 @@ func (m *ExecApprovalMethods) handleApprove(_ context.Context, client *gateway.C } var params struct { - ID string `json:"id"` - Always bool `json:"always"` // true = allow-always, false = allow-once + ID string `json:"id"` + Always bool `json:"always"` // true = allow-always, false = allow-once } if req.Params != nil { json.Unmarshal(req.Params, ¶ms) diff --git a/internal/gateway/router.go b/internal/gateway/router.go index dfee89a6..65cd8bea 100644 --- a/internal/gateway/router.go +++ b/internal/gateway/router.go @@ -209,13 +209,13 @@ func (r *MethodRouter) handleHealth(ctx context.Context, client *Client, req *pr } client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{ - "status": "ok", - "version": s.version, - "uptime": uptimeMs, - "mode": mode, - "database": dbStatus, - "tools": toolCount, - "clients": clientList, + "status": "ok", + "version": s.version, + "uptime": uptimeMs, + "mode": mode, + "database": dbStatus, + "tools": toolCount, + "clients": clientList, "currentId": client.ID(), })) } @@ -244,4 +244,3 @@ func (r *MethodRouter) handleStatus(ctx context.Context, client *Client, req *pr "sessions": sessionCount, })) } - diff --git a/internal/gateway/server.go b/internal/gateway/server.go index eda92206..d65769dc 100644 --- a/internal/gateway/server.go +++ b/internal/gateway/server.go @@ -33,12 +33,12 @@ type Server struct { tools *tools.Registry router *MethodRouter - policyEngine *permissions.PolicyEngine - pairingService store.PairingStore - agentsHandler *httpapi.AgentsHandler // agent CRUD API - skillsHandler *httpapi.SkillsHandler // skill management API - tracesHandler *httpapi.TracesHandler // LLM trace listing API - mcpHandler *httpapi.MCPHandler // MCP server management API + policyEngine *permissions.PolicyEngine + pairingService store.PairingStore + agentsHandler *httpapi.AgentsHandler // agent CRUD API + skillsHandler *httpapi.SkillsHandler // skill management API + tracesHandler *httpapi.TracesHandler // LLM trace listing API + mcpHandler *httpapi.MCPHandler // MCP server management API customToolsHandler *httpapi.CustomToolsHandler // custom tool CRUD API channelInstancesHandler *httpapi.ChannelInstancesHandler // channel instance CRUD API providersHandler *httpapi.ProvidersHandler // provider CRUD API @@ -49,7 +49,7 @@ type Server struct { storageHandler *httpapi.StorageHandler // storage file management mediaUploadHandler *httpapi.MediaUploadHandler // media upload endpoint mediaServeHandler *httpapi.MediaServeHandler // media serve endpoint - agentStore store.AgentStore // for context injection in tools_invoke + agentStore store.AgentStore // for context injection in tools_invoke upgrader websocket.Upgrader rateLimiter *RateLimiter diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index b4b9dca8..c9a87c04 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -14,8 +14,8 @@ const ( // HookConfig defines a single quality gate. type HookConfig struct { - Event string `json:"event"` // e.g. "delegation.completed" - Type HookType `json:"type"` // "command" or "agent" + Event string `json:"event"` // e.g. "delegation.completed" + Type HookType `json:"type"` // "command" or "agent" Command string `json:"command,omitempty"` // for type=command: shell command to run Agent string `json:"agent,omitempty"` // for type=agent: reviewer agent key BlockOnFailure bool `json:"block_on_failure"` // true = block and optionally retry diff --git a/internal/http/agents.go b/internal/http/agents.go index d2b24bf6..ecac8f37 100644 --- a/internal/http/agents.go +++ b/internal/http/agents.go @@ -19,8 +19,8 @@ import ( type AgentsHandler struct { agents store.AgentStore token string - msgBus *bus.MessageBus // for cache invalidation events (nil = no events) - summoner *AgentSummoner // LLM-based agent setup (nil = disabled) + msgBus *bus.MessageBus // for cache invalidation events (nil = no events) + summoner *AgentSummoner // LLM-based agent setup (nil = disabled) isOwner func(string) bool // checks if user ID is a system owner (nil = no owners configured) } diff --git a/internal/http/channel_instances.go b/internal/http/channel_instances.go index c532bc0b..e8851234 100644 --- a/internal/http/channel_instances.go +++ b/internal/http/channel_instances.go @@ -241,18 +241,18 @@ func (h *ChannelInstancesHandler) handleDelete(w http.ResponseWriter, r *http.Re // maskInstanceHTTP returns a map with credentials masked for HTTP responses. func maskInstanceHTTP(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, + "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, } if len(inst.Credentials) > 0 { diff --git a/internal/http/chat_completions.go b/internal/http/chat_completions.go index 90db0e8d..df74493b 100644 --- a/internal/http/chat_completions.go +++ b/internal/http/chat_completions.go @@ -18,9 +18,9 @@ import ( type ChatCompletionsHandler struct { agents *agent.Router sessions store.SessionStore - token string // expected bearer token (empty = no auth) + token string // expected bearer token (empty = no auth) isManaged bool - rateLimiter func(string) bool // rate limit check: key → allowed (nil = no limit) + rateLimiter func(string) bool // rate limit check: key → allowed (nil = no limit) } // NewChatCompletionsHandler creates a handler for the chat completions endpoint. @@ -39,10 +39,10 @@ func (h *ChatCompletionsHandler) SetRateLimiter(fn func(string) bool) { } type chatCompletionsRequest struct { - Model string `json:"model"` - Messages []chatMessage `json:"messages"` - Stream bool `json:"stream"` - User string `json:"user,omitempty"` + Model string `json:"model"` + Messages []chatMessage `json:"messages"` + Stream bool `json:"stream"` + User string `json:"user,omitempty"` } type chatMessage struct { @@ -52,12 +52,12 @@ type chatMessage struct { } type chatCompletionsResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []chatChoice `json:"choices"` - Usage *chatUsage `json:"usage,omitempty"` + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []chatChoice `json:"choices"` + Usage *chatUsage `json:"usage,omitempty"` } type chatChoice struct { diff --git a/internal/http/provider_models.go b/internal/http/provider_models.go index 81e88533..0e51ed77 100644 --- a/internal/http/provider_models.go +++ b/internal/http/provider_models.go @@ -268,4 +268,3 @@ func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo } return models, nil } - diff --git a/internal/http/storage.go b/internal/http/storage.go index dab3109b..1bcad171 100644 --- a/internal/http/storage.go +++ b/internal/http/storage.go @@ -87,8 +87,8 @@ func (h *StorageHandler) handleList(w http.ResponseWriter, r *http.Request) { Name string `json:"name"` IsDir bool `json:"isDir"` Size int64 `json:"size"` - TotalSize int64 `json:"totalSize"` // recursive size for directories - Protected bool `json:"protected"` // true if deletion is blocked + TotalSize int64 `json:"totalSize"` // recursive size for directories + Protected bool `json:"protected"` // true if deletion is blocked } // Compute directory sizes via a two-pass approach: diff --git a/internal/http/summoner.go b/internal/http/summoner.go index 8c1d6f0c..8cac54ec 100644 --- a/internal/http/summoner.go +++ b/internal/http/summoner.go @@ -322,8 +322,8 @@ func (s *AgentSummoner) generateFiles(ctx context.Context, providerName, model, Options: map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, - providers.OptSessionKey: summonSessionKey, - providers.OptDisableTools: true, + providers.OptSessionKey: summonSessionKey, + providers.OptDisableTools: true, }, }) if err != nil { diff --git a/internal/mcp/manager.go b/internal/mcp/manager.go index de61e0bf..b7149d74 100644 --- a/internal/mcp/manager.go +++ b/internal/mcp/manager.go @@ -47,9 +47,9 @@ type serverState struct { timeoutSec int cancel context.CancelFunc - mu sync.Mutex - reconnAttempts int - lastErr string + mu sync.Mutex + reconnAttempts int + lastErr string } // Manager orchestrates MCP server connections and tool registration. @@ -74,12 +74,12 @@ type Manager struct { // Shared connection pool (nil = standalone mode) pool *Pool - poolServers map[string]struct{} // server names acquired from pool (for cleanup) - poolToolNames map[string][]string // per-agent tool names for pool-backed servers + poolServers map[string]struct{} // server names acquired from pool (for cleanup) + poolToolNames map[string][]string // per-agent tool names for pool-backed servers // Search mode: deferred tools not registered in registry deferredTools map[string]*BridgeTool // registeredName → BridgeTool - activatedTools map[string]struct{} // tracks activated tool names for group:mcp + activatedTools map[string]struct{} // tracks activated tool names for group:mcp searchMode bool } diff --git a/internal/oauth/openai.go b/internal/oauth/openai.go index 120924c3..cf1e3474 100644 --- a/internal/oauth/openai.go +++ b/internal/oauth/openai.go @@ -4,10 +4,10 @@ package oauth import ( "context" "crypto/rand" - "errors" "crypto/sha256" "encoding/base64" "encoding/json" + "errors" "fmt" "html" "io" @@ -16,16 +16,16 @@ import ( "net/http" "net/url" "os/exec" - "sync" "runtime" + "sync" "time" ) const ( - OpenAIAuthURL = "https://auth.openai.com/oauth/authorize" - OpenAITokenURL = "https://auth.openai.com/oauth/token" - OpenAIClientID = "app_EMoamEEZ73f0CkXaXp7hrann" - OpenAIScopes = "openid profile email offline_access api.connectors.read api.connectors.invoke" + OpenAIAuthURL = "https://auth.openai.com/oauth/authorize" + OpenAITokenURL = "https://auth.openai.com/oauth/token" + OpenAIClientID = "app_EMoamEEZ73f0CkXaXp7hrann" + OpenAIScopes = "openid profile email offline_access api.connectors.read api.connectors.invoke" OpenAIRedirectURI = "http://localhost:1455/auth/callback" callbackPort = "1455" @@ -133,15 +133,15 @@ func StartLoginOpenAI() (*PendingLogin, error) { params := url.Values{ "client_id": {OpenAIClientID}, - "redirect_uri": {OpenAIRedirectURI}, - "response_type": {"code"}, - "scope": {OpenAIScopes}, - "code_challenge": {challenge}, + "redirect_uri": {OpenAIRedirectURI}, + "response_type": {"code"}, + "scope": {OpenAIScopes}, + "code_challenge": {challenge}, "code_challenge_method": {"S256"}, - "state": {state}, + "state": {state}, "codex_cli_simplified_flow": {"true"}, "id_token_add_organizations": {"true"}, - "originator": {"pi"}, + "originator": {"pi"}, } authURL := OpenAIAuthURL + "?" + params.Encode() diff --git a/internal/providers/anthropic.go b/internal/providers/anthropic.go index c3c6fff2..8139f57e 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 @@ -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/claude_cli.go b/internal/providers/claude_cli.go index e42c70b8..7daad23e 100644 --- a/internal/providers/claude_cli.go +++ b/internal/providers/claude_cli.go @@ -16,16 +16,16 @@ const OptDisableTools = "disable_tools" // It acts as a thin proxy: CLI manages session history, tool execution, and context. // GoClaw only forwards the latest user message and streams back the response. type ClaudeCLIProvider struct { - cliPath string // path to claude binary (default: "claude") - defaultModel string // default: "sonnet" - baseWorkDir string // base dir for agent workspaces - mcpConfigPath string // pre-built MCP config file path (empty = no MCP) - permMode string // permission mode (default: "bypassPermissions") - hooksSettingsPath string // generated settings.json with security hooks (empty = no hooks) - hooksCleanup func() // cleanup function for hooks temp files - mcpCleanup func() // cleanup function for MCP config temp file - mu sync.Mutex // protects workdir creation - sessionMu sync.Map // key: string, value: *sync.Mutex — per-session lock + cliPath string // path to claude binary (default: "claude") + defaultModel string // default: "sonnet" + baseWorkDir string // base dir for agent workspaces + mcpConfigPath string // pre-built MCP config file path (empty = no MCP) + permMode string // permission mode (default: "bypassPermissions") + hooksSettingsPath string // generated settings.json with security hooks (empty = no hooks) + hooksCleanup func() // cleanup function for hooks temp files + mcpCleanup func() // cleanup function for MCP config temp file + mu sync.Mutex // protects workdir creation + sessionMu sync.Map // key: string, value: *sync.Mutex — per-session lock } // ClaudeCLIOption configures the provider. @@ -101,7 +101,7 @@ func NewClaudeCLIProvider(cliPath string, opts ...ClaudeCLIOption) *ClaudeCLIPro return p } -func (p *ClaudeCLIProvider) Name() string { return "claude-cli" } +func (p *ClaudeCLIProvider) Name() string { return "claude-cli" } func (p *ClaudeCLIProvider) DefaultModel() string { return p.defaultModel } // Close cleans up temp files (MCP config, hooks settings). Implements io.Closer. diff --git a/internal/providers/codex.go b/internal/providers/codex.go index 744a6c76..059bf73e 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 } diff --git a/internal/providers/codex_test.go b/internal/providers/codex_test.go index b7c1d60d..bbc86e3c 100644 --- a/internal/providers/codex_test.go +++ b/internal/providers/codex_test.go @@ -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/codex_types.go b/internal/providers/codex_types.go index 79a2a2cf..b0d9e6ba 100644 --- a/internal/providers/codex_types.go +++ b/internal/providers/codex_types.go @@ -3,24 +3,24 @@ package providers // Wire types for the OpenAI Responses API (Codex flow). type codexAPIResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Model string `json:"model"` - Output []codexItem `json:"output"` - Usage *codexUsage `json:"usage,omitempty"` - Status string `json:"status"` + ID string `json:"id"` + Object string `json:"object"` + Model string `json:"model"` + Output []codexItem `json:"output"` + Usage *codexUsage `json:"usage,omitempty"` + Status string `json:"status"` } type codexItem struct { - ID string `json:"id"` - Type string `json:"type"` // "message", "function_call", "reasoning" - Role string `json:"role,omitempty"` - Phase string `json:"phase,omitempty"` // gpt-5.3-codex: "commentary" or "final_answer" - Content []codexContent `json:"content,omitempty"` - CallID string `json:"call_id,omitempty"` - Name string `json:"name,omitempty"` - Arguments string `json:"arguments,omitempty"` - Summary []codexSummary `json:"summary,omitempty"` + ID string `json:"id"` + Type string `json:"type"` // "message", "function_call", "reasoning" + Role string `json:"role,omitempty"` + Phase string `json:"phase,omitempty"` // gpt-5.3-codex: "commentary" or "final_answer" + Content []codexContent `json:"content,omitempty"` + CallID string `json:"call_id,omitempty"` + Name string `json:"name,omitempty"` + Arguments string `json:"arguments,omitempty"` + Summary []codexSummary `json:"summary,omitempty"` } type codexContent struct { @@ -34,10 +34,10 @@ type codexSummary struct { } type codexUsage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - TotalTokens int `json:"total_tokens"` - OutputTokensDetails *codexTokensDetails `json:"output_tokens_details,omitempty"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + TotalTokens int `json:"total_tokens"` + OutputTokensDetails *codexTokensDetails `json:"output_tokens_details,omitempty"` } type codexTokensDetails struct { diff --git a/internal/providers/dashscope.go b/internal/providers/dashscope.go index 68203a3b..31f59c54 100644 --- a/internal/providers/dashscope.go +++ b/internal/providers/dashscope.go @@ -29,7 +29,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. diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 82343f87..c6244354 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"). @@ -404,4 +404,3 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse { return result } - diff --git a/internal/providers/types.go b/internal/providers/types.go index 054bccaa..ea2107c1 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). @@ -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 diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go index bd5b7612..86cbf02f 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, diff --git a/internal/scheduler/queue.go b/internal/scheduler/queue.go index 31c6ea27..70311f39 100644 --- a/internal/scheduler/queue.go +++ b/internal/scheduler/queue.go @@ -82,11 +82,11 @@ type activeRunEntry struct { // SessionQueue manages agent runs for a single session key. // Supports configurable concurrency: 1 (serial) or N (concurrent). type SessionQueue struct { - key string - config QueueConfig - runFn RunFunc + key string + config QueueConfig + runFn RunFunc laneMgr *LaneManager - lane string + lane string mu sync.Mutex queue []*PendingRequest diff --git a/internal/sessions/manager.go b/internal/sessions/manager.go index 24765a6a..9208847f 100644 --- a/internal/sessions/manager.go +++ b/internal/sessions/manager.go @@ -14,24 +14,24 @@ import ( // Session stores conversation history for one agent+scope combination. type Session struct { - Key string `json:"key"` // agent:{agentId}:{sessionKey} + Key string `json:"key"` // agent:{agentId}:{sessionKey} Messages []providers.Message `json:"messages"` Summary string `json:"summary,omitempty"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` // Metadata (matching TS SessionEntry subset) - Model string `json:"model,omitempty"` - Provider string `json:"provider,omitempty"` - Channel string `json:"channel,omitempty"` - InputTokens int64 `json:"inputTokens,omitempty"` - OutputTokens int64 `json:"outputTokens,omitempty"` - CompactionCount int `json:"compactionCount,omitempty"` + Model string `json:"model,omitempty"` + Provider string `json:"provider,omitempty"` + Channel string `json:"channel,omitempty"` + InputTokens int64 `json:"inputTokens,omitempty"` + OutputTokens int64 `json:"outputTokens,omitempty"` + CompactionCount int `json:"compactionCount,omitempty"` MemoryFlushCompactionCount int `json:"memoryFlushCompactionCount,omitempty"` MemoryFlushAt int64 `json:"memoryFlushAt,omitempty"` // unix ms Label string `json:"label,omitempty"` - SpawnedBy string `json:"spawnedBy,omitempty"` - SpawnDepth int `json:"spawnDepth,omitempty"` + SpawnedBy string `json:"spawnedBy,omitempty"` + SpawnDepth int `json:"spawnDepth,omitempty"` ContextWindow int `json:"contextWindow,omitempty"` LastPromptTokens int `json:"lastPromptTokens,omitempty"` @@ -395,24 +395,24 @@ func (m *Manager) Save(key string) error { // Snapshot under lock snapshot := Session{ - Key: s.Key, - Summary: s.Summary, - Created: s.Created, - Updated: s.Updated, - Model: s.Model, - Provider: s.Provider, - Channel: s.Channel, - InputTokens: s.InputTokens, - OutputTokens: s.OutputTokens, - CompactionCount: s.CompactionCount, + Key: s.Key, + Summary: s.Summary, + Created: s.Created, + Updated: s.Updated, + Model: s.Model, + Provider: s.Provider, + Channel: s.Channel, + InputTokens: s.InputTokens, + OutputTokens: s.OutputTokens, + CompactionCount: s.CompactionCount, MemoryFlushCompactionCount: s.MemoryFlushCompactionCount, - MemoryFlushAt: s.MemoryFlushAt, - Label: s.Label, - SpawnedBy: s.SpawnedBy, - SpawnDepth: s.SpawnDepth, - ContextWindow: s.ContextWindow, - LastPromptTokens: s.LastPromptTokens, - LastMessageCount: s.LastMessageCount, + MemoryFlushAt: s.MemoryFlushAt, + Label: s.Label, + SpawnedBy: s.SpawnedBy, + SpawnDepth: s.SpawnDepth, + ContextWindow: s.ContextWindow, + LastPromptTokens: s.LastPromptTokens, + LastMessageCount: s.LastMessageCount, } if len(s.Messages) > 0 { snapshot.Messages = make([]providers.Message, len(s.Messages)) diff --git a/internal/store/agent_link_store.go b/internal/store/agent_link_store.go index 8b67b8de..3f6a4850 100644 --- a/internal/store/agent_link_store.go +++ b/internal/store/agent_link_store.go @@ -25,7 +25,7 @@ type AgentLinkData struct { BaseModel SourceAgentID uuid.UUID `json:"source_agent_id"` TargetAgentID uuid.UUID `json:"target_agent_id"` - Direction string `json:"direction"` // "outbound", "inbound", "bidirectional" + Direction string `json:"direction"` // "outbound", "inbound", "bidirectional" TeamID *uuid.UUID `json:"team_id,omitempty"` // non-nil = auto-created by team Description string `json:"description,omitempty"` MaxConcurrent int `json:"max_concurrent"` diff --git a/internal/store/agent_store.go b/internal/store/agent_store.go index 2cca6714..70823eaa 100644 --- a/internal/store/agent_store.go +++ b/internal/store/agent_store.go @@ -16,9 +16,9 @@ const ( // Agent status constants. const ( - AgentStatusActive = "active" - AgentStatusInactive = "inactive" - AgentStatusSummoning = "summoning" + AgentStatusActive = "active" + AgentStatusInactive = "inactive" + AgentStatusSummoning = "summoning" AgentStatusSummonFailed = "summon_failed" ) diff --git a/internal/store/channel_instance_store.go b/internal/store/channel_instance_store.go index 50b975ab..358e8a47 100644 --- a/internal/store/channel_instance_store.go +++ b/internal/store/channel_instance_store.go @@ -15,7 +15,7 @@ type ChannelInstanceData struct { DisplayName string `json:"display_name"` ChannelType string `json:"channel_type"` AgentID uuid.UUID `json:"agent_id"` - Credentials []byte `json:"-"` // encrypted, never serialized to API + Credentials []byte `json:"-"` // encrypted, never serialized to API Config json.RawMessage `json:"config"` Enabled bool `json:"enabled"` CreatedBy string `json:"created_by"` diff --git a/internal/store/cron_store.go b/internal/store/cron_store.go index 5e008c69..470a8c90 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"` diff --git a/internal/store/custom_tool_store.go b/internal/store/custom_tool_store.go index eb054198..e50070c5 100644 --- a/internal/store/custom_tool_store.go +++ b/internal/store/custom_tool_store.go @@ -16,7 +16,7 @@ type CustomToolDef struct { Command string `json:"command"` WorkingDir string `json:"working_dir,omitempty"` TimeoutSeconds int `json:"timeout_seconds"` - Env []byte `json:"-"` // encrypted JSONB — never serialized to API + Env []byte `json:"-"` // encrypted JSONB — never serialized to API AgentID *uuid.UUID `json:"agent_id,omitempty"` Enabled bool `json:"enabled"` CreatedBy string `json:"created_by"` diff --git a/internal/store/pg/cron.go b/internal/store/pg/cron.go index f3e0d137..d7122dab 100644 --- a/internal/store/pg/cron.go +++ b/internal/store/pg/cron.go @@ -475,4 +475,3 @@ func (s *PGCronStore) RunJob(jobID string, force bool) (bool, string, error) { } return true, content, err } - diff --git a/internal/store/pg/factory.go b/internal/store/pg/factory.go index 42666b8a..443aec25 100644 --- a/internal/store/pg/factory.go +++ b/internal/store/pg/factory.go @@ -23,15 +23,15 @@ func NewPGStores(cfg store.StoreConfig) (*store.Stores, error) { skillsDir = config.ExpandHome(skillsDir) return &store.Stores{ - DB: db, - Sessions: NewPGSessionStore(db), - Memory: NewPGMemoryStore(db, memCfg), - Cron: NewPGCronStore(db), - Pairing: NewPGPairingStore(db), - Skills: NewPGSkillStore(db, skillsDir), - Agents: NewPGAgentStore(db), - Providers: NewPGProviderStore(db, cfg.EncryptionKey), - Tracing: NewPGTracingStore(db), + DB: db, + Sessions: NewPGSessionStore(db), + Memory: NewPGMemoryStore(db, memCfg), + Cron: NewPGCronStore(db), + Pairing: NewPGPairingStore(db), + Skills: NewPGSkillStore(db, skillsDir), + Agents: NewPGAgentStore(db), + Providers: NewPGProviderStore(db, cfg.EncryptionKey), + Tracing: NewPGTracingStore(db), MCP: NewPGMCPServerStore(db, cfg.EncryptionKey), CustomTools: NewPGCustomToolStore(db, cfg.EncryptionKey), ChannelInstances: NewPGChannelInstanceStore(db, cfg.EncryptionKey), diff --git a/internal/store/pg/memory_search.go b/internal/store/pg/memory_search.go index ca98427e..ecb78b6a 100644 --- a/internal/store/pg/memory_search.go +++ b/internal/store/pg/memory_search.go @@ -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/teams_messaging.go b/internal/store/pg/teams_messaging.go index 2bc8b88b..9255dc5d 100644 --- a/internal/store/pg/teams_messaging.go +++ b/internal/store/pg/teams_messaging.go @@ -117,4 +117,3 @@ func scanMessageRowsJoined(rows *sql.Rows) ([]store.TeamMessageData, error) { } return messages, rows.Err() } - diff --git a/internal/store/pg/tracing.go b/internal/store/pg/tracing.go index c4556d1f..74087011 100644 --- a/internal/store/pg/tracing.go +++ b/internal/store/pg/tracing.go @@ -356,4 +356,3 @@ func (s *PGTracingStore) BatchUpdateTraceAggregates(ctx context.Context, traceID WHERE id = $1`, traceID) return err } - diff --git a/internal/store/session_store.go b/internal/store/session_store.go index 460e2ee8..93b91490 100644 --- a/internal/store/session_store.go +++ b/internal/store/session_store.go @@ -18,15 +18,15 @@ type SessionData struct { AgentUUID uuid.UUID `json:"agentUUID,omitempty"` // DB agent UUID UserID string `json:"userID,omitempty"` // External user ID (e.g. Telegram user ID) - Model string `json:"model,omitempty"` - Provider string `json:"provider,omitempty"` - Channel string `json:"channel,omitempty"` - InputTokens int64 `json:"inputTokens,omitempty"` - OutputTokens int64 `json:"outputTokens,omitempty"` - CompactionCount int `json:"compactionCount,omitempty"` - MemoryFlushCompactionCount int `json:"memoryFlushCompactionCount,omitempty"` - MemoryFlushAt int64 `json:"memoryFlushAt,omitempty"` - Label string `json:"label,omitempty"` + Model string `json:"model,omitempty"` + Provider string `json:"provider,omitempty"` + Channel string `json:"channel,omitempty"` + InputTokens int64 `json:"inputTokens,omitempty"` + OutputTokens int64 `json:"outputTokens,omitempty"` + CompactionCount int `json:"compactionCount,omitempty"` + MemoryFlushCompactionCount int `json:"memoryFlushCompactionCount,omitempty"` + MemoryFlushAt int64 `json:"memoryFlushAt,omitempty"` + Label string `json:"label,omitempty"` SpawnedBy string `json:"spawnedBy,omitempty"` SpawnDepth int `json:"spawnDepth,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` diff --git a/internal/store/stores.go b/internal/store/stores.go index 425d18ac..c708001f 100644 --- a/internal/store/stores.go +++ b/internal/store/stores.go @@ -4,15 +4,15 @@ import "database/sql" // Stores is the top-level container for all storage backends. type Stores struct { - DB *sql.DB // underlying connection - Sessions SessionStore - Memory MemoryStore - Cron CronStore - Pairing PairingStore - Skills SkillStore - Agents AgentStore - Providers ProviderStore - Tracing TracingStore + DB *sql.DB // underlying connection + Sessions SessionStore + Memory MemoryStore + Cron CronStore + Pairing PairingStore + Skills SkillStore + Agents AgentStore + Providers ProviderStore + Tracing TracingStore MCP MCPServerStore CustomTools CustomToolStore ChannelInstances ChannelInstanceStore diff --git a/internal/store/tracing_store.go b/internal/store/tracing_store.go index 0747dac3..b215f247 100644 --- a/internal/store/tracing_store.go +++ b/internal/store/tracing_store.go @@ -94,12 +94,12 @@ type SpanData struct { // TraceListOpts configures trace listing. type TraceListOpts struct { - AgentID *uuid.UUID - UserID string - SessionKey string - Status string - Limit int - Offset int + AgentID *uuid.UUID + UserID string + SessionKey string + Status string + Limit int + Offset int } // TracingStore manages LLM traces and spans. diff --git a/internal/tools/announce_queue.go b/internal/tools/announce_queue.go index 240cb724..d714e708 100644 --- a/internal/tools/announce_queue.go +++ b/internal/tools/announce_queue.go @@ -137,9 +137,10 @@ func FormatBatchedAnnounce(items []AnnounceQueueItem, remainingActive int) strin // Single item: use the same format as before (no batching overhead) item := items[0] statusLabel := "completed successfully" - if item.Status == "failed" { + switch item.Status { + case "failed": statusLabel = "failed: " + item.Result - } else if item.Status == "cancelled" { + case "cancelled": statusLabel = "was cancelled" } @@ -162,9 +163,10 @@ func FormatBatchedAnnounce(items []AnnounceQueueItem, remainingActive int) strin for i, item := range items { statusLabel := "completed" - if item.Status == "failed" { + switch item.Status { + case "failed": statusLabel = "failed" - } else if item.Status == "cancelled" { + case "cancelled": statusLabel = "cancelled" } diff --git a/internal/tools/boundary_test.go b/internal/tools/boundary_test.go index 5c801537..fa7302bb 100644 --- a/internal/tools/boundary_test.go +++ b/internal/tools/boundary_test.go @@ -269,7 +269,7 @@ func TestIsPathInside(t *testing.T) { }{ {"/a/b/c", "/a/b", true}, {"/a/b", "/a/b", true}, - {"/a/bc", "/a/b", false}, // not a child, just prefix match + {"/a/bc", "/a/b", false}, // not a child, just prefix match {"/a", "/a/b", false}, {"/x/y", "/a/b", false}, } diff --git a/internal/tools/context_file_interceptor.go b/internal/tools/context_file_interceptor.go index f47a97ee..0561f3c8 100644 --- a/internal/tools/context_file_interceptor.go +++ b/internal/tools/context_file_interceptor.go @@ -73,7 +73,7 @@ const defaultContextCacheTTL = 5 * time.Minute // Routes based on agent type: "open" → all per-user, "predefined" → only USER.md per-user. type ContextFileInterceptor struct { agentStore store.AgentStore - workspace string // workspace root for matching absolute paths + workspace string // workspace root for matching absolute paths agentCache cache.Cache[[]store.AgentContextFileData] // agent-level files, keyed by agentID.String() userCache cache.Cache[[]store.AgentContextFileData] // user-level files, keyed by "agentID:userID" ttl time.Duration diff --git a/internal/tools/context_file_interceptor_test.go b/internal/tools/context_file_interceptor_test.go index f2ea9171..4b40498f 100644 --- a/internal/tools/context_file_interceptor_test.go +++ b/internal/tools/context_file_interceptor_test.go @@ -42,13 +42,19 @@ func (s *stubAgentStore) DeleteUserContextFile(_ context.Context, _ uuid.UUID, _ } // Remaining interface methods — not exercised in these tests. -func (s *stubAgentStore) Create(_ context.Context, _ *store.AgentData) error { return nil } -func (s *stubAgentStore) GetByKey(_ context.Context, _ string) (*store.AgentData, error) { return nil, nil } -func (s *stubAgentStore) GetByID(_ context.Context, _ uuid.UUID) (*store.AgentData, error) { return nil, nil } -func (s *stubAgentStore) GetDefault(_ context.Context) (*store.AgentData, error) { return nil, nil } -func (s *stubAgentStore) Update(_ context.Context, _ uuid.UUID, _ map[string]any) error { return nil } -func (s *stubAgentStore) Delete(_ context.Context, _ uuid.UUID) error { return nil } -func (s *stubAgentStore) List(_ context.Context, _ string) ([]store.AgentData, error) { return nil, nil } +func (s *stubAgentStore) Create(_ context.Context, _ *store.AgentData) error { return nil } +func (s *stubAgentStore) GetByKey(_ context.Context, _ string) (*store.AgentData, error) { + return nil, nil +} +func (s *stubAgentStore) GetByID(_ context.Context, _ uuid.UUID) (*store.AgentData, error) { + return nil, nil +} +func (s *stubAgentStore) GetDefault(_ context.Context) (*store.AgentData, error) { return nil, nil } +func (s *stubAgentStore) Update(_ context.Context, _ uuid.UUID, _ map[string]any) error { return nil } +func (s *stubAgentStore) Delete(_ context.Context, _ uuid.UUID) error { return nil } +func (s *stubAgentStore) List(_ context.Context, _ string) ([]store.AgentData, error) { + return nil, nil +} func (s *stubAgentStore) ShareAgent(_ context.Context, _ uuid.UUID, _, _, _ string) error { return nil } func (s *stubAgentStore) RevokeShare(_ context.Context, _ uuid.UUID, _ string) error { return nil } func (s *stubAgentStore) ListShares(_ context.Context, _ uuid.UUID) ([]store.AgentShareData, error) { diff --git a/internal/tools/create_audio.go b/internal/tools/create_audio.go index 56380508..f1049596 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, } } diff --git a/internal/tools/cron.go b/internal/tools/cron.go index 9b309320..52811f5e 100644 --- a/internal/tools/cron.go +++ b/internal/tools/cron.go @@ -82,8 +82,8 @@ func (t *CronTool) Parameters() map[string]interface{} { "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)", + "type": "object", + "description": "Job definition for add action (name, schedule, message, deliver, channel, to, agentId, deleteAfterRun)", "additionalProperties": true, }, "jobId": map[string]interface{}{ @@ -95,8 +95,8 @@ func (t *CronTool) Parameters() map[string]interface{} { "description": "Backward compatibility alias for jobId", }, "patch": map[string]interface{}{ - "type": "object", - "description": "Patch object for update action", + "type": "object", + "description": "Patch object for update action", "additionalProperties": true, }, "runMode": map[string]interface{}{ diff --git a/internal/tools/delegate.go b/internal/tools/delegate.go index eb2db85a..2800b30c 100644 --- a/internal/tools/delegate.go +++ b/internal/tools/delegate.go @@ -17,20 +17,20 @@ const defaultProgressInterval = 30 * time.Second // DelegationTask tracks an active delegation for concurrency control and cancellation. type DelegationTask struct { - ID string `json:"id"` - SourceAgentID uuid.UUID `json:"source_agent_id"` - SourceAgentKey string `json:"source_agent_key"` - TargetAgentID uuid.UUID `json:"target_agent_id"` - SourceDisplayName string `json:"-"` - TargetAgentKey string `json:"target_agent_key"` - TargetDisplayName string `json:"-"` - UserID string `json:"user_id"` - Task string `json:"task"` - Status string `json:"status"` // "running", "completed", "failed", "cancelled" - Mode string `json:"mode"` // "sync" or "async" - SessionKey string `json:"session_key"` - CreatedAt time.Time `json:"created_at"` - CompletedAt *time.Time `json:"completed_at,omitempty"` + ID string `json:"id"` + SourceAgentID uuid.UUID `json:"source_agent_id"` + SourceAgentKey string `json:"source_agent_key"` + TargetAgentID uuid.UUID `json:"target_agent_id"` + SourceDisplayName string `json:"-"` + TargetAgentKey string `json:"target_agent_key"` + TargetDisplayName string `json:"-"` + UserID string `json:"user_id"` + Task string `json:"task"` + Status string `json:"status"` // "running", "completed", "failed", "cancelled" + Mode string `json:"mode"` // "sync" or "async" + SessionKey string `json:"session_key"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at,omitempty"` // Origin metadata for async announce routing OriginChannel string `json:"-"` @@ -114,7 +114,7 @@ type DelegateArtifacts struct { // included in the final announce so the lead has all results in one message. type DelegateResultSummary struct { AgentKey string - DisplayName string // target agent display name + DisplayName string // target agent display name Content string HasMedia bool Deliverables []string // actual content from tool outputs @@ -128,8 +128,8 @@ type AgentRunFunc func(ctx context.Context, agentKey string, req DelegateRunRequ type DelegateResult struct { Content string Iterations int - DelegationID string // for async: the delegation ID to track/cancel - TeamTaskID string // auto-created or provided team task ID (for tracing) + DelegationID string // for async: the delegation ID to track/cancel + TeamTaskID string // auto-created or provided team task ID (for tracing) Media []bus.MediaFile // media files from delegation result } @@ -153,11 +153,11 @@ type DelegateManager struct { runAgent AgentRunFunc linkStore store.AgentLinkStore agentStore store.AgentStore - teamStore store.TeamStore // optional: enables auto-complete of team tasks - sessionStore store.SessionStore // optional: enables session cleanup + teamStore store.TeamStore // optional: enables auto-complete of team tasks + sessionStore store.SessionStore // optional: enables session cleanup mediaLoader MediaPathLoader // optional: enables image propagation to delegates - msgBus *bus.MessageBus // for event broadcast + async announce (PublishInbound) - hookEngine *hooks.Engine // optional: quality gate evaluation + msgBus *bus.MessageBus // for event broadcast + async announce (PublishInbound) + hookEngine *hooks.Engine // optional: quality gate evaluation active sync.Map // delegationID → *DelegationTask pendingArtifacts sync.Map // sourceAgentID string → *DelegateArtifacts @@ -206,4 +206,3 @@ func (dm *DelegateManager) SetMediaLoader(ml MediaPathLoader) { func (dm *DelegateManager) SetProgressEnabled(enabled bool) { dm.progressEnabled = enabled } - diff --git a/internal/tools/delegate_async.go b/internal/tools/delegate_async.go index db82fae4..ceec684b 100644 --- a/internal/tools/delegate_async.go +++ b/internal/tools/delegate_async.go @@ -123,8 +123,18 @@ func (dm *DelegateManager) DelegateAsync(ctx context.Context, opts DelegateOpts) 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 "" }(), + 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 "" + }(), SiblingsRemaining: siblingCount, ElapsedMS: int(time.Since(task.CreatedAt).Milliseconds()), }, @@ -192,13 +202,18 @@ func (dm *DelegateManager) DelegateAsync(ctx context.Context, opts DelegateOpts) SourceAgentKey: task.SourceAgentKey, SourceDisplayName: task.SourceDisplayName, UserID: task.UserID, - Channel: task.OriginChannel, - ChatID: task.OriginChatID, - TeamID: func() string { if task.TeamID != uuid.Nil { return task.TeamID.String() }; return "" }(), - Results: announceSummaries, + Channel: task.OriginChannel, + ChatID: task.OriginChatID, + TeamID: func() string { + if task.TeamID != uuid.Nil { + return task.TeamID.String() + } + return "" + }(), + Results: announceSummaries, CompletedTaskIDs: artifacts.CompletedTaskIDs, - TotalElapsedMS: int(elapsed.Milliseconds()), - HasMedia: hasMedia, + TotalElapsedMS: int(elapsed.Milliseconds()), + HasMedia: hasMedia, }, }) diff --git a/internal/tools/delegate_policy.go b/internal/tools/delegate_policy.go index 4c5eba86..4d44679d 100644 --- a/internal/tools/delegate_policy.go +++ b/internal/tools/delegate_policy.go @@ -233,12 +233,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_prep.go b/internal/tools/delegate_prep.go index a60b3338..3fba4e8b 100644 --- a/internal/tools/delegate_prep.go +++ b/internal/tools/delegate_prep.go @@ -156,17 +156,17 @@ func (dm *DelegateManager) prepareDelegation(ctx context.Context, opts DelegateO delegationID := uuid.NewString()[:12] task := &DelegationTask{ - ID: delegationID, - SourceAgentID: sourceAgentID, + ID: delegationID, + SourceAgentID: sourceAgentID, SourceAgentKey: sourceAgent.AgentKey, SourceDisplayName: sourceAgent.DisplayName, - TargetAgentID: targetAgent.ID, + TargetAgentID: targetAgent.ID, TargetAgentKey: opts.TargetAgentKey, TargetDisplayName: targetAgent.DisplayName, - UserID: userID, - Task: opts.Task, - Status: "running", - Mode: mode, + UserID: userID, + Task: opts.Task, + Status: "running", + Mode: mode, SessionKey: fmt.Sprintf("delegate:%s:%s:%s", sourceAgentID.String()[:8], opts.TargetAgentKey, delegationID), CreatedAt: time.Now(), @@ -305,8 +305,13 @@ func (dm *DelegateManager) sendProgressNotification(task *DelegationTask) { UserID: task.UserID, Channel: task.OriginChannel, ChatID: task.OriginChatID, - TeamID: func() string { if task.TeamID != uuid.Nil { return task.TeamID.String() }; return "" }(), - Active: progressItems, + TeamID: func() string { + if task.TeamID != uuid.Nil { + return task.TeamID.String() + } + return "" + }(), + Active: progressItems, }, }) } @@ -335,9 +340,19 @@ func (dm *DelegateManager) buildRunRequest(task *DelegationTask, message string) "- Do NOT use your persona name or self-references (e.g. do not say your name). Write factual, neutral content.\n" + "- Be concise and deliver actionable results.\n" + "- IMPORTANT: If the delegated task falls outside your expertise scope (as defined in your SOUL.md), politely refuse and explain that this task is not within your domain. Do NOT attempt tasks outside your scope.", - DelegationID: task.ID, - 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 "" }(), + DelegationID: task.ID, + 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 "" + }(), ParentAgentID: task.SourceAgentKey, } diff --git a/internal/tools/dynamic_tool.go b/internal/tools/dynamic_tool.go index 8e14741a..8057e5a0 100644 --- a/internal/tools/dynamic_tool.go +++ b/internal/tools/dynamic_tool.go @@ -35,9 +35,9 @@ func NewDynamicTool(def store.CustomToolDef, workspace string) *DynamicTool { 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]interface{} { return t.params } func (t *DynamicTool) Execute(ctx context.Context, args map[string]interface{}) *Result { // Render command template with shell-escaped args diff --git a/internal/tools/exec_approval.go b/internal/tools/exec_approval.go index 646e1ca3..fd15fbfc 100644 --- a/internal/tools/exec_approval.go +++ b/internal/tools/exec_approval.go @@ -96,11 +96,11 @@ type PendingApproval struct { // ExecApprovalManager manages pending approval requests and the dynamic allowlist. type ExecApprovalManager struct { - config ExecApprovalConfig - pending map[string]*PendingApproval - alwaysAllow map[string]bool // patterns added via "allow-always" decisions - mu sync.Mutex - nextID int + config ExecApprovalConfig + pending map[string]*PendingApproval + alwaysAllow map[string]bool // patterns added via "allow-always" decisions + mu sync.Mutex + nextID int } // NewExecApprovalManager creates an approval manager with the given config. diff --git a/internal/tools/filesystem.go b/internal/tools/filesystem.go index fc6cd5bf..20cb43ef 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. diff --git a/internal/tools/filesystem_write.go b/internal/tools/filesystem_write.go index ce70c83f..8575b1c1 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,8 +53,10 @@ 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) 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{}{ "type": "object", diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index 1725a22b..0e02e0d6 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -19,9 +19,9 @@ type MediaProviderEntry struct { Provider string `json:"provider"` // name for registry.Get() Model string `json:"model"` Enabled bool `json:"enabled"` - Timeout int `json:"timeout"` // seconds, default 120 - MaxRetries int `json:"max_retries"` // default 2 - Params map[string]any `json:"params,omitempty"` // provider-specific config + Timeout int `json:"timeout"` // seconds, default 120 + MaxRetries int `json:"max_retries"` // default 2 + Params map[string]any `json:"params,omitempty"` // provider-specific config } // mediaProviderChain is the settings JSON structure for media tools. diff --git a/internal/tools/message.go b/internal/tools/message.go index 94455bc4..f10349f4 100644 --- a/internal/tools/message.go +++ b/internal/tools/message.go @@ -21,7 +21,7 @@ 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 { diff --git a/internal/tools/message_test.go b/internal/tools/message_test.go index 91506eb7..b843b03f 100644 --- a/internal/tools/message_test.go +++ b/internal/tools/message_test.go @@ -10,10 +10,10 @@ func TestParseMediaPath(t *testing.T) { tmpDir := os.TempDir() tests := []struct { - name string - input string - want string - wantOK bool + name string + input string + want string + wantOK bool }{ {"valid temp file", "MEDIA:" + filepath.Join(tmpDir, "test.png"), filepath.Join(tmpDir, "test.png"), true}, {"valid nested temp file", "MEDIA:" + filepath.Join(tmpDir, "sub", "file.txt"), filepath.Join(tmpDir, "sub", "file.txt"), true}, diff --git a/internal/tools/rate_limiter.go b/internal/tools/rate_limiter.go index 0ef995ae..2167a7e4 100644 --- a/internal/tools/rate_limiter.go +++ b/internal/tools/rate_limiter.go @@ -9,10 +9,10 @@ import ( // ToolRateLimiter implements a sliding window rate limiter for tool executions. // Tracks actions per key (typically agent:userID) within a configurable window. type ToolRateLimiter struct { - mu sync.Mutex - windows map[string][]time.Time - maxPerHr int - window time.Duration + mu sync.Mutex + windows map[string][]time.Time + maxPerHr int + window time.Duration } // NewToolRateLimiter creates a rate limiter with the given max actions per hour. diff --git a/internal/tools/read_document_gemini.go b/internal/tools/read_document_gemini.go index ee554379..4aec67f3 100644 --- a/internal/tools/read_document_gemini.go +++ b/internal/tools/read_document_gemini.go @@ -27,7 +27,7 @@ func geminiNativeDocumentCall(ctx context.Context, apiKey, model, prompt string, { "inline_data": map[string]interface{}{ "mime_type": docMime, - "data": b64, + "data": b64, }, }, { @@ -117,4 +117,3 @@ func geminiNativeDocumentCall(ctx context.Context, apiKey, model, prompt string, }, }, nil } - diff --git a/internal/tools/result.go b/internal/tools/result.go index 08d6564d..cbddcd24 100644 --- a/internal/tools/result.go +++ b/internal/tools/result.go @@ -8,11 +8,11 @@ import ( // Result is the unified return type from tool execution. type Result struct { ForLLM string `json:"for_llm"` // content sent to the LLM - ForUser string `json:"for_user,omitempty"` // content shown to the user - Silent bool `json:"silent"` // suppress user message - IsError bool `json:"is_error"` // marks error - Async bool `json:"async"` // running asynchronously - Err error `json:"-"` // internal error (not serialized) + ForUser string `json:"for_user,omitempty"` // content shown to the user + Silent bool `json:"silent"` // suppress user message + IsError bool `json:"is_error"` // marks error + Async bool `json:"async"` // running asynchronously + Err error `json:"-"` // internal error (not serialized) // Media holds media files to forward as output (e.g. images from delegation). Media []bus.MediaFile `json:"-"` diff --git a/internal/tools/scrub_test.go b/internal/tools/scrub_test.go index 3925515f..a3fd7ff5 100644 --- a/internal/tools/scrub_test.go +++ b/internal/tools/scrub_test.go @@ -72,10 +72,10 @@ func TestScrubCredentials_GenericKeyValue(t *testing.T) { func TestScrubCredentials_NoFalsePositive(t *testing.T) { inputs := []string{ "hello world", - "sk-short", // too short for OpenAI pattern - "ghp_tooshort", // too short for GitHub pattern + "sk-short", // too short for OpenAI pattern + "ghp_tooshort", // too short for GitHub pattern "normal text with no secrets", - "AKIA1234", // too short for AWS (needs 16 chars after AKIA) + "AKIA1234", // too short for AWS (needs 16 chars after AKIA) } for _, input := range inputs { got := ScrubCredentials(input) diff --git a/internal/tools/shell.go b/internal/tools/shell.go index d563aa8b..cb3c9b16 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. diff --git a/internal/tools/subagent.go b/internal/tools/subagent.go index 562326a5..e8145958 100644 --- a/internal/tools/subagent.go +++ b/internal/tools/subagent.go @@ -43,25 +43,25 @@ const ( // SubagentTask tracks a running or completed subagent. type SubagentTask struct { - ID string `json:"id"` - ParentID string `json:"parentId"` - Task string `json:"task"` - Label string `json:"label"` - Status string `json:"status"` // "running", "completed", "failed", "cancelled" - Result string `json:"result,omitempty"` - Depth int `json:"depth"` - Model string `json:"model,omitempty"` // model override for this subagent - OriginChannel string `json:"originChannel,omitempty"` - OriginChatID string `json:"originChatId,omitempty"` - OriginPeerKind string `json:"originPeerKind,omitempty"` // "direct" or "group" (for session key building) - OriginLocalKey string `json:"originLocalKey,omitempty"` // composite key with topic/thread suffix for routing - OriginUserID string `json:"originUserId,omitempty"` // parent's userID for per-user scoping propagation - OriginSessionKey string `json:"originSessionKey,omitempty"` // exact parent session key for announce routing (WS uses non-standard format) - CreatedAt int64 `json:"createdAt"` - CompletedAt int64 `json:"completedAt,omitempty"` - Media []bus.MediaFile `json:"-"` // media files from tool results - OriginTraceID uuid.UUID `json:"-"` // parent trace for announce linking - OriginRootSpanID uuid.UUID `json:"-"` // parent agent's root span ID + ID string `json:"id"` + ParentID string `json:"parentId"` + Task string `json:"task"` + Label string `json:"label"` + Status string `json:"status"` // "running", "completed", "failed", "cancelled" + Result string `json:"result,omitempty"` + Depth int `json:"depth"` + Model string `json:"model,omitempty"` // model override for this subagent + OriginChannel string `json:"originChannel,omitempty"` + OriginChatID string `json:"originChatId,omitempty"` + OriginPeerKind string `json:"originPeerKind,omitempty"` // "direct" or "group" (for session key building) + OriginLocalKey string `json:"originLocalKey,omitempty"` // composite key with topic/thread suffix for routing + OriginUserID string `json:"originUserId,omitempty"` // parent's userID for per-user scoping propagation + OriginSessionKey string `json:"originSessionKey,omitempty"` // exact parent session key for announce routing (WS uses non-standard format) + CreatedAt int64 `json:"createdAt"` + CompletedAt int64 `json:"completedAt,omitempty"` + Media []bus.MediaFile `json:"-"` // media files from tool results + OriginTraceID uuid.UUID `json:"-"` // parent trace for announce linking + OriginRootSpanID uuid.UUID `json:"-"` // parent agent's root span ID cancelFunc context.CancelFunc `json:"-"` // per-task context cancel } @@ -195,12 +195,12 @@ func (sm *SubagentManager) Spawn( OriginChannel: channel, OriginChatID: chatID, OriginPeerKind: peerKind, - OriginLocalKey: ToolLocalKeyFromCtx(ctx), - OriginUserID: store.UserIDFromContext(ctx), - OriginSessionKey: ToolSessionKeyFromCtx(ctx), - OriginTraceID: tracing.TraceIDFromContext(ctx), - OriginRootSpanID: tracing.ParentSpanIDFromContext(ctx), - CreatedAt: time.Now().UnixMilli(), + OriginLocalKey: ToolLocalKeyFromCtx(ctx), + OriginUserID: store.UserIDFromContext(ctx), + OriginSessionKey: ToolSessionKeyFromCtx(ctx), + OriginTraceID: tracing.TraceIDFromContext(ctx), + OriginRootSpanID: tracing.ParentSpanIDFromContext(ctx), + CreatedAt: time.Now().UnixMilli(), } // Create per-task context for real goroutine cancellation taskCtx, taskCancel := context.WithCancel(ctx) diff --git a/internal/tools/team_message_tool.go b/internal/tools/team_message_tool.go index fb6d7f42..445f46e1 100644 --- a/internal/tools/team_message_tool.go +++ b/internal/tools/team_message_tool.go @@ -185,7 +185,7 @@ func (t *TeamMessageTool) executeBroadcast(ctx context.Context, args map[string] ChatID: ToolChatIDFromCtx(ctx), }) - return NewResult(fmt.Sprintf("Broadcast sent to all teammates.")) + return NewResult("Broadcast sent to all teammates.") } func (t *TeamMessageTool) executeRead(ctx context.Context) *Result { diff --git a/internal/tools/web_fetch_convert.go b/internal/tools/web_fetch_convert.go index 0b3a4501..799d7fb1 100644 --- a/internal/tools/web_fetch_convert.go +++ b/internal/tools/web_fetch_convert.go @@ -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_test.go b/internal/tools/web_fetch_convert_test.go index 42da43af..969c0c24 100644 --- a/internal/tools/web_fetch_convert_test.go +++ b/internal/tools/web_fetch_convert_test.go @@ -346,8 +346,8 @@ window.__INITIAL_STATE__ = {"user":null,"theme":"dark"}; "analytics loaded", "console.log", "main.abc123.css", "main.def456.js", "enable JavaScript", - "viewport", // meta from head - "All rights", // footer + "viewport", // meta from head + "All rights", // footer "Home", "About", "Contact", // nav } { if strings.Contains(got, bad) { diff --git a/internal/tools/web_search.go b/internal/tools/web_search.go index 8e391ebf..d14b7903 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 { diff --git a/internal/tools/web_shared.go b/internal/tools/web_shared.go index c524707d..e5559515 100644 --- a/internal/tools/web_shared.go +++ b/internal/tools/web_shared.go @@ -125,13 +125,13 @@ func isPrivateIP(ipStr string) bool { network string mask int }{ - {"0.0.0.0", 8}, // current network - {"10.0.0.0", 8}, // private - {"127.0.0.0", 8}, // loopback - {"169.254.0.0", 16}, // link-local - {"172.16.0.0", 12}, // private - {"192.168.0.0", 16}, // private - {"100.64.0.0", 10}, // carrier-grade NAT + {"0.0.0.0", 8}, // current network + {"10.0.0.0", 8}, // private + {"127.0.0.0", 8}, // loopback + {"169.254.0.0", 16}, // link-local + {"172.16.0.0", 12}, // private + {"192.168.0.0", 16}, // private + {"100.64.0.0", 10}, // carrier-grade NAT } for _, r := range privateRanges { @@ -143,11 +143,11 @@ func isPrivateIP(ipStr string) bool { // IPv6 private ranges ipv6Ranges := []string{ - "::0/128", // unspecified - "::1/128", // loopback - "fe80::/10", // link-local - "fec0::/10", // site-local (deprecated) - "fc00::/7", // unique local + "::0/128", // unspecified + "::1/128", // loopback + "fe80::/10", // link-local + "fec0::/10", // site-local (deprecated) + "fc00::/7", // unique local } for _, cidrStr := range ipv6Ranges { _, cidr, _ := net.ParseCIDR(cidrStr) diff --git a/internal/tracing/context.go b/internal/tracing/context.go index 5b696f8c..033baf0d 100644 --- a/internal/tracing/context.go +++ b/internal/tracing/context.go @@ -9,11 +9,11 @@ import ( type contextKey string const ( - traceIDKey contextKey = "goclaw_trace_id" - parentSpanKey contextKey = "goclaw_parent_span_id" - collectorKey contextKey = "goclaw_trace_collector" - announceParentKey contextKey = "goclaw_announce_parent_span_id" - delegateParentTraceKey contextKey = "goclaw_delegate_parent_trace_id" + traceIDKey contextKey = "goclaw_trace_id" + parentSpanKey contextKey = "goclaw_parent_span_id" + collectorKey contextKey = "goclaw_trace_collector" + announceParentKey contextKey = "goclaw_announce_parent_span_id" + delegateParentTraceKey contextKey = "goclaw_delegate_parent_trace_id" ) // WithTraceID returns a context with the given trace ID. diff --git a/internal/tts/elevenlabs.go b/internal/tts/elevenlabs.go index f5db5b7d..0fae1941 100644 --- a/internal/tts/elevenlabs.go +++ b/internal/tts/elevenlabs.go @@ -81,9 +81,9 @@ func (p *ElevenLabsProvider) Synthesize(ctx context.Context, text string, opts O "text": text, "model_id": modelID, "voice_settings": map[string]interface{}{ - "stability": 0.5, - "similarity_boost": 0.75, - "style": 0.0, + "stability": 0.5, + "similarity_boost": 0.75, + "style": 0.0, "use_speaker_boost": true, }, } diff --git a/internal/tts/types.go b/internal/tts/types.go index a7b75a02..5ae517e2 100644 --- a/internal/tts/types.go +++ b/internal/tts/types.go @@ -32,10 +32,10 @@ type SynthResult struct { type AutoMode string const ( - AutoOff AutoMode = "off" // Disabled - AutoAlways AutoMode = "always" // Apply to all eligible replies + AutoOff AutoMode = "off" // Disabled + AutoAlways AutoMode = "always" // Apply to all eligible replies AutoInbound AutoMode = "inbound" // Only if user sent audio/voice - AutoTagged AutoMode = "tagged" // Only if reply contains [[tts]] directive + AutoTagged AutoMode = "tagged" // Only if reply contains [[tts]] directive ) // Mode controls which reply types get TTS. diff --git a/pkg/browser/actions.go b/pkg/browser/actions.go index 83c27c5b..356942fa 100644 --- a/pkg/browser/actions.go +++ b/pkg/browser/actions.go @@ -18,9 +18,10 @@ func (m *Manager) Click(ctx context.Context, targetID, ref string, opts ClickOpt } button := proto.InputMouseButtonLeft - if opts.Button == "right" { + switch opts.Button { + case "right": button = proto.InputMouseButtonRight - } else if opts.Button == "middle" { + case "middle": button = proto.InputMouseButtonMiddle } @@ -106,7 +107,7 @@ func (m *Manager) Wait(ctx context.Context, targetID string, opts WaitOpts) erro // Wait for text to appear if opts.Text != "" { return rod.Try(func() { - page.Timeout(30 * time.Second).MustElementR("*", opts.Text) + page.Timeout(30*time.Second).MustElementR("*", opts.Text) }) } diff --git a/pkg/browser/snapshot.go b/pkg/browser/snapshot.go index 4f2ca0a3..15aac978 100644 --- a/pkg/browser/snapshot.go +++ b/pkg/browser/snapshot.go @@ -28,8 +28,8 @@ func axValue(v *proto.AccessibilityAXValue) string { // axNodeTree is an internal tree node built from flat AX nodes. type axNodeTree struct { - node *proto.AccessibilityAXNode - depth int + node *proto.AccessibilityAXNode + depth int } // buildAXTree converts flat CDP AX nodes into a tree structure. diff --git a/pkg/browser/types.go b/pkg/browser/types.go index d93944d4..607ac446 100644 --- a/pkg/browser/types.go +++ b/pkg/browser/types.go @@ -84,11 +84,11 @@ type WaitOpts struct { // ConsoleMessage is a captured browser console message. type ConsoleMessage struct { - Level string `json:"level"` // "log", "warn", "error", "info" - Text string `json:"text"` - URL string `json:"url,omitempty"` - LineNo int `json:"lineNo,omitempty"` - ColNo int `json:"colNo,omitempty"` + Level string `json:"level"` // "log", "warn", "error", "info" + Text string `json:"text"` + URL string `json:"url,omitempty"` + LineNo int `json:"lineNo,omitempty"` + ColNo int `json:"colNo,omitempty"` } // StatusInfo describes the current browser state. diff --git a/pkg/protocol/events.go b/pkg/protocol/events.go index ed1710c1..a8e8dac6 100644 --- a/pkg/protocol/events.go +++ b/pkg/protocol/events.go @@ -2,22 +2,22 @@ package protocol // WebSocket event names pushed from server to client. const ( - EventAgent = "agent" - EventChat = "chat" - EventHealth = "health" - EventCron = "cron" - EventExecApprovalReq = "exec.approval.requested" - EventExecApprovalRes = "exec.approval.resolved" - EventPresence = "presence" - EventTick = "tick" - EventShutdown = "shutdown" - EventNodePairRequested = "node.pair.requested" - EventNodePairResolved = "node.pair.resolved" - EventDevicePairReq = "device.pair.requested" - EventDevicePairRes = "device.pair.resolved" - EventVoicewakeChanged = "voicewake.changed" - EventConnectChallenge = "connect.challenge" - EventTalkMode = "talk.mode" + EventAgent = "agent" + EventChat = "chat" + EventHealth = "health" + EventCron = "cron" + EventExecApprovalReq = "exec.approval.requested" + EventExecApprovalRes = "exec.approval.resolved" + EventPresence = "presence" + EventTick = "tick" + EventShutdown = "shutdown" + EventNodePairRequested = "node.pair.requested" + EventNodePairResolved = "node.pair.resolved" + EventDevicePairReq = "device.pair.requested" + EventDevicePairRes = "device.pair.resolved" + EventVoicewakeChanged = "voicewake.changed" + EventConnectChallenge = "connect.challenge" + EventTalkMode = "talk.mode" // Agent summoning events (predefined agent setup via LLM). EventAgentSummoning = "agent.summoning" @@ -80,7 +80,7 @@ const ( // Chat event subtypes (in payload.type) const ( - ChatEventChunk = "chunk" - ChatEventMessage = "message" - ChatEventThinking = "thinking" + ChatEventChunk = "chunk" + ChatEventMessage = "message" + ChatEventThinking = "thinking" ) diff --git a/pkg/protocol/frames.go b/pkg/protocol/frames.go index a64c80b7..4502738a 100644 --- a/pkg/protocol/frames.go +++ b/pkg/protocol/frames.go @@ -30,9 +30,9 @@ 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 + 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) Error *ErrorShape `json:"error,omitempty"` // error info (when ok=false) } diff --git a/pkg/protocol/methods.go b/pkg/protocol/methods.go index dc62406e..88196b93 100644 --- a/pkg/protocol/methods.go +++ b/pkg/protocol/methods.go @@ -46,8 +46,8 @@ const ( // Phase 2 - NEEDED methods const ( - MethodSkillsList = "skills.list" - MethodSkillsGet = "skills.get" + MethodSkillsList = "skills.list" + MethodSkillsGet = "skills.get" MethodSkillsUpdate = "skills.update" MethodCronList = "cron.list" @@ -102,10 +102,10 @@ const ( // Agent teams const ( - MethodTeamsList = "teams.list" - MethodTeamsCreate = "teams.create" - MethodTeamsGet = "teams.get" - MethodTeamsDelete = "teams.delete" + MethodTeamsList = "teams.list" + MethodTeamsCreate = "teams.create" + MethodTeamsGet = "teams.get" + MethodTeamsDelete = "teams.delete" MethodTeamsTaskList = "teams.tasks.list" MethodTeamsMembersAdd = "teams.members.add" MethodTeamsMembersRemove = "teams.members.remove" @@ -135,6 +135,6 @@ const ( MethodBrowserScreenshot = "browser.screenshot" // Zalo Personal - MethodZaloPersonalQRStart = "zalo.personal.qr.start" - MethodZaloPersonalContacts = "zalo.personal.contacts" + MethodZaloPersonalQRStart = "zalo.personal.qr.start" + MethodZaloPersonalContacts = "zalo.personal.contacts" )