chore: upgrade Go 1.25 → 1.26 and apply go fix modernizations

- Update go.mod and Dockerfile to Go 1.26
- Apply `go fix ./...` stdlib modernizations across 170+ files
- Add `go fix` to post-implementation checklist in CLAUDE.md
- Fix go fix misapplied rewrite in loop_history.go
This commit is contained in:
viettranx committed 2026-03-10 00:09:15 +07:00
1 parent e593b9cf22
commit bdb60de7ae
178 files changed
+1475 -1541

No files matched your search

+1
View File
@@ -69,6 +69,7 @@ cd ui/web && pnpm install && pnpm dev # Web dashboard (dev)
After implementing or modifying Go code, run these checks:
```bash
go fix ./... # Apply Go version upgrades (run before commit)
go build ./... # Compile check
go vet ./... # Static analysis
go test -race ./tests/integration/ # Integration tests with race detector
+1 -1
View File
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# ── Stage 1: Build ──
FROM golang:1.25-bookworm AS builder
FROM golang:1.26-bookworm AS builder
WORKDIR /src
+5 -5
View File
@@ -115,7 +115,7 @@ func wsConnect(conn *websocket.Conn, token string) error {
// displaying events (tool calls, chunks) in real-time.
func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (string, error) {
reqID := uuid.NewString()[:8]
params, _ := json.Marshal(map[string]interface{}{
params, _ := json.Marshal(map[string]any{
"message": message,
"agentId": agentID,
"sessionKey": sessionKey,
@@ -159,7 +159,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri
return "", fmt.Errorf("agent error (unknown)")
}
// Extract content from payload
if payload, ok := resp.Payload.(map[string]interface{}); ok {
if payload, ok := resp.Payload.(map[string]any); ok {
if content, ok := payload["content"].(string); ok && content != "" {
finalContent = content
}
@@ -178,7 +178,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri
// handleCLIEvent displays agent events in the terminal.
func handleCLIEvent(evt protocol.EventFrame) {
payload, ok := evt.Payload.(map[string]interface{})
payload, ok := evt.Payload.(map[string]any)
if !ok {
return
}
@@ -189,7 +189,7 @@ func handleCLIEvent(evt protocol.EventFrame) {
case protocol.EventAgent:
switch evtType {
case protocol.AgentEventToolCall:
if p, ok := payload["payload"].(map[string]interface{}); ok {
if p, ok := payload["payload"].(map[string]any); ok {
name, _ := p["toolName"].(string)
if name == "" {
name, _ = p["name"].(string)
@@ -197,7 +197,7 @@ func handleCLIEvent(evt protocol.EventFrame) {
fmt.Fprintf(os.Stderr, " [tool] %s\n", name)
}
case protocol.AgentEventToolResult:
if p, ok := payload["payload"].(map[string]interface{}); ok {
if p, ok := payload["payload"].(map[string]any); ok {
isErr, _ := p["is_error"].(bool)
name, _ := p["toolName"].(string)
if name == "" {
+2 -2
View File
@@ -39,7 +39,7 @@ func gatewayURL() string {
}
// gatewayRequest sends an authenticated request to the running gateway.
func gatewayRequest(method, path string) (map[string]interface{}, error) {
func gatewayRequest(method, path string) (map[string]any, error) {
url := gatewayURL() + path
req, err := http.NewRequest(method, url, nil)
if err != nil {
@@ -57,7 +57,7 @@ func gatewayRequest(method, path string) (map[string]interface{}, error) {
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
var result map[string]interface{}
var result map[string]any
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("invalid response from gateway: %s", string(body))
}
+4 -4
View File
@@ -68,15 +68,15 @@ func configValidateCmd() *cobra.Command {
}
// redactConfig returns a JSON-safe copy with secrets masked.
func redactConfig(cfg *config.Config) interface{} {
func redactConfig(cfg *config.Config) any {
data, _ := json.Marshal(cfg)
var raw map[string]interface{}
var raw map[string]any
json.Unmarshal(data, &raw)
redactMap(raw)
return raw
}
func redactMap(m map[string]interface{}) {
func redactMap(m map[string]any) {
secretKeys := map[string]bool{
"apiKey": true, "api_key": true, "token": true,
"botToken": true, "bot_token": true, "secret": true,
@@ -89,7 +89,7 @@ func redactMap(m map[string]interface{}) {
} else if s, ok := v.(string); ok && s != "" {
m[k] = "****"
}
} else if sub, ok := v.(map[string]interface{}); ok {
} else if sub, ok := v.(map[string]any); ok {
redactMap(sub)
}
}
+2 -2
View File
@@ -67,7 +67,7 @@ func cronToggleCmd() *cobra.Command {
func cronListRPC(showDisabled, jsonOutput bool) {
requireGateway()
params, _ := json.Marshal(map[string]interface{}{"includeDisabled": showDisabled})
params, _ := json.Marshal(map[string]any{"includeDisabled": showDisabled})
resp, err := gatewayRPC(protocol.MethodCronList, params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -109,7 +109,7 @@ func cronDeleteRPC(jobID string) {
func cronToggleRPC(jobID string, enabled bool) {
requireGateway()
params, _ := json.Marshal(map[string]interface{}{"jobId": jobID, "enabled": enabled})
params, _ := json.Marshal(map[string]any{"jobId": jobID, "enabled": enabled})
resp, err := gatewayRPC(protocol.MethodCronToggle, params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+1 -1
View File
@@ -771,7 +771,7 @@ func runGateway() {
// Route activity events to Router (status registry) and DelegateManager (progress tracking).
if agentEvent.Type == protocol.AgentEventActivity {
payloadMap, _ := agentEvent.Payload.(map[string]interface{})
payloadMap, _ := agentEvent.Payload.(map[string]any)
phase, _ := payloadMap["phase"].(string)
tool, _ := payloadMap["tool"].(string)
iteration := 0
+1 -1
View File
@@ -205,7 +205,7 @@ func gatewayRPC(method string, params json.RawMessage) (*protocol.ResponseFrame,
defer conn.Close()
// Step 1: Send connect handshake
connectParams, _ := json.Marshal(map[string]interface{}{
connectParams, _ := json.Marshal(map[string]any{
"token": cfg.Gateway.Token,
"protocol": protocol.ProtocolVersion,
})
+2 -2
View File
@@ -1,6 +1,6 @@
module github.com/nextlevelbuilder/goclaw
go 1.25.5
go 1.26.0
require (
github.com/adhocore/gronx v1.19.6
@@ -16,6 +16,7 @@ require (
github.com/mattn/go-runewidth v0.0.16
github.com/mymmrac/telego v1.6.0
github.com/redis/go-redis/v9 v9.18.0
github.com/slack-go/slack v0.19.0
github.com/spf13/cobra v1.10.2
github.com/titanous/json5 v1.0.0
go.opentelemetry.io/otel v1.40.0
@@ -86,7 +87,6 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/slack-go/slack v0.19.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
+2
View File
@@ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
+1 -1
View File
@@ -81,7 +81,7 @@ func ClassifyIntent(ctx context.Context, provider providers.Provider, model, use
{Role: "user", Content: userMessage},
},
Model: model,
Options: map[string]interface{}{
Options: map[string]any{
providers.OptMaxTokens: 20,
providers.OptTemperature: 0.0,
},
+13 -13
View File
@@ -334,7 +334,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
totalToolCalls := 0
var finalContent string
var finalThinking string
var asyncToolCalls []string // track async spawn tool names for fallback
var asyncToolCalls []string // track async spawn tool names for fallback
var mediaResults []MediaResult // media files from tool MEDIA: results
var deliverables []string // actual content from tool outputs (for team task results)
var blockReplies int // count of block.reply events emitted (for dedup in consumer)
@@ -347,7 +347,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
// Team task orphan detection: track team_tasks create vs spawn calls.
// If the LLM creates tasks but forgets to spawn, inject a reminder.
var teamTaskCreates int // count of team_tasks action=create calls
var teamTaskSpawns int // count of spawn calls with team_task_id
var teamTaskSpawns int // count of spawn calls with team_task_id
var teamTaskRetried bool // only retry once to prevent infinite loops
// Inject retry hook so channels can update placeholder on LLM retries.
@@ -379,7 +379,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Type: protocol.AgentEventActivity,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"phase": "thinking", "iteration": iteration},
Payload: map[string]any{"phase": "thinking", "iteration": iteration},
})
// Build provider request with policy-filtered tools
@@ -399,7 +399,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Messages: messages,
Tools: toolDefs,
Model: l.model,
Options: map[string]interface{}{
Options: map[string]any{
providers.OptMaxTokens: 8192,
providers.OptTemperature: 0.7,
providers.OptSessionKey: req.SessionKey,
@@ -505,7 +505,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Type: protocol.AgentEventActivity,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"phase": "compacting", "iteration": iteration},
Payload: map[string]any{"phase": "compacting", "iteration": iteration},
})
if compacted := l.compactMessagesInPlace(ctx, messages); compacted != nil {
messages = compacted
@@ -562,7 +562,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Content: resp.Content,
Thinking: resp.Thinking, // reasoning_content passback for thinking models (Kimi, DeepSeek)
ToolCalls: resp.ToolCalls,
Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex)
Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex)
RawAssistantContent: resp.RawAssistantContent, // preserve thinking blocks for Anthropic passback
}
messages = append(messages, assistantMsg)
@@ -617,7 +617,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Type: protocol.AgentEventActivity,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{
Payload: map[string]any{
"phase": "tool_exec",
"tool": toolNames[0],
"tools": toolNames,
@@ -634,7 +634,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Type: protocol.AgentEventToolCall,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
})
argsJSON, _ := json.Marshal(tc.Arguments)
@@ -675,7 +675,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
}
toolResultPayload := map[string]interface{}{
toolResultPayload := map[string]any{
"name": tc.Name,
"id": tc.ID,
"is_error": result.IsError,
@@ -748,7 +748,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Type: protocol.AgentEventToolCall,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
})
}
@@ -816,7 +816,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
}
parToolResultPayload := map[string]interface{}{
parToolResultPayload := map[string]any{
"name": r.tc.Name,
"id": r.tc.ID,
"is_error": r.result.IsError,
@@ -981,8 +981,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
// truncateToolArgs returns a copy of arguments with string values truncated to maxLen.
func truncateToolArgs(args map[string]interface{}, maxLen int) map[string]interface{} {
out := make(map[string]interface{}, len(args))
func truncateToolArgs(args map[string]any, maxLen int) map[string]any {
out := make(map[string]any, len(args))
for k, v := range args {
if s, ok := v.(string); ok && len(s) > maxLen {
out[k] = truncateStr(s, maxLen)
+1 -1
View File
@@ -66,7 +66,7 @@ func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers.
Content: "Provide a concise summary of this conversation, preserving key findings, data, and context:\n\n" + sb.String(),
}},
Model: l.model,
Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3},
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
})
if err != nil {
slog.Warn("mid_loop_compaction_failed", "agent", l.id, "error", err)
+14 -13
View File
@@ -120,7 +120,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s
HasSpawn: l.tools != nil && hasSpawn,
HasSkillSearch: hasSkillSearch,
HasMCPToolSearch: hasMCPToolSearch,
HasKnowledgeGraph: hasKG,
HasKnowledgeGraph: hasKG,
MCPToolDescs: mcpToolDescs,
ContextFiles: contextFiles,
AgentType: l.agentType,
@@ -397,46 +397,47 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) {
summary := l.sessions.GetSummary(sessionKey)
toSummarize := history[:len(history)-keepLast]
var sb string
var sb strings.Builder
var mediaKinds []string
for _, m := range toSummarize {
if m.Role == "user" {
sb += fmt.Sprintf("user: %s\n", m.Content)
sb.WriteString(fmt.Sprintf("user: %s\n", m.Content))
} else if m.Role == "assistant" {
sb += fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content))
sb.WriteString(fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content)))
}
for _, ref := range m.MediaRefs {
mediaKinds = append(mediaKinds, ref.Kind)
}
}
prompt := "Provide a concise summary of this conversation, preserving key context:\n"
var prompt strings.Builder
prompt.WriteString("Provide a concise summary of this conversation, preserving key context:\n")
if len(mediaKinds) > 0 {
// Deduplicate and count media types for a compact note.
counts := make(map[string]int)
for _, k := range mediaKinds {
counts[k]++
}
prompt += "\nNote: user shared media files ("
prompt.WriteString("\nNote: user shared media files (")
first := true
for k, n := range counts {
if !first {
prompt += ", "
prompt.WriteString(", ")
}
prompt += fmt.Sprintf("%d %s(s)", n, k)
prompt.WriteString(fmt.Sprintf("%d %s(s)", n, k))
first = false
}
prompt += ") which are no longer in context. Mention briefly if relevant.\n"
prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n")
}
if summary != "" {
prompt += "Existing context: " + summary + "\n"
prompt.WriteString("Existing context: " + summary + "\n")
}
prompt += "\n" + sb
prompt.WriteString("\n" + sb.String())
resp, err := l.provider.Chat(sctx, providers.ChatRequest{
Messages: []providers.Message{{Role: "user", Content: prompt}},
Messages: []providers.Message{{Role: "user", Content: prompt.String()}},
Model: l.model,
Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3},
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
})
if err != nil {
slog.Warn("summarization failed", "session", sessionKey, "error", err)
+2 -2
View File
@@ -36,7 +36,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
Type: protocol.AgentEventRunStarted,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"message": req.Message},
Payload: map[string]any{"message": req.Message},
})
// Create trace
@@ -132,7 +132,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
Type: protocol.AgentEventRunCompleted,
AgentID: l.id,
RunID: req.RunID,
Payload: map[string]interface{}{"content": result.Content},
Payload: map[string]any{"content": result.Content},
})
if !isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
l.traceCollector.FinishTrace(ctx, traceID, store.TraceStatusCompleted, "", truncateStr(result.Content, 500))
+96 -96
View File
@@ -47,12 +47,12 @@ type Loop struct {
maxToolCalls int
workspace string
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
sessions store.SessionStore
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
sessions store.SessionStore
tools *tools.Registry
toolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
agentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
activeRuns atomic.Int32 // number of currently executing runs
activeRuns atomic.Int32 // number of currently executing runs
// Per-session summarization lock: prevents concurrent summarize goroutines for the same session.
summarizeMu sync.Map // sessionKey → *sync.Mutex
@@ -65,10 +65,10 @@ type Loop struct {
contextFiles []bootstrap.ContextFile
// Per-user file seeding + dynamic context loading
ensureUserFiles EnsureUserFilesFunc
contextFileLoader ContextFileLoaderFunc
bootstrapCleanup BootstrapCleanupFunc
userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles)
ensureUserFiles EnsureUserFilesFunc
contextFileLoader ContextFileLoaderFunc
bootstrapCleanup BootstrapCleanupFunc
userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles)
// Compaction config (memory flush settings)
compactionCfg *config.CompactionConfig
@@ -77,8 +77,8 @@ type Loop struct {
contextPruningCfg *config.ContextPruningConfig
// Sandbox info
sandboxEnabled bool
sandboxContainerDir string
sandboxEnabled bool
sandboxContainerDir string
sandboxWorkspaceAccess string
// Event callback for broadcasting agent events (run.started, chunk, tool.call, etc.)
@@ -113,11 +113,11 @@ type Loop struct {
// AgentEvent is emitted during agent execution for WS broadcasting.
type AgentEvent struct {
Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result"
AgentID string `json:"agentId"`
RunID string `json:"runId"`
RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs
Payload interface{} `json:"payload,omitempty"`
Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result"
AgentID string `json:"agentId"`
RunID string `json:"runId"`
RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs
Payload any `json:"payload,omitempty"`
// Delegation context (omitempty — only present when agent runs inside a delegation)
DelegationID string `json:"delegationId,omitempty"`
@@ -133,15 +133,15 @@ type AgentEvent struct {
// LoopConfig configures a new Loop.
type LoopConfig struct {
ID string
Provider providers.Provider
Model string
ContextWindow int
MaxIterations int
MaxToolCalls int
Workspace string
Bus bus.EventPublisher
Sessions store.SessionStore
ID string
Provider providers.Provider
Model string
ContextWindow int
MaxIterations int
MaxToolCalls int
Workspace string
Bus bus.EventPublisher
Sessions store.SessionStore
Tools *tools.Registry
ToolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
AgentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
@@ -161,8 +161,8 @@ type LoopConfig struct {
ContextPruningCfg *config.ContextPruningConfig
// Sandbox info (injected into system prompt)
SandboxEnabled bool
SandboxContainerDir string // e.g. "/workspace"
SandboxEnabled bool
SandboxContainerDir string // e.g. "/workspace"
SandboxWorkspaceAccess string // "none", "ro", "rw"
// Agent UUID for context propagation to tools
@@ -178,9 +178,9 @@ type LoopConfig struct {
TraceCollector *tracing.Collector
// Security: input guard for injection detection, max message size
InputGuard *InputGuard // nil = auto-create when InjectionAction != "off"
InjectionAction string // "log", "warn" (default), "block", "off"
MaxMessageChars int // 0 = use default (32000)
InputGuard *InputGuard // nil = auto-create when InjectionAction != "off"
InjectionAction string // "log", "warn" (default), "block", "off"
MaxMessageChars int // 0 = use default (32000)
// Global builtin tool settings (from builtin_tools table)
BuiltinToolSettings tools.BuiltinToolSettings
@@ -225,71 +225,71 @@ func NewLoop(cfg LoopConfig) *Loop {
}
return &Loop{
id: cfg.ID,
agentUUID: cfg.AgentUUID,
agentType: cfg.AgentType,
provider: cfg.Provider,
model: cfg.Model,
contextWindow: cfg.ContextWindow,
maxIterations: cfg.MaxIterations,
maxToolCalls: cfg.MaxToolCalls,
workspace: cfg.Workspace,
eventPub: cfg.Bus,
sessions: cfg.Sessions,
tools: cfg.Tools,
toolPolicy: cfg.ToolPolicy,
agentToolPolicy: cfg.AgentToolPolicy,
onEvent: cfg.OnEvent,
ownerIDs: cfg.OwnerIDs,
skillsLoader: cfg.SkillsLoader,
skillAllowList: cfg.SkillAllowList,
hasMemory: cfg.HasMemory,
contextFiles: cfg.ContextFiles,
ensureUserFiles: cfg.EnsureUserFiles,
contextFileLoader: cfg.ContextFileLoader,
bootstrapCleanup: cfg.BootstrapCleanup,
compactionCfg: cfg.CompactionCfg,
contextPruningCfg: cfg.ContextPruningCfg,
sandboxEnabled: cfg.SandboxEnabled,
sandboxContainerDir: cfg.SandboxContainerDir,
id: cfg.ID,
agentUUID: cfg.AgentUUID,
agentType: cfg.AgentType,
provider: cfg.Provider,
model: cfg.Model,
contextWindow: cfg.ContextWindow,
maxIterations: cfg.MaxIterations,
maxToolCalls: cfg.MaxToolCalls,
workspace: cfg.Workspace,
eventPub: cfg.Bus,
sessions: cfg.Sessions,
tools: cfg.Tools,
toolPolicy: cfg.ToolPolicy,
agentToolPolicy: cfg.AgentToolPolicy,
onEvent: cfg.OnEvent,
ownerIDs: cfg.OwnerIDs,
skillsLoader: cfg.SkillsLoader,
skillAllowList: cfg.SkillAllowList,
hasMemory: cfg.HasMemory,
contextFiles: cfg.ContextFiles,
ensureUserFiles: cfg.EnsureUserFiles,
contextFileLoader: cfg.ContextFileLoader,
bootstrapCleanup: cfg.BootstrapCleanup,
compactionCfg: cfg.CompactionCfg,
contextPruningCfg: cfg.ContextPruningCfg,
sandboxEnabled: cfg.SandboxEnabled,
sandboxContainerDir: cfg.SandboxContainerDir,
sandboxWorkspaceAccess: cfg.SandboxWorkspaceAccess,
traceCollector: cfg.TraceCollector,
inputGuard: guard,
injectionAction: action,
maxMessageChars: cfg.MaxMessageChars,
builtinToolSettings: cfg.BuiltinToolSettings,
thinkingLevel: cfg.ThinkingLevel,
selfEvolve: cfg.SelfEvolve,
groupWriterCache: cfg.GroupWriterCache,
teamStore: cfg.TeamStore,
mediaStore: cfg.MediaStore,
traceCollector: cfg.TraceCollector,
inputGuard: guard,
injectionAction: action,
maxMessageChars: cfg.MaxMessageChars,
builtinToolSettings: cfg.BuiltinToolSettings,
thinkingLevel: cfg.ThinkingLevel,
selfEvolve: cfg.SelfEvolve,
groupWriterCache: cfg.GroupWriterCache,
teamStore: cfg.TeamStore,
mediaStore: cfg.MediaStore,
}
}
// RunRequest is the input for processing a message through the agent.
type RunRequest struct {
SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId}
Message string // user message
Media []bus.MediaFile // local media files with MIME types
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
Channel string // source channel instance name (e.g. "my-telegram-bot")
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
ChatID string // source chat ID
PeerKind string // "direct" or "group" (for session key building and tool context)
RunID string // unique run identifier
UserID string // external user ID (TEXT, free-form) for multi-tenant scoping
SenderID string // original individual sender ID (preserved in group chats for permission checks)
Stream bool // whether to stream response chunks
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
TraceName string // override trace name (default: "chat <agentID>")
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 <agentID>")
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)
}
+2 -2
View File
@@ -173,12 +173,12 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *
// Run LLM iteration loop (max 5 iterations for flush)
maxFlushIter := 5
for i := 0; i < maxFlushIter; i++ {
for range maxFlushIter {
resp, err := l.provider.Chat(flushCtx, providers.ChatRequest{
Messages: messages,
Tools: toolDefs,
Model: l.model,
Options: map[string]interface{}{
Options: map[string]any{
"max_tokens": 4096,
"temperature": 0.3,
},
+5 -8
View File
@@ -2,6 +2,7 @@ package agent
import (
"fmt"
"slices"
"strings"
"github.com/google/uuid"
@@ -172,10 +173,8 @@ func agentToolPolicyForTeam(policy *config.ToolPolicySpec, isLead bool) *config.
if policy == nil {
policy = &config.ToolPolicySpec{}
}
for _, d := range policy.Deny {
if d == "team_message" {
return policy
}
if slices.Contains(policy.Deny, "team_message") {
return policy
}
policy.Deny = append(policy.Deny, "team_message")
return policy
@@ -191,10 +190,8 @@ func agentToolPolicyWithMCP(policy *config.ToolPolicySpec, hasMCP bool) *config.
policy = &config.ToolPolicySpec{}
}
// Check if group:mcp is already present
for _, a := range policy.AlsoAllow {
if a == "group:mcp" {
return policy
}
if slices.Contains(policy.AlsoAllow, "group:mcp") {
return policy
}
policy.AlsoAllow = append(policy.AlsoAllow, "group:mcp")
return policy
+6 -6
View File
@@ -22,8 +22,8 @@ type agentEntry struct {
// AgentActivityStatus tracks the current phase of a running agent for status queries.
type AgentActivityStatus struct {
RunID string
Phase string // "thinking", "tool_exec", "compacting"
Tool string // current tool name (when Phase == "tool_exec")
Phase string // "thinking", "tool_exec", "compacting"
Tool string // current tool name (when Phase == "tool_exec")
Iteration int
StartedAt time.Time
}
@@ -34,9 +34,9 @@ type AgentActivityStatus struct {
type Router struct {
agents map[string]*agentEntry
mu sync.RWMutex
activeRuns sync.Map // runID → *ActiveRun
sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy)
agentActivity sync.Map // sessionKey → *AgentActivityStatus
activeRuns sync.Map // runID → *ActiveRun
sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy)
agentActivity sync.Map // sessionKey → *AgentActivityStatus
resolver ResolverFunc // optional: lazy creation from DB
ttl time.Duration
}
@@ -262,7 +262,7 @@ func (r *Router) IsSessionBusy(sessionKey string) bool {
// Returns the list of aborted run IDs.
func (r *Router) AbortRunsForSession(sessionKey string) []string {
var aborted []string
r.activeRuns.Range(func(key, val interface{}) bool {
r.activeRuns.Range(func(key, val any) bool {
run := val.(*ActiveRun)
if run.SessionKey == sessionKey {
run.Cancel()
+5 -5
View File
@@ -28,7 +28,7 @@ type toolCallRecord struct {
}
// record adds a tool call to history and returns its argsHash.
func (s *toolLoopState) record(toolName string, args map[string]interface{}) string {
func (s *toolLoopState) record(toolName string, args map[string]any) string {
h := hashToolCall(toolName, args)
s.history = append(s.history, toolCallRecord{
toolName: toolName,
@@ -98,7 +98,7 @@ func (s *toolLoopState) detect(toolName string, argsHash string) (level, message
}
// hashToolCall produces a deterministic hash of tool name + arguments.
func hashToolCall(toolName string, args map[string]interface{}) string {
func hashToolCall(toolName string, args map[string]any) string {
s := toolName + ":" + stableJSON(args)
h := sha256.Sum256([]byte(s))
return fmt.Sprintf("%x", h[:16]) // 32 hex chars, enough for dedup
@@ -111,9 +111,9 @@ func hashResult(content string) string {
}
// stableJSON serializes a value with sorted keys for deterministic hashing.
func stableJSON(v interface{}) string {
func stableJSON(v any) string {
switch val := v.(type) {
case map[string]interface{}:
case map[string]any:
keys := make([]string, 0, len(val))
for k := range val {
keys = append(keys, k)
@@ -124,7 +124,7 @@ func stableJSON(v interface{}) string {
parts[i] = fmt.Sprintf("%q:%s", k, stableJSON(val[k]))
}
return "{" + strings.Join(parts, ",") + "}"
case []interface{}:
case []any:
parts := make([]string, len(val))
for i, elem := range val {
parts[i] = stableJSON(elem)
+17 -17
View File
@@ -6,8 +6,8 @@ func TestToolLoopDetection_NoLoop(t *testing.T) {
var s toolLoopState
// 2 identical calls with same result → below threshold, no detection
for i := 0; i < 2; i++ {
h := s.record("list_files", map[string]interface{}{"path": "."})
for i := range 2 {
h := s.record("list_files", map[string]any{"path": "."})
s.recordResult(h, "access denied")
level, _ := s.detect("list_files", h)
if level != "" {
@@ -20,8 +20,8 @@ func TestToolLoopDetection_Warning(t *testing.T) {
var s toolLoopState
var lastLevel string
for i := 0; i < toolLoopWarningThreshold; i++ {
h := s.record("list_files", map[string]interface{}{"path": "."})
for range toolLoopWarningThreshold {
h := s.record("list_files", map[string]any{"path": "."})
s.recordResult(h, "access denied")
lastLevel, _ = s.detect("list_files", h)
}
@@ -34,8 +34,8 @@ func TestToolLoopDetection_Critical(t *testing.T) {
var s toolLoopState
var lastLevel string
for i := 0; i < toolLoopCriticalThreshold; i++ {
h := s.record("list_files", map[string]interface{}{"path": "."})
for range toolLoopCriticalThreshold {
h := s.record("list_files", map[string]any{"path": "."})
s.recordResult(h, "access denied")
lastLevel, _ = s.detect("list_files", h)
}
@@ -48,8 +48,8 @@ func TestToolLoopDetection_DifferentArgs(t *testing.T) {
var s toolLoopState
// Same tool but different args each time → no detection
for i := 0; i < 15; i++ {
args := map[string]interface{}{"path": string(rune('a' + i))}
for i := range 15 {
args := map[string]any{"path": string(rune('a' + i))}
h := s.record("list_files", args)
s.recordResult(h, "access denied")
level, _ := s.detect("list_files", h)
@@ -63,8 +63,8 @@ func TestToolLoopDetection_DifferentResults(t *testing.T) {
var s toolLoopState
// Same args but different results each time → progress, no detection
for i := 0; i < 15; i++ {
h := s.record("web_fetch", map[string]interface{}{"url": "https://example.com"})
for i := range 15 {
h := s.record("web_fetch", map[string]any{"url": "https://example.com"})
s.recordResult(h, "result content "+string(rune('a'+i)))
level, _ := s.detect("web_fetch", h)
if level != "" {
@@ -78,12 +78,12 @@ func TestToolLoopDetection_MixedTools(t *testing.T) {
// Alternate between two tools with same result → each tool only hit ~half
// With 8 iterations, each tool is called 4 times → below critical (5)
for i := 0; i < 8; i++ {
for i := range 8 {
toolName := "list_files"
if i%2 == 1 {
toolName = "read_file"
}
h := s.record(toolName, map[string]interface{}{"path": "."})
h := s.record(toolName, map[string]any{"path": "."})
s.recordResult(h, "error")
level, _ := s.detect(toolName, h)
// Each tool is only called 4 times, should at most warn
@@ -95,8 +95,8 @@ func TestToolLoopDetection_MixedTools(t *testing.T) {
func TestStableJSON(t *testing.T) {
// Same keys in different order → same hash
a := stableJSON(map[string]interface{}{"b": 2, "a": 1})
b := stableJSON(map[string]interface{}{"a": 1, "b": 2})
a := stableJSON(map[string]any{"b": 2, "a": 1})
b := stableJSON(map[string]any{"a": 1, "b": 2})
if a != b {
t.Fatalf("stableJSON not deterministic: %q != %q", a, b)
}
@@ -104,14 +104,14 @@ func TestStableJSON(t *testing.T) {
func TestHashToolCall(t *testing.T) {
// Same input → same hash
h1 := hashToolCall("list_files", map[string]interface{}{"path": "."})
h2 := hashToolCall("list_files", map[string]interface{}{"path": "."})
h1 := hashToolCall("list_files", map[string]any{"path": "."})
h2 := hashToolCall("list_files", map[string]any{"path": "."})
if h1 != h2 {
t.Fatal("hashToolCall not deterministic")
}
// Different tool → different hash
h3 := hashToolCall("read_file", map[string]interface{}{"path": "."})
h3 := hashToolCall("read_file", map[string]any{"path": "."})
if h1 == h3 {
t.Fatal("different tools should have different hashes")
}
+2 -3
View File
@@ -2,6 +2,7 @@ package bootstrap
import (
"context"
"maps"
"testing"
"github.com/google/uuid"
@@ -253,9 +254,7 @@ func TestSeedUserFiles_IdempotentOnSecondCall(t *testing.T) {
SeedUserFiles(context.Background(), as, agentID, "user-frank", store.AgentTypePredefined)
// Simulate what the first call wrote (move seededUserFiles → userFiles)
for k, v := range as.seededUserFiles {
as.userFiles[k] = v
}
maps.Copy(as.userFiles, as.seededUserFiles)
as.seededUserFiles = make(map[string]string)
// Second call — must seed nothing (all files already exist)
+2 -2
View File
@@ -43,8 +43,8 @@ type MediaAttachment struct {
// Event represents a server-side event to broadcast to WebSocket clients.
type Event struct {
Name string `json:"name"` // event name (e.g. "agent", "chat", "health")
Payload interface{} `json:"payload,omitempty"`
Name string `json:"name"` // event name (e.g. "agent", "chat", "health")
Payload any `json:"payload,omitempty"`
}
// Cache invalidation kind constants.
+16 -16
View File
@@ -13,7 +13,7 @@ import (
// HandleAgentEvent routes agent lifecycle events to streaming/reaction channels.
// Called from the bus event subscriber — must be non-blocking.
// eventType: "run.started", "chunk", "tool.call", "tool.result", "run.completed", "run.failed"
func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{}) {
func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
val, ok := m.runs.Load(runID)
if !ok {
return
@@ -187,11 +187,11 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{})
}
// extractPayloadString extracts a string field from a payload (map[string]string or map[string]interface{}).
func extractPayloadString(payload interface{}, key string) string {
func extractPayloadString(payload any, key string) string {
switch p := payload.(type) {
case map[string]string:
return p[key]
case map[string]interface{}:
case map[string]any:
if v, ok := p[key].(string); ok {
return v
}
@@ -214,28 +214,28 @@ func copyRoutingMeta(src map[string]string) map[string]string {
// toolStatusMap maps builtin tool names to user-friendly status messages.
var toolStatusMap = map[string]string{
// Filesystem
"read_file": "📝 Reading file...",
"write_file": "📝 Writing file...",
"list_files": "📝 Listing files...",
"edit": "📝 Editing file...",
"read_file": "📝 Reading file...",
"write_file": "📝 Writing file...",
"list_files": "📝 Listing files...",
"edit": "📝 Editing file...",
// Runtime
"exec": "⚡ Running code...",
// Web
"web_search": "🔍 Searching the web...",
"web_fetch": "🔍 Fetching web content...",
// Memory
"memory_search": "🧠 Searching memory...",
"memory_get": "🧠 Retrieving memory...",
"memory_search": "🧠 Searching memory...",
"memory_get": "🧠 Retrieving memory...",
"knowledge_graph_search": "🧠 Querying knowledge graph...",
// Media
"read_image": "👁 Analyzing image...",
"read_image": "👁 Analyzing image...",
"read_document": "📄 Reading document...",
"read_audio": "🎧 Processing audio...",
"read_video": "🎬 Processing video...",
"create_image": "🎨 Creating image...",
"create_video": "🎬 Creating video...",
"create_audio": "🎵 Creating audio...",
"tts": "🔊 Generating speech...",
"read_audio": "🎧 Processing audio...",
"read_video": "🎬 Processing video...",
"create_image": "🎨 Creating image...",
"create_video": "🎬 Creating video...",
"create_audio": "🎵 Creating audio...",
"tts": "🔊 Generating speech...",
// Browser
"browser": "🌐 Browsing...",
// Delegation & teams
+6 -6
View File
@@ -100,12 +100,12 @@ func parseMessageContent(rawContent, messageType string) string {
}
func parsePostContent(rawContent string) string {
var post map[string]interface{}
var post map[string]any
if err := json.Unmarshal([]byte(rawContent), &post); err != nil {
return rawContent
}
var langContent interface{}
var langContent any
for _, lang := range []string{"zh_cn", "en_us"} {
if lc, ok := post[lang]; ok {
langContent = lc
@@ -122,25 +122,25 @@ func parsePostContent(rawContent string) string {
return rawContent
}
langMap, ok := langContent.(map[string]interface{})
langMap, ok := langContent.(map[string]any)
if !ok {
return rawContent
}
contentArr, ok := langMap["content"].([]interface{})
contentArr, ok := langMap["content"].([]any)
if !ok {
return rawContent
}
var textParts []string
for _, para := range contentArr {
paraArr, ok := para.([]interface{})
paraArr, ok := para.([]any)
if !ok {
continue
}
var lineParts []string
for _, elem := range paraArr {
elemMap, ok := elem.(map[string]interface{})
elemMap, ok := elem.(map[string]any)
if !ok {
continue
}
+9 -9
View File
@@ -40,7 +40,7 @@ type Channel struct {
dedup sync.Map // message_id → struct{}
pairingDebounce sync.Map // senderID → time.Time
reactions sync.Map // chatID → *reactionState
approvedGroups sync.Map // chatID → true (in-memory cache for paired groups)
approvedGroups sync.Map // chatID → true (in-memory cache for paired groups)
groupAllowList []string
groupHistory *channels.PendingHistory
historyLimit int
@@ -391,9 +391,9 @@ func resolveReceiveIDType(id string) string {
// --- Content builders ---
func buildPostContent(text string) string {
content := map[string]interface{}{
"zh_cn": map[string]interface{}{
"content": [][]map[string]interface{}{
content := map[string]any{
"zh_cn": map[string]any{
"content": [][]map[string]any{
{
{
"tag": "md",
@@ -407,14 +407,14 @@ func buildPostContent(text string) string {
return string(data)
}
func buildMarkdownCard(text string) map[string]interface{} {
return map[string]interface{}{
func buildMarkdownCard(text string) map[string]any {
return map[string]any{
"schema": "2.0",
"config": map[string]interface{}{
"config": map[string]any{
"wide_screen_mode": true,
},
"body": map[string]interface{}{
"elements": []map[string]interface{}{
"body": map[string]any{
"elements": []map[string]any{
{
"tag": "markdown",
"content": text,
+2 -2
View File
@@ -107,7 +107,7 @@ type apiResponse struct {
}
// doJSON performs an authenticated JSON API call with auto token refresh.
func (c *LarkClient) doJSON(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) {
func (c *LarkClient) doJSON(ctx context.Context, method, path string, body any) (*apiResponse, error) {
resp, err := c.doJSONOnce(ctx, method, path, body)
if err != nil {
return nil, err
@@ -120,7 +120,7 @@ func (c *LarkClient) doJSON(ctx context.Context, method, path string, body inter
return resp, nil
}
func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) {
func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body any) (*apiResponse, error) {
token, err := c.getToken(ctx)
if err != nil {
return nil, err
@@ -112,7 +112,7 @@ func (c *LarkClient) CreateCard(ctx context.Context, cardType, data string) (str
func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings string, seq int, uuid string) error {
path := "/open-apis/cardkit/v1/cards/" + cardID
resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{
resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{
"settings": settings,
"sequence": seq,
"uuid": uuid,
@@ -128,7 +128,7 @@ func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings st
func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, content string, seq int, uuid string) error {
path := fmt.Sprintf("/open-apis/cardkit/v1/cards/%s/elements/%s", cardID, elementID)
resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{
resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{
"content": content,
"sequence": seq,
"uuid": uuid,
@@ -150,7 +150,7 @@ func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, c
// Lark API: POST /open-apis/im/v1/messages/{message_id}/reactions
func (c *LarkClient) AddMessageReaction(ctx context.Context, messageID, emojiType string) (string, error) {
path := fmt.Sprintf("/open-apis/im/v1/messages/%s/reactions", messageID)
body := map[string]interface{}{
body := map[string]any{
"reaction_type": map[string]string{
"emoji_type": emojiType,
},
+7 -8
View File
@@ -16,12 +16,12 @@ import (
)
const (
defaultPingInterval = 120 * time.Second
defaultReconnectNonce = 30 // seconds max jitter
defaultReconnectWait = 120 * time.Second
frameTypeControl = 0
frameTypeData = 1
fragmentBufferTTL = 5 * time.Second
defaultPingInterval = 120 * time.Second
defaultReconnectNonce = 30 // seconds max jitter
defaultReconnectWait = 120 * time.Second
frameTypeControl = 0
frameTypeData = 1
fragmentBufferTTL = 5 * time.Second
)
// WSEventHandler processes incoming WebSocket events.
@@ -315,7 +315,7 @@ func (c *WSClient) sendResponse(original *wsFrame, headers map[string]string) {
}
respHeaders = append(respHeaders, wsHeader{Key: "biz_rt", Value: "0"})
respPayload, _ := json.Marshal(map[string]interface{}{
respPayload, _ := json.Marshal(map[string]any{
"code": 0,
"msg": "success",
})
@@ -415,4 +415,3 @@ func (c *WSClient) reassemble(msgID string, total, seq int, data []byte) []byte
delete(c.fragments, msgID)
return result
}
+1 -1
View File
@@ -103,7 +103,7 @@ func (ph *PendingHistory) runCompaction(historyKey string, cfg *CompactionConfig
Content: "Summarize these group chat messages concisely, preserving key topics, decisions, names, and important context:\n\n" + sb.String(),
}},
Model: cfg.Model,
Options: map[string]interface{}{"max_tokens": 512, "temperature": 0.3},
Options: map[string]any{"max_tokens": 512, "temperature": 0.3},
})
if err != nil {
slog.Warn("compaction.llm_failed", "channel", ph.channelName, "key", historyKey, "error", err)
+3 -4
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"maps"
"sync"
"time"
@@ -146,7 +147,7 @@ func coerceStringBools(data json.RawMessage) json.RawMessage {
if len(data) == 0 {
return data
}
var m map[string]interface{}
var m map[string]any
if json.Unmarshal(data, &m) != nil {
return data
}
@@ -176,9 +177,7 @@ func (l *InstanceLoader) LoadedNames() map[string]struct{} {
defer l.mu.Unlock()
result := make(map[string]struct{}, len(l.loaded))
for k, v := range l.loaded {
result[k] = v
}
maps.Copy(result, l.loaded)
return result
}
+3 -3
View File
@@ -107,13 +107,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) {
}
// GetStatus returns the running status of all channels.
func (m *Manager) GetStatus() map[string]interface{} {
func (m *Manager) GetStatus() map[string]any {
m.mu.RLock()
defer m.mu.RUnlock()
status := make(map[string]interface{})
status := make(map[string]any)
for name, channel := range m.channels {
status[name] = map[string]interface{}{
status[name] = map[string]any{
"enabled": true,
"running": channel.IsRunning(),
}
+6 -9
View File
@@ -298,12 +298,12 @@ func parseTableRow(line string) []string {
// stripInlineMarkdown removes common inline markdown markers from text.
// Used for table cells that render inside code blocks where formatting has no effect.
var (
reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`)
reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`)
reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`)
reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`)
reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`)
reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`)
reStripItalicUnderscore = regexp.MustCompile(`_([^_]+)_`)
reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
reStripInlineCode = regexp.MustCompile("`([^`]+)`")
reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
reStripInlineCode = regexp.MustCompile("`([^`]+)`")
)
func stripInlineMarkdown(s string) string {
@@ -325,10 +325,7 @@ func renderRow(cells []string, colWidths []int) string {
cell = cells[j]
}
// Pad with spaces to align columns
padding := w - displayWidth(cell)
if padding < 0 {
padding = 0
}
padding := max(w-displayWidth(cell), 0)
parts = append(parts, " "+cell+strings.Repeat(" ", padding)+" ")
}
return "|" + strings.Join(parts, "|") + "|"
@@ -6,12 +6,13 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/config"
)
func boolPtr(b bool) *bool { return &b }
//go:fix inline
func boolPtr(b bool) *bool { return new(b) }
func TestResolveTopicConfig_Defaults(t *testing.T) {
cfg := config.TelegramConfig{
GroupPolicy: "open",
RequireMention: boolPtr(true),
RequireMention: new(true),
AllowFrom: []string{"user1"},
}
@@ -37,7 +38,7 @@ func TestResolveTopicConfig_WildcardGroup(t *testing.T) {
Groups: map[string]*config.TelegramGroupConfig{
"*": {
GroupPolicy: "allowlist",
RequireMention: boolPtr(false),
RequireMention: new(false),
AllowFrom: []string{"admin1"},
},
},
@@ -62,7 +63,7 @@ func TestResolveTopicConfig_SpecificGroupOverridesWildcard(t *testing.T) {
Groups: map[string]*config.TelegramGroupConfig{
"*": {
GroupPolicy: "allowlist",
RequireMention: boolPtr(false),
RequireMention: new(false),
AllowFrom: []string{"admin1"},
SystemPrompt: "wildcard prompt",
},
@@ -96,12 +97,12 @@ func TestResolveTopicConfig_TopicOverridesGroup(t *testing.T) {
GroupPolicy: "open",
Groups: map[string]*config.TelegramGroupConfig{
"-100123": {
RequireMention: boolPtr(true),
RequireMention: new(true),
SystemPrompt: "group prompt",
Skills: []string{"skill_a", "skill_b"},
Topics: map[string]*config.TelegramTopicConfig{
"42": {
RequireMention: boolPtr(false),
RequireMention: new(false),
Skills: []string{"skill_c"},
SystemPrompt: "topic prompt",
},
@@ -171,10 +172,10 @@ func TestResolveTopicConfig_DisabledTopic(t *testing.T) {
cfg := config.TelegramConfig{
Groups: map[string]*config.TelegramGroupConfig{
"-100123": {
Enabled: boolPtr(true),
Enabled: new(true),
Topics: map[string]*config.TelegramTopicConfig{
"42": {
Enabled: boolPtr(false),
Enabled: new(false),
},
},
},
@@ -230,7 +231,7 @@ func TestEffectiveRequireMention(t *testing.T) {
}
// explicit requireMention overrides default
r2 := resolvedTopicConfig{requireMention: boolPtr(false)}
r2 := resolvedTopicConfig{requireMention: new(false)}
if r2.effectiveRequireMention(true) != false {
t.Error("effectiveRequireMention(true) with override=false should be false")
}
+5 -5
View File
@@ -101,7 +101,7 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
return fmt.Errorf("whatsapp bridge not connected")
}
payload := map[string]interface{}{
payload := map[string]any{
"type": "message",
"to": msg.ChatID,
"content": msg.Content,
@@ -188,7 +188,7 @@ func (c *Channel) listenLoop() {
continue
}
var msg map[string]interface{}
var msg map[string]any
if err := json.Unmarshal(message, &msg); err != nil {
slog.Warn("invalid whatsapp message JSON", "error", err)
continue
@@ -203,7 +203,7 @@ func (c *Channel) listenLoop() {
// handleIncomingMessage processes a message received from the bridge.
// Expected format: {"type":"message","from":"...","chat":"...","content":"...","id":"...","from_name":"...","media":[...]}
func (c *Channel) handleIncomingMessage(msg map[string]interface{}) {
func (c *Channel) handleIncomingMessage(msg map[string]any) {
senderID, ok := msg["from"].(string)
if !ok || senderID == "" {
return
@@ -244,7 +244,7 @@ func (c *Channel) handleIncomingMessage(msg map[string]interface{}) {
}
var media []string
if mediaData, ok := msg["media"].([]interface{}); ok {
if mediaData, ok := msg["media"].([]any); ok {
media = make([]string, 0, len(mediaData))
for _, m := range mediaData {
if path, ok := m.(string); ok {
@@ -368,7 +368,7 @@ func (c *Channel) sendPairingReply(senderID, chatID string) {
return
}
payload, _ := json.Marshal(map[string]interface{}{
payload, _ := json.Marshal(map[string]any{
"type": "message",
"to": chatID,
"content": replyText,
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"maps"
"net/http"
"net/url"
"regexp"
@@ -294,13 +295,13 @@ func loadLoginPage(ctx context.Context, sess *Session) (string, error) {
}
var qrHeaders = http.Header{
"Accept": {"*/*"},
"Content-Type": {"application/x-www-form-urlencoded"},
"Sec-Fetch-Dest": {"empty"},
"Sec-Fetch-Mode": {"cors"},
"Sec-Fetch-Site": {"same-origin"},
"Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"},
"Referrer-Policy": {"strict-origin-when-cross-origin"},
"Accept": {"*/*"},
"Content-Type": {"application/x-www-form-urlencoded"},
"Sec-Fetch-Dest": {"empty"},
"Sec-Fetch-Mode": {"cors"},
"Sec-Fetch-Site": {"same-origin"},
"Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"},
"Referrer-Policy": {"strict-origin-when-cross-origin"},
}
func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[string]string) (*http.Response, error) {
@@ -310,9 +311,7 @@ func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[st
return nil, err
}
req.Header.Set("User-Agent", sess.UserAgent)
for k, v := range qrHeaders {
req.Header[k] = v
}
maps.Copy(req.Header, qrHeaders)
return sess.Client.Do(req)
}
@@ -456,9 +455,7 @@ func qrGetUserInfo(ctx context.Context, sess *Session) (*QRUserInfo, error) {
func setDefaultHeaders(req *http.Request, sess *Session) {
h := defaultHeaders(sess)
for k, v := range h {
req.Header[k] = v
}
maps.Copy(req.Header, h)
}
// readJSON decodes a potentially gzip/deflate-encoded JSON response.
@@ -104,13 +104,14 @@ func generateSignKey(typeStr string, params map[string]any) string {
}
sort.Strings(keys)
signStr := "zsecure" + typeStr
var signStr strings.Builder
signStr.WriteString("zsecure" + typeStr)
for _, k := range keys {
if v := params[k]; v != nil {
signStr += convertToString(v)
signStr.WriteString(convertToString(v))
}
}
hash := md5.Sum([]byte(signStr))
hash := md5.Sum([]byte(signStr.String()))
return hex.EncodeToString(hash[:])
}
@@ -131,11 +132,11 @@ func getEncryptParam(sess *Session, typeStr string) (params map[string]any, enk
}
params = map[string]any{
"zcid": zcid,
"enc_ver": DefaultEncryptVer,
"zcid_ext": zcidExt,
"params": encKey.encData,
"type": DefaultAPIType,
"zcid": zcid,
"enc_ver": DefaultEncryptVer,
"zcid_ext": zcidExt,
"params": encKey.encData,
"type": DefaultAPIType,
"client_version": DefaultAPIVersion,
}
@@ -145,7 +145,7 @@ func TestReverseCopy(t *testing.T) {
}
func TestRandomHexString(t *testing.T) {
for i := 0; i < 10; i++ {
for range 10 {
s := randomHexString(6, 12)
if len(s) < 6 || len(s) > 12 {
t.Errorf("randomHexString(6,12) = len %d, want 6-12", len(s))
+11 -11
View File
@@ -35,14 +35,14 @@ const (
// Channel connects to the Zalo OA Bot API.
type Channel struct {
*channels.BaseChannel
token string
dmPolicy string
mediaMaxMB int
blockReply *bool
pairingService store.PairingStore
token string
dmPolicy string
mediaMaxMB int
blockReply *bool
pairingService store.PairingStore
pairingDebounce sync.Map // senderID → time.Time
stopCh chan struct{}
client *http.Client
stopCh chan struct{}
client *http.Client
}
// New creates a new Zalo channel.
@@ -379,7 +379,7 @@ type zaloUpdate struct {
Message *zaloMessage `json:"message,omitempty"`
}
func (c *Channel) callAPI(method string, body interface{}) (json.RawMessage, error) {
func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) {
url := fmt.Sprintf("%s/bot%s/%s", apiBase, c.token, method)
var reqBody io.Reader
@@ -436,7 +436,7 @@ func (c *Channel) getMe() (*zaloBotInfo, error) {
}
func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) {
params := map[string]interface{}{
params := map[string]any{
"timeout": timeout,
}
@@ -453,7 +453,7 @@ func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) {
}
func (c *Channel) sendMessage(chatID, text string) error {
params := map[string]interface{}{
params := map[string]any{
"chat_id": chatID,
"text": text,
}
@@ -463,7 +463,7 @@ func (c *Channel) sendMessage(chatID, text string) error {
}
func (c *Channel) sendPhoto(chatID, photoURL, caption string) error {
params := map[string]interface{}{
params := map[string]any{
"chat_id": chatID,
"photo": photoURL,
}
+40 -40
View File
@@ -19,7 +19,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
*f = ss
return nil
}
var raw []interface{}
var raw []any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
@@ -46,22 +46,22 @@ type Config struct {
Gateway GatewayConfig `json:"gateway"`
Tools ToolsConfig `json:"tools"`
Sessions SessionsConfig `json:"sessions"`
Database DatabaseConfig `json:"database,omitempty"`
Tts TtsConfig `json:"tts,omitempty"`
Cron CronConfig `json:"cron,omitempty"`
Telemetry TelemetryConfig `json:"telemetry,omitempty"`
Tailscale TailscaleConfig `json:"tailscale,omitempty"`
Database DatabaseConfig `json:"database"`
Tts TtsConfig `json:"tts"`
Cron CronConfig `json:"cron"`
Telemetry TelemetryConfig `json:"telemetry"`
Tailscale TailscaleConfig `json:"tailscale"`
Bindings []AgentBinding `json:"bindings,omitempty"`
mu sync.RWMutex
mu sync.RWMutex
}
// TailscaleConfig configures the optional Tailscale tsnet listener.
// Requires building with -tags tsnet. Auth key from env only (never persisted).
type TailscaleConfig struct {
Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway")
StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw)
AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only
Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false)
Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway")
StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw)
AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only
Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false)
EnableTLS bool `json:"enable_tls,omitempty"` // use ListenTLS for auto HTTPS certs
}
@@ -86,7 +86,7 @@ type AgentBinding struct {
// BindingMatch specifies what messages this binding applies to.
type BindingMatch struct {
Channel string `json:"channel"` // "telegram", "discord", "slack", etc.
Channel string `json:"channel"` // "telegram", "discord", "slack", etc.
AccountID string `json:"accountId,omitempty"` // bot account ID
Peer *BindingPeer `json:"peer,omitempty"` // specific DM/group
GuildID string `json:"guildId,omitempty"` // Discord guild
@@ -106,21 +106,21 @@ type AgentsConfig struct {
// AgentDefaults are default settings for all agents.
type AgentDefaults struct {
Workspace string `json:"workspace"`
RestrictToWorkspace bool `json:"restrict_to_workspace"`
Provider string `json:"provider"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
MaxToolIterations int `json:"max_tool_iterations"`
ContextWindow int `json:"context_window"`
MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25)
AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined"
Subagents *SubagentsConfig `json:"subagents,omitempty"`
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
Workspace string `json:"workspace"`
RestrictToWorkspace bool `json:"restrict_to_workspace"`
Provider string `json:"provider"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
MaxToolIterations int `json:"max_tool_iterations"`
ContextWindow int `json:"context_window"`
MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25)
AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined"
Subagents *SubagentsConfig `json:"subagents,omitempty"`
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
Memory *MemoryConfig `json:"memory,omitempty"`
Compaction *CompactionConfig `json:"compaction,omitempty"`
ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"`
Compaction *CompactionConfig `json:"compaction,omitempty"`
ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"`
// Bootstrap context truncation limits (matching TS bootstrapMaxChars / bootstrapTotalMaxChars)
BootstrapMaxChars int `json:"bootstrapMaxChars,omitempty"` // per-file max before truncation (default 20000)
BootstrapTotalMaxChars int `json:"bootstrapTotalMaxChars,omitempty"` // total budget across all files (default 24000)
@@ -137,7 +137,7 @@ type CompactionConfig struct {
MaxHistoryShare float64 `json:"maxHistoryShare,omitempty"` // max share of context for history (default 0.75)
MinMessages int `json:"minMessages,omitempty"` // min messages before compaction triggers (default 50)
KeepLastMessages int `json:"keepLastMessages,omitempty"` // messages to keep after compaction (default 4)
MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush
MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush
}
// MemoryFlushConfig configures the pre-compaction memory flush.
@@ -146,20 +146,20 @@ type MemoryFlushConfig struct {
Enabled *bool `json:"enabled,omitempty"` // default true (nil = enabled)
SoftThresholdTokens int `json:"softThresholdTokens,omitempty"` // flush when within N tokens of compaction (default 4000)
Prompt string `json:"prompt,omitempty"` // user prompt for flush turn
SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn
SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn
}
// ContextPruningConfig configures in-memory context pruning of old tool results.
// Matching TS src/agents/pi-extensions/context-pruning/settings.ts.
// Mode "cache-ttl": prune when context exceeds softTrimRatio of context window.
type ContextPruningConfig struct {
Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl"
KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3)
SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3)
HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5)
Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl"
KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3)
SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3)
HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5)
MinPrunableToolChars int `json:"minPrunableToolChars,omitempty"` // min chars in prunable tools before acting (default 50000)
SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"`
HardClear *ContextPruningHardClear `json:"hardClear,omitempty"`
SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"`
HardClear *ContextPruningHardClear `json:"hardClear,omitempty"`
}
// ContextPruningSoftTrim configures how long tool results are trimmed.
@@ -205,9 +205,9 @@ type SandboxConfig struct {
Env map[string]string `json:"env,omitempty"` // extra environment variables
// Enhanced security
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default)
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB)
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default)
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB)
// Pruning (matching TS SandboxPruneSettings)
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
@@ -355,9 +355,9 @@ type AgentSpec struct {
MaxToolIterations int `json:"max_tool_iterations,omitempty"`
ContextWindow int `json:"context_window,omitempty"`
MaxToolCalls int `json:"max_tool_calls,omitempty"` // per-agent override
AgentType string `json:"agent_type,omitempty"` // "open" or "predefined"
Skills []string `json:"skills,omitempty"` // nil = all skills allowed
Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy
AgentType string `json:"agent_type,omitempty"` // "open" or "predefined"
Skills []string `json:"skills,omitempty"` // nil = all skills allowed
Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy
Workspace string `json:"workspace,omitempty"`
Default bool `json:"default,omitempty"`
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
+51 -51
View File
@@ -70,11 +70,11 @@ type TelegramTopicConfig struct {
}
type DiscordConfig struct {
Enabled bool `json:"enabled"`
Token string `json:"token"`
AllowFrom FlexibleStringSlice `json:"allow_from"`
DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled"
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled"
Enabled bool `json:"enabled"`
Token string `json:"token"`
AllowFrom FlexibleStringSlice `json:"allow_from"`
DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled"
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled"
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in groups (default true)
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
@@ -88,22 +88,22 @@ type DiscordConfig struct {
type SlackConfig struct {
Enabled bool `json:"enabled"`
BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token)
AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode)
UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity)
BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token)
AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode)
UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity)
AllowFrom FlexibleStringSlice `json:"allow_from"`
DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled"
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled"
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true)
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false)
GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false)
NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false)
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full"
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled)
ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)
MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB)
DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled"
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled"
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true)
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false)
GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false)
NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false)
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full"
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled)
ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)
MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB)
}
type WhatsAppConfig struct {
@@ -159,7 +159,7 @@ type FeishuConfig struct {
Streaming *bool `json:"streaming,omitempty"` // default true
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full" — typing emoji reactions
HistoryLimit int `json:"history_limit,omitempty"`
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
STTProxyURL string `json:"stt_proxy_url,omitempty"`
STTAPIKey string `json:"stt_api_key,omitempty"`
STTTenantID string `json:"stt_tenant_id,omitempty"`
@@ -169,22 +169,22 @@ type FeishuConfig struct {
// ProvidersConfig maps provider name to its config.
type ProvidersConfig struct {
Anthropic ProviderConfig `json:"anthropic"`
OpenAI ProviderConfig `json:"openai"`
OpenRouter ProviderConfig `json:"openrouter"`
Groq ProviderConfig `json:"groq"`
Gemini ProviderConfig `json:"gemini"`
DeepSeek ProviderConfig `json:"deepseek"`
Mistral ProviderConfig `json:"mistral"`
XAI ProviderConfig `json:"xai"`
MiniMax ProviderConfig `json:"minimax"`
Cohere ProviderConfig `json:"cohere"`
Perplexity ProviderConfig `json:"perplexity"`
DashScope ProviderConfig `json:"dashscope"`
Bailian ProviderConfig `json:"bailian"`
Zai ProviderConfig `json:"zai"`
ZaiCoding ProviderConfig `json:"zai_coding"`
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
Anthropic ProviderConfig `json:"anthropic"`
OpenAI ProviderConfig `json:"openai"`
OpenRouter ProviderConfig `json:"openrouter"`
Groq ProviderConfig `json:"groq"`
Gemini ProviderConfig `json:"gemini"`
DeepSeek ProviderConfig `json:"deepseek"`
Mistral ProviderConfig `json:"mistral"`
XAI ProviderConfig `json:"xai"`
MiniMax ProviderConfig `json:"minimax"`
Cohere ProviderConfig `json:"cohere"`
Perplexity ProviderConfig `json:"perplexity"`
DashScope ProviderConfig `json:"dashscope"`
Bailian ProviderConfig `json:"bailian"`
Zai ProviderConfig `json:"zai"`
ZaiCoding ProviderConfig `json:"zai_coding"`
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
}
// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).
@@ -259,13 +259,13 @@ type GatewayConfig struct {
// ToolsConfig controls tool availability, policy, and web search.
type ToolsConfig struct {
Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full"
Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx")
Deny []string `json:"deny,omitempty"` // global deny list
AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing
ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides
ExecApproval ExecApprovalCfg `json:"execApproval,omitempty"` // exec command approval settings
WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching
Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full"
Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx")
Deny []string `json:"deny,omitempty"` // global deny list
AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing
ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides
ExecApproval ExecApprovalCfg `json:"execApproval"` // exec command approval settings
WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching
Web WebToolsConfig `json:"web"`
Browser BrowserToolConfig `json:"browser"`
RateLimitPerHour int `json:"rate_limit_per_hour,omitempty"` // max tool executions per hour per session (0 = disabled)
@@ -307,9 +307,9 @@ type WebFetchPolicyConfig struct {
// BrowserToolConfig controls the browser automation tool.
type BrowserToolConfig struct {
Enabled bool `json:"enabled"` // enable the browser tool (default false)
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222"
Enabled bool `json:"enabled"` // enable the browser tool (default false)
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222"
}
// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).
@@ -370,10 +370,10 @@ type TtsConfig struct {
Mode string `json:"mode,omitempty"` // "final" (default), "all"
MaxLength int `json:"max_length,omitempty"` // max text length before truncation (default 1500)
TimeoutMs int `json:"timeout_ms,omitempty"` // API timeout in ms (default 30000)
OpenAI TtsOpenAIConfig `json:"openai,omitempty"`
ElevenLabs TtsElevenLabsConfig `json:"elevenlabs,omitempty"`
Edge TtsEdgeConfig `json:"edge,omitempty"`
MiniMax TtsMiniMaxConfig `json:"minimax,omitempty"`
OpenAI TtsOpenAIConfig `json:"openai"`
ElevenLabs TtsElevenLabsConfig `json:"elevenlabs"`
Edge TtsEdgeConfig `json:"edge"`
MiniMax TtsMiniMaxConfig `json:"minimax"`
}
// TtsOpenAIConfig configures the OpenAI TTS provider.
+3 -4
View File
@@ -40,10 +40,9 @@ func ExecuteWithRetry(fn func() (string, error), cfg RetryConfig) (result string
// backoffWithJitter computes delay = min(base * 2^attempt, max) + jitter(±25%).
func backoffWithJitter(base, max time.Duration, attempt int) time.Duration {
delay := base << uint(attempt) // base * 2^attempt
if delay > max {
delay = max
}
delay := min(
// base * 2^attempt
base<<uint(attempt), max)
// Jitter: ±25% of delay
quarter := delay / 4
+6 -7
View File
@@ -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",
@@ -259,14 +259,13 @@ func (cs *Service) UpdateJob(jobID string, patch JobPatch) (*Job, error) {
}
// Status returns the service status.
func (cs *Service) Status() map[string]interface{} {
func (cs *Service) Status() map[string]any {
cs.mu.Lock()
defer cs.mu.Unlock()
return map[string]interface{}{
return map[string]any{
"enabled": cs.running,
"jobs": len(cs.store.Jobs),
"nextWakeAtMs": cs.getNextWakeMS(),
}
}
+9 -9
View File
@@ -31,7 +31,7 @@ type LogTee struct {
// Ring buffer of recent entries for replay on subscribe.
ringMu sync.RWMutex
ring []map[string]interface{}
ring []map[string]any
ringPos int
ringFul bool
}
@@ -48,7 +48,7 @@ func NewLogTee(inner slog.Handler) *LogTee {
return &LogTee{
inner: inner,
clients: make(map[string]*logSubscriber),
ring: make([]map[string]interface{}, ringBufferSize),
ring: make([]map[string]any, ringBufferSize),
}
}
@@ -109,14 +109,14 @@ func (t *LogTee) Handle(ctx context.Context, r slog.Record) error {
}
// buildEntry creates the WS payload from a log record, redacting sensitive attrs.
func (t *LogTee) buildEntry(r slog.Record) map[string]interface{} {
entry := map[string]interface{}{
func (t *LogTee) buildEntry(r slog.Record) map[string]any {
entry := map[string]any{
"timestamp": r.Time.UnixMilli(),
"level": levelName(r.Level),
"message": r.Message,
}
attrs := map[string]interface{}{}
attrs := map[string]any{}
r.Attrs(func(a slog.Attr) bool {
key := a.Key
val := a.Value.String()
@@ -168,10 +168,10 @@ func (t *LogTee) Subscribe(client *Client, level slog.Level) {
// Replay ring buffer entries at the requested level.
t.ringMu.RLock()
var entries []map[string]interface{}
var entries []map[string]any
if t.ringFul {
// Buffer is full — read from ringPos (oldest) to ringPos-1 (newest).
for i := 0; i < ringBufferSize; i++ {
for i := range ringBufferSize {
idx := (t.ringPos + i) % ringBufferSize
e := t.ring[idx]
if e != nil && logLevelValue(e["level"]) >= level {
@@ -194,7 +194,7 @@ func (t *LogTee) Subscribe(client *Client, level slog.Level) {
}
// Send sentinel so the client knows tailing started.
client.SendEvent(*protocol.NewEvent("log", map[string]interface{}{
client.SendEvent(*protocol.NewEvent("log", map[string]any{
"timestamp": time.Now().UnixMilli(),
"level": "info",
"message": "Log tailing started",
@@ -223,7 +223,7 @@ func levelName(l slog.Level) string {
}
// logLevelValue converts a level name string back to slog.Level for filtering.
func logLevelValue(v interface{}) slog.Level {
func logLevelValue(v any) slog.Level {
s, _ := v.(string)
switch s {
case "error":
+4 -4
View File
@@ -88,7 +88,7 @@ func (m *AgentLinksMethods) handleList(ctx context.Context, client *gateway.Clie
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"links": links,
"count": len(links),
}))
@@ -182,7 +182,7 @@ func (m *AgentLinksMethods) handleCreate(ctx context.Context, client *gateway.Cl
}
m.emitTeamCacheInvalidate()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"link": link,
}))
@@ -281,7 +281,7 @@ func (m *AgentLinksMethods) handleUpdate(ctx context.Context, client *gateway.Cl
m.invalidateLinkAgents(ctx, linkID)
m.emitTeamCacheInvalidate()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit agent_link.updated event
if m.msgBus != nil {
@@ -350,7 +350,7 @@ func (m *AgentLinksMethods) handleDelete(ctx context.Context, client *gateway.Cl
}
m.emitTeamCacheInvalidate()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit agent_link.deleted event
if m.msgBus != nil && link != nil {
+8 -12
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"log/slog"
"slices"
"github.com/nextlevelbuilder/goclaw/internal/agent"
"github.com/nextlevelbuilder/goclaw/internal/config"
@@ -34,12 +35,7 @@ func (m *AgentsMethods) isOwnerUser(userID string) bool {
if userID == "" {
return false
}
for _, id := range m.cfg.Gateway.OwnerIDs {
if id == userID {
return true
}
}
return false
return slices.Contains(m.cfg.Gateway.OwnerIDs, userID)
}
func (m *AgentsMethods) Register(router *gateway.MethodRouter) {
@@ -74,7 +70,7 @@ func (m *AgentsMethods) handleAgent(_ context.Context, client *gateway.Client, r
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"id": loop.ID(),
"isRunning": loop.IsRunning(),
}))
@@ -96,7 +92,7 @@ func (m *AgentsMethods) handleAgentWait(_ context.Context, client *gateway.Clien
}
// Return current status (blocking wait is a future enhancement).
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"id": loop.ID(),
"status": "idle",
}))
@@ -124,12 +120,12 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
return
}
infos := make([]map[string]interface{}, 0, len(agents))
infos := make([]map[string]any, 0, len(agents))
for _, a := range agents {
if a.Status != store.AgentStatusActive {
continue
}
infos = append(infos, map[string]interface{}{
infos = append(infos, map[string]any{
"id": a.AgentKey,
"name": a.DisplayName,
"model": a.Model,
@@ -139,7 +135,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
"isRunning": m.agents.IsRunning(a.AgentKey),
})
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agents": infos,
}))
return
@@ -147,7 +143,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
// Fallback: return router-cached agents.
infos := m.agents.ListInfo()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agents": infos,
}))
}
+1 -1
View File
@@ -157,7 +157,7 @@ func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client
os.MkdirAll(ws, 0755)
bootstrap.EnsureWorkspaceFiles(ws)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"agentId": agentID,
"name": params.Name,
+1 -1
View File
@@ -92,7 +92,7 @@ func (m *AgentsMethods) handleDelete(ctx context.Context, client *gateway.Client
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"agentId": params.AgentID,
"removedBindings": removedBindings,
+22 -26
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
"github.com/nextlevelbuilder/goclaw/internal/config"
@@ -55,23 +56,23 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
dbMap[f.FileName] = f
}
files := make([]map[string]interface{}, 0, len(allowedAgentFiles))
files := make([]map[string]any, 0, len(allowedAgentFiles))
for _, name := range allowedAgentFiles {
if f, ok := dbMap[name]; ok {
files = append(files, map[string]interface{}{
files = append(files, map[string]any{
"name": name,
"missing": false,
"size": len(f.Content),
})
} else {
files = append(files, map[string]interface{}{
files = append(files, map[string]any{
"name": name,
"missing": true,
})
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"files": files,
}))
@@ -80,19 +81,19 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
// --- Fallback: filesystem ---
ws := m.resolveWorkspace(params.AgentID)
files := make([]map[string]interface{}, 0, len(allowedAgentFiles))
files := make([]map[string]any, 0, len(allowedAgentFiles))
for _, name := range allowedAgentFiles {
p := filepath.Join(ws, name)
info, err := os.Stat(p)
if err != nil {
files = append(files, map[string]interface{}{
files = append(files, map[string]any{
"name": name,
"path": p,
"missing": true,
})
} else {
files = append(files, map[string]interface{}{
files = append(files, map[string]any{
"name": name,
"path": p,
"missing": false,
@@ -102,7 +103,7 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"files": files,
@@ -150,9 +151,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
for _, f := range dbFiles {
if f.FileName == params.Name {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"missing": false,
"size": len(f.Content),
@@ -164,9 +165,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
}
// File not found in DB
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"missing": true,
},
@@ -180,10 +181,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
info, err := os.Stat(p)
if err != nil {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": true,
@@ -193,10 +194,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
}
content, _ := os.ReadFile(p)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": false,
@@ -253,9 +254,9 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie
m.interceptor.InvalidateAgent(ag.ID)
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"missing": false,
"size": len(params.Content),
@@ -276,10 +277,10 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie
}
info, _ := os.Stat(p)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]interface{}{
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": false,
@@ -300,10 +301,5 @@ func (m *AgentsMethods) resolveWorkspace(agentID string) string {
}
func isAllowedFile(name string) bool {
for _, f := range allowedAgentFiles {
if f == name {
return true
}
}
return false
return slices.Contains(allowedAgentFiles, name)
}
+2 -2
View File
@@ -36,7 +36,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli
}
}
result := map[string]interface{}{
result := map[string]any{
"agentId": params.AgentID,
}
@@ -106,7 +106,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli
// parseIdentityContent parses IDENTITY.md content string and extracts Key: Value fields.
func parseIdentityContent(content string) map[string]string {
result := make(map[string]string)
for _, line := range strings.Split(content, "\n") {
for line := range strings.SplitSeq(content, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") || line == "" {
continue
+1 -1
View File
@@ -155,7 +155,7 @@ func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"agentId": params.AgentID,
}))
+21 -21
View File
@@ -54,12 +54,12 @@ func (m *ChannelInstancesMethods) handleList(ctx context.Context, client *gatewa
}
// Mask credentials in response — never expose secrets via WS.
result := make([]map[string]interface{}, 0, len(instances))
result := make([]map[string]any, 0, len(instances))
for _, inst := range instances {
result = append(result, maskInstance(inst))
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"instances": result,
}))
}
@@ -160,7 +160,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
return
}
var updates map[string]interface{}
var updates map[string]any
if err := json.Unmarshal(params.Updates, &updates); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidUpdates)))
return
@@ -173,7 +173,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
}
m.emitCacheInvalidate()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "updated"}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "updated"}))
}
func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
@@ -209,31 +209,31 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate
}
m.emitCacheInvalidate()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "deleted"}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "deleted"}))
}
// maskInstance returns a map representation with credentials masked.
func maskInstance(inst store.ChannelInstanceData) map[string]interface{} {
result := map[string]interface{}{
"id": inst.ID,
"name": inst.Name,
"display_name": inst.DisplayName,
"channel_type": inst.ChannelType,
"agent_id": inst.AgentID,
"config": inst.Config,
"enabled": inst.Enabled,
"is_default": store.IsDefaultChannelInstance(inst.Name),
"has_credentials": len(inst.Credentials) > 0,
"created_by": inst.CreatedBy,
"created_at": inst.CreatedAt,
"updated_at": inst.UpdatedAt,
func maskInstance(inst store.ChannelInstanceData) map[string]any {
result := map[string]any{
"id": inst.ID,
"name": inst.Name,
"display_name": inst.DisplayName,
"channel_type": inst.ChannelType,
"agent_id": inst.AgentID,
"config": inst.Config,
"enabled": inst.Enabled,
"is_default": store.IsDefaultChannelInstance(inst.Name),
"has_credentials": len(inst.Credentials) > 0,
"created_by": inst.CreatedBy,
"created_at": inst.CreatedAt,
"updated_at": inst.UpdatedAt,
}
// Mask credentials: show keys with "***" values
if len(inst.Credentials) > 0 {
var raw map[string]interface{}
var raw map[string]any
if json.Unmarshal(inst.Credentials, &raw) == nil {
masked := make(map[string]interface{}, len(raw))
masked := make(map[string]any, len(raw))
for k := range raw {
masked[k] = "***"
}
+2 -2
View File
@@ -28,7 +28,7 @@ func (m *ChannelsMethods) Register(router *gateway.MethodRouter) {
func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
enabled := m.manager.GetEnabledChannels()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"channels": enabled,
}))
}
@@ -36,7 +36,7 @@ func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client,
func (m *ChannelsMethods) handleStatus(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
status := m.manager.GetStatus()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"channels": status,
}))
}
+4 -4
View File
@@ -155,7 +155,7 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
return
}
resp := map[string]interface{}{
resp := map[string]any{
"runId": result.RunID,
"content": result.Content,
"usage": result.Usage,
@@ -191,7 +191,7 @@ func (m *ChatMethods) handleHistory(ctx context.Context, client *gateway.Client,
history := m.sessions.GetHistory(sessionKey)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"messages": history,
}))
}
@@ -237,7 +237,7 @@ func (m *ChatMethods) handleInject(ctx context.Context, client *gateway.Client,
Content: text,
})
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"messageId": messageID,
}))
@@ -281,7 +281,7 @@ func (m *ChatMethods) handleAbort(ctx context.Context, client *gateway.Client, r
abortedIDs = m.agents.AbortRunsForSession(params.SessionKey)
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"aborted": len(abortedIDs) > 0,
"runIds": abortedIDs,
+12 -12
View File
@@ -36,7 +36,7 @@ func (m *ConfigMethods) Register(router *gateway.MethodRouter) {
}
func (m *ConfigMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"config": m.cfg.MaskedCopy(),
"hash": m.cfg.Hash(),
"path": m.cfgPath,
@@ -93,7 +93,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
m.cfg.ApplyEnvOverrides()
m.broadcastChanged()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"path": m.cfgPath,
"config": m.cfg.MaskedCopy(),
@@ -165,7 +165,7 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client,
m.cfg.ApplyEnvOverrides()
m.broadcastChanged()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"path": m.cfgPath,
"config": m.cfg.MaskedCopy(),
@@ -184,37 +184,37 @@ func (m *ConfigMethods) broadcastChanged() {
// handleSchema returns the config JSON schema for UI form generation.
// Matching TS config.schema (src/gateway/server-methods/config.ts:276-289).
func (m *ConfigMethods) handleSchema(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
schema := map[string]interface{}{
schema := map[string]any{
"type": "object",
"properties": map[string]interface{}{
"agents": map[string]interface{}{
"properties": map[string]any{
"agents": map[string]any{
"type": "object",
"description": "Agent configuration (defaults + per-agent overrides)",
},
"channels": map[string]interface{}{
"channels": map[string]any{
"type": "object",
"description": "Channel configuration (telegram, discord, slack, etc.)",
},
"providers": map[string]interface{}{
"providers": map[string]any{
"type": "object",
"description": "AI provider API keys and settings",
},
"gateway": map[string]interface{}{
"gateway": map[string]any{
"type": "object",
"description": "Gateway server settings (host, port, token)",
},
"tools": map[string]interface{}{
"tools": map[string]any{
"type": "object",
"description": "Tool configuration (browser, exec, web search)",
},
"sessions": map[string]interface{}{
"sessions": map[string]any{
"type": "object",
"description": "Session storage configuration",
},
},
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"json": schema,
}))
}
+9 -9
View File
@@ -44,7 +44,7 @@ func (m *CronMethods) handleList(_ context.Context, client *gateway.Client, req
jobs := m.service.ListJobs(params.IncludeDisabled, "", "")
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"jobs": jobs,
"status": m.service.Status(),
}))
@@ -84,7 +84,7 @@ func (m *CronMethods) handleCreate(ctx context.Context, client *gateway.Client,
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"job": job,
}))
}
@@ -108,7 +108,7 @@ func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client,
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"deleted": true,
}))
}
@@ -133,7 +133,7 @@ func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client,
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"jobId": params.JobID,
"enabled": params.Enabled,
}))
@@ -146,8 +146,8 @@ func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, re
func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params struct {
JobID string `json:"jobId"`
ID string `json:"id"` // alias (matching TS)
JobID string `json:"jobId"`
ID string `json:"id"` // alias (matching TS)
Patch store.CronJobPatch `json:"patch"`
}
if req.Params != nil {
@@ -169,7 +169,7 @@ func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client,
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"job": job,
}))
}
@@ -204,7 +204,7 @@ func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req
}
// Respond immediately — job execution happens in background
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"ran": true,
}))
@@ -233,7 +233,7 @@ func (m *CronMethods) handleRuns(_ context.Context, client *gateway.Client, req
}
entries, total := m.service.GetRunLog(jobID, params.Limit, params.Offset)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"entries": entries,
"total": total,
}))
+1 -1
View File
@@ -87,7 +87,7 @@ func (m *DelegationsMethods) handleList(ctx context.Context, client *gateway.Cli
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"records": records,
"total": total,
}))
+4 -4
View File
@@ -28,7 +28,7 @@ func (m *ExecApprovalMethods) Register(router *gateway.MethodRouter) {
func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
if m.manager == nil {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"pending": []any{},
}))
return
@@ -52,7 +52,7 @@ func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Clie
})
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"pending": items,
}))
}
@@ -87,7 +87,7 @@ func (m *ExecApprovalMethods) handleApprove(ctx context.Context, client *gateway
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"resolved": true,
"decision": string(decision),
}))
@@ -117,7 +117,7 @@ func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Cl
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"resolved": true,
"decision": "deny",
}))
+2 -2
View File
@@ -38,13 +38,13 @@ func (m *LogsMethods) handleTail(ctx context.Context, client *gateway.Client, re
case "start":
level := parseLogLevel(params.Level)
m.logTee.Subscribe(client, level)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "tailing",
"level": params.Level,
}))
case "stop":
m.logTee.Unsubscribe(client.ID())
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "stopped",
}))
default:
+8 -8
View File
@@ -74,7 +74,7 @@ func (m *PairingMethods) handleRequest(ctx context.Context, client *gateway.Clie
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"code": code,
}))
}
@@ -113,7 +113,7 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "approved"}))
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"paired": paired,
}))
}
@@ -141,7 +141,7 @@ func (m *PairingMethods) handleDeny(ctx context.Context, client *gateway.Client,
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "denied"}))
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"denied": true,
}))
}
@@ -150,7 +150,7 @@ func (m *PairingMethods) handleList(_ context.Context, client *gateway.Client, r
pending := m.service.ListPending()
paired := m.service.ListPaired()
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"pending": pending,
"paired": paired,
}))
@@ -191,7 +191,7 @@ func (m *PairingMethods) handleRevoke(ctx context.Context, client *gateway.Clien
})
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"revoked": true,
}))
}
@@ -213,7 +213,7 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client
}
if m.service.IsPaired(params.SenderID, "browser") {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "approved",
}))
return
@@ -223,14 +223,14 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client
pending := m.service.ListPending()
for _, p := range pending {
if p.SenderID == params.SenderID && p.Channel == "browser" {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "pending",
}))
return
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "expired",
}))
}
+1 -1
View File
@@ -55,7 +55,7 @@ func (m *SendMethods) handleSend(ctx context.Context, client *gateway.Client, re
Content: params.Message,
})
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"channel": params.Channel,
"to": params.To,
+5 -5
View File
@@ -48,7 +48,7 @@ func (m *SessionsMethods) handleList(_ context.Context, client *gateway.Client,
Limit: params.Limit,
Offset: params.Offset,
})
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"sessions": result.Sessions,
"total": result.Total,
"limit": params.Limit,
@@ -71,7 +71,7 @@ func (m *SessionsMethods) handlePreview(ctx context.Context, client *gateway.Cli
history := m.sessions.GetHistory(params.Key)
summary := m.sessions.GetSummary(params.Key)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"key": params.Key,
"messages": history,
"summary": summary,
@@ -116,7 +116,7 @@ func (m *SessionsMethods) handlePatch(ctx context.Context, client *gateway.Clien
// Save changes to DB
m.sessions.Save(params.Key)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"key": params.Key,
}))
@@ -135,7 +135,7 @@ func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Clie
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
}))
}
@@ -150,7 +150,7 @@ func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Clien
m.sessions.Reset(params.Key)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
}))
}
+8 -8
View File
@@ -30,9 +30,9 @@ func (m *SkillsMethods) Register(router *gateway.MethodRouter) {
func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
allSkills := m.store.ListSkills()
result := make([]map[string]interface{}, 0, len(allSkills))
result := make([]map[string]any, 0, len(allSkills))
for _, s := range allSkills {
entry := map[string]interface{}{
entry := map[string]any{
"name": s.Name,
"slug": s.Slug,
"description": s.Description,
@@ -51,7 +51,7 @@ func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, re
result = append(result, entry)
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"skills": result,
}))
}
@@ -77,7 +77,7 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r
content, _ := m.store.LoadSkill(params.Name)
resp := map[string]interface{}{
resp := map[string]any{
"name": info.Name,
"slug": info.Slug,
"description": info.Description,
@@ -99,15 +99,15 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r
// skillUpdater is an optional interface for stores that support skill updates (e.g. PGSkillStore).
type skillUpdater interface {
UpdateSkill(id uuid.UUID, updates map[string]interface{}) error
UpdateSkill(id uuid.UUID, updates map[string]any) error
}
func (m *SkillsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params struct {
Name string `json:"name"`
ID string `json:"id"`
Updates map[string]interface{} `json:"updates"`
Name string `json:"name"`
ID string `json:"id"`
Updates map[string]any `json:"updates"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
+2 -2
View File
@@ -64,7 +64,7 @@ func (m *TeamsMethods) handleList(ctx context.Context, client *gateway.Client, r
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"teams": teams,
"count": len(teams),
}))
@@ -164,7 +164,7 @@ func (m *TeamsMethods) handleCreate(ctx context.Context, client *gateway.Client,
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"team": team,
}))
+7 -7
View File
@@ -55,7 +55,7 @@ func (m *TeamsMethods) handleGet(ctx context.Context, client *gateway.Client, re
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"team": team,
"members": members,
}))
@@ -107,7 +107,7 @@ func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client,
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit team.deleted event
if m.msgBus != nil && team != nil {
@@ -157,7 +157,7 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"tasks": tasks,
"count": len(tasks),
}))
@@ -166,8 +166,8 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
// --- Update (settings) ---
type teamsUpdateParams struct {
TeamID string `json:"teamId"`
Settings map[string]interface{} `json:"settings"`
TeamID string `json:"teamId"`
Settings map[string]any `json:"settings"`
}
func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
@@ -225,7 +225,7 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client,
m.invalidateTeamCaches(ctx, teamID)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit team.updated event
if m.msgBus != nil {
@@ -280,7 +280,7 @@ func (m *TeamsMethods) handleKnownUsers(ctx context.Context, client *gateway.Cli
return
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"users": users,
}))
}
+2 -2
View File
@@ -81,7 +81,7 @@ func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Clie
// Invalidate caches for all team members
m.invalidateTeamCaches(ctx, teamID)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit team.member.added event
if m.msgBus != nil {
@@ -170,7 +170,7 @@ func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.C
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
// Emit team.member.removed event
if m.msgBus != nil && removedAgent != nil {
+4 -10
View File
@@ -83,17 +83,11 @@ func (m *UsageMethods) handleGet(_ context.Context, client *gateway.Client, req
total := len(records)
// Apply offset + limit
offset := params.Offset
if offset > total {
offset = total
}
end := offset + params.Limit
if end > total {
end = total
}
offset := min(params.Offset, total)
end := min(offset+params.Limit, total)
records = records[offset:end]
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"records": records,
"total": total,
"limit": params.Limit,
@@ -132,7 +126,7 @@ func (m *UsageMethods) handleSummary(_ context.Context, client *gateway.Client,
totalRecords++
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"byAgent": byAgent,
"totalRecords": totalRecords,
}))
+13 -14
View File
@@ -142,12 +142,12 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
client.pairingCode = code
client.pairingPending = true
// Not authenticated — can only call browser.pairing.status
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"protocol": protocol.ProtocolVersion,
"status": "pending_pairing",
"pairing_code": code,
"sender_id": client.id,
"server": map[string]interface{}{
"server": map[string]any{
"name": "goclaw",
"version": "0.2.0",
},
@@ -164,11 +164,11 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
}
func (r *MethodRouter) sendConnectResponse(client *Client, reqID string) {
client.SendResponse(protocol.NewOKResponse(reqID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(reqID, map[string]any{
"protocol": protocol.ProtocolVersion,
"role": string(client.role),
"user_id": client.userID,
"server": map[string]interface{}{
"server": map[string]any{
"name": "goclaw",
"version": "0.2.0",
},
@@ -219,14 +219,14 @@ func (r *MethodRouter) handleHealth(ctx context.Context, client *Client, req *pr
toolCount = s.tools.Count()
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
"status": "ok",
"version": s.version,
"uptime": uptimeMs,
"mode": mode,
"database": dbStatus,
"tools": toolCount,
"clients": clientList,
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"status": "ok",
"version": s.version,
"uptime": uptimeMs,
"mode": mode,
"database": dbStatus,
"tools": toolCount,
"clients": clientList,
"currentId": client.ID(),
}))
}
@@ -248,11 +248,10 @@ func (r *MethodRouter) handleStatus(ctx context.Context, client *Client, req *pr
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agents": agents,
"agentTotal": agentTotal,
"clients": len(r.server.clients),
"sessions": sessionCount,
}))
}
+3 -3
View File
@@ -2,6 +2,8 @@
// user-facing error messages. Messages use fmt.Sprintf-style templates.
package i18n
import "maps"
import "fmt"
// Supported locales.
@@ -21,9 +23,7 @@ func register(locale string, msgs map[string]string) {
if catalogs[locale] == nil {
catalogs[locale] = make(map[string]string, len(msgs))
}
for k, v := range msgs {
catalogs[locale][k] = v
}
maps.Copy(catalogs[locale], msgs)
}
// T returns a localized message for the given key.
+1 -1
View File
@@ -46,7 +46,7 @@ func (e *Extractor) Extract(ctx context.Context, text string) (*ExtractionResult
{Role: "user", Content: text},
},
Model: e.model,
Options: map[string]interface{}{
Options: map[string]any{
"max_tokens": 8192,
"temperature": 0.0,
},
+7 -7
View File
@@ -19,7 +19,7 @@ type BridgeTool struct {
toolName string // original MCP tool name
registeredName string // may include prefix: "{prefix}__{toolName}"
description string
inputSchema map[string]interface{} // JSON Schema for parameters
inputSchema map[string]any // JSON Schema for parameters
client *mcpclient.Client
timeoutSec int
connected *atomic.Bool
@@ -73,9 +73,9 @@ func ensureMCPPrefix(prefix, serverName string) string {
return prefix
}
func (t *BridgeTool) Name() string { return t.registeredName }
func (t *BridgeTool) Description() string { return t.description }
func (t *BridgeTool) Parameters() map[string]interface{} { return t.inputSchema }
func (t *BridgeTool) Name() string { return t.registeredName }
func (t *BridgeTool) Description() string { return t.description }
func (t *BridgeTool) Parameters() map[string]any { return t.inputSchema }
// ServerName returns the name of the MCP server this tool belongs to.
func (t *BridgeTool) ServerName() string { return t.serverName }
@@ -83,7 +83,7 @@ func (t *BridgeTool) ServerName() string { return t.serverName }
// OriginalName returns the original MCP tool name (without prefix).
func (t *BridgeTool) OriginalName() string { return t.toolName }
func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result {
func (t *BridgeTool) Execute(ctx context.Context, args map[string]any) *tools.Result {
if !t.connected.Load() {
return tools.ErrorResult(fmt.Sprintf("MCP server %q is disconnected", t.serverName))
}
@@ -113,8 +113,8 @@ func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) *
}
// inputSchemaToMap converts mcp.ToolInputSchema to the map format expected by tools.Tool.Parameters().
func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]interface{} {
m := map[string]interface{}{
func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]any {
m := map[string]any{
"type": schema.Type,
}
if schema.Type == "" {
+1 -1
View File
@@ -3,6 +3,6 @@ package mcp
import "encoding/json"
// jsonUnmarshal is a thin wrapper to keep the main files import-clean.
func jsonUnmarshal(data []byte, v interface{}) error {
func jsonUnmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
+1 -4
View File
@@ -269,10 +269,7 @@ func (m *Manager) tryReconnect(ctx context.Context, ss *serverState) {
attempt := ss.reconnAttempts
ss.mu.Unlock()
backoff := initialBackoff * time.Duration(1<<(attempt-1))
if backoff > maxBackoff {
backoff = maxBackoff
}
backoff := min(initialBackoff*time.Duration(1<<(attempt-1)), maxBackoff)
slog.Info("mcp.server.reconnecting",
"server", ss.name,
+6 -4
View File
@@ -1,5 +1,7 @@
package mcp
import "strings"
func mapToEnvSlice(env map[string]string) []string {
if len(env) == 0 {
return nil
@@ -23,14 +25,14 @@ func toSet(items []string) map[string]struct{} {
}
func joinErrors(errs []string) string {
result := ""
var result strings.Builder
for i, e := range errs {
if i > 0 {
result += "; "
result.WriteString("; ")
}
result += e
result.WriteString(e)
}
return result
return result.String()
}
// jsonBytesToStringSlice converts JSONB []byte to []string. Returns nil on error.
+7 -7
View File
@@ -47,15 +47,15 @@ func (t *MCPToolSearchTool) Description() string {
"Discovered tools become immediately available for use."
}
func (t *MCPToolSearchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *MCPToolSearchTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "English keywords describing the operation you need (e.g. 'create github issue', 'query postgres', 'send slack message')",
},
"max_results": map[string]interface{}{
"max_results": map[string]any{
"type": "integer",
"description": "Maximum number of tools to return (default: 5)",
},
@@ -64,7 +64,7 @@ func (t *MCPToolSearchTool) Parameters() map[string]interface{} {
}
}
func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result {
func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]any) *tools.Result {
query, _ := args["query"].(string)
if query == "" {
return tools.ErrorResult("query parameter is required")
@@ -91,7 +91,7 @@ func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interfa
}
t.manager.ActivateTools(names)
data, _ := json.MarshalIndent(map[string]interface{}{
data, _ := json.MarshalIndent(map[string]any{
"tools": results,
"count": len(results),
}, "", " ")
+1 -1
View File
@@ -128,7 +128,7 @@ func (p *OpenAIEmbeddingProvider) Name() string { return p.name }
func (p *OpenAIEmbeddingProvider) Model() string { return p.model }
func (p *OpenAIEmbeddingProvider) Embed(ctx context.Context, texts []string) ([][]float32, error) {
reqBody := map[string]interface{}{
reqBody := map[string]any{
"input": texts,
"model": p.model,
}
+7 -21
View File
@@ -12,6 +12,7 @@
package permissions
import (
"slices"
"strings"
"sync"
@@ -90,25 +91,15 @@ func (pe *PolicyEngine) CanAccessWithScopes(scopes []Scope, method string) bool
// RoleFromScopes determines the effective role from a set of scopes.
func RoleFromScopes(scopes []Scope) Role {
for _, s := range scopes {
if s == ScopeAdmin {
return RoleAdmin
}
}
hasWrite := false
for _, s := range scopes {
if s == ScopeWrite {
hasWrite = true
break
}
if slices.Contains(scopes, ScopeAdmin) {
return RoleAdmin
}
hasWrite := slices.Contains(scopes, ScopeWrite)
if hasWrite {
return RoleOperator
}
for _, s := range scopes {
if s == ScopeRead {
return RoleViewer
}
if slices.Contains(scopes, ScopeRead) {
return RoleViewer
}
return RoleViewer
}
@@ -166,12 +157,7 @@ func isAdminMethod(method string) bool {
protocol.MethodTeamsDelete,
protocol.MethodTeamsTaskList,
}
for _, m := range adminMethods {
if method == m {
return true
}
}
return false
return slices.Contains(adminMethods, method)
}
func isWriteMethod(method string) bool {
+13 -13
View File
@@ -12,9 +12,9 @@ import (
)
const (
defaultClaudeModel = "claude-sonnet-4-5-20250929"
anthropicAPIBase = "https://api.anthropic.com/v1"
anthropicAPIVersion = "2023-06-01"
defaultClaudeModel = "claude-sonnet-4-5-20250929"
anthropicAPIBase = "https://api.anthropic.com/v1"
anthropicAPIVersion = "2023-06-01"
)
// AnthropicProvider implements Provider using the Anthropic Claude API via net/http.
@@ -55,9 +55,9 @@ func WithAnthropicBaseURL(baseURL string) AnthropicOption {
}
}
func (p *AnthropicProvider) Name() string { return "anthropic" }
func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel }
func (p *AnthropicProvider) SupportsThinking() bool { return true }
func (p *AnthropicProvider) Name() string { return "anthropic" }
func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel }
func (p *AnthropicProvider) SupportsThinking() bool { return true }
func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
model := req.Model
@@ -83,7 +83,7 @@ func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatRes
})
}
func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
func (p *AnthropicProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("anthropic: marshal request: %w", err)
@@ -99,7 +99,7 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io
httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
// Add beta header for interleaved thinking when thinking is enabled
if bodyMap, ok := body.(map[string]interface{}); ok {
if bodyMap, ok := body.(map[string]any); ok {
if _, hasThinking := bodyMap["thinking"]; hasThinking {
httpReq.Header.Set("anthropic-beta", "interleaved-thinking-2025-05-14")
}
@@ -138,7 +138,7 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse
case "redacted_thinking":
// Encrypted thinking — cannot display but must preserve for passback
case "tool_use":
args := make(map[string]interface{})
args := make(map[string]any)
_ = json.Unmarshal(block.Input, &args)
result.ToolCalls = append(result.ToolCalls, ToolCall{
ID: block.ID,
@@ -182,8 +182,8 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse
type anthropicResponse struct {
Content []anthropicContentBlock `json:"content"`
StopReason string `json:"stop_reason"`
Usage anthropicUsage `json:"usage"`
StopReason string `json:"stop_reason"`
Usage anthropicUsage `json:"usage"`
}
type anthropicContentBlock struct {
@@ -221,8 +221,8 @@ type anthropicContentBlockDeltaEvent struct {
Delta struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Thinking string `json:"thinking,omitempty"` // for thinking_delta
Signature string `json:"signature,omitempty"` // for signature_delta
Thinking string `json:"thinking,omitempty"` // for thinking_delta
Signature string `json:"signature,omitempty"` // for signature_delta
PartialJSON string `json:"partial_json,omitempty"`
} `json:"delta"`
}
+27 -27
View File
@@ -7,7 +7,7 @@ import "encoding/json"
func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse, toolCallJSON map[int]string, _ int) json.RawMessage {
switch blockType {
case "thinking":
block := map[string]interface{}{
block := map[string]any{
"type": "thinking",
"thinking": result.Thinking,
}
@@ -15,7 +15,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
return b
}
case "text":
block := map[string]interface{}{
block := map[string]any{
"type": "text",
"text": result.Content,
}
@@ -26,13 +26,13 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
if len(result.ToolCalls) > 0 {
tc := result.ToolCalls[len(result.ToolCalls)-1]
// Parse accumulated JSON for this tool call
args := make(map[string]interface{})
args := make(map[string]any)
for i, rawJSON := range toolCallJSON {
if i == len(result.ToolCalls)-1 && rawJSON != "" {
_ = json.Unmarshal([]byte(rawJSON), &args)
}
}
block := map[string]interface{}{
block := map[string]any{
"type": "tool_use",
"id": tc.ID,
"name": tc.Name,
@@ -44,7 +44,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
}
case "redacted_thinking":
// Pass through as-is (we don't have the encrypted data in streaming)
block := map[string]interface{}{
block := map[string]any{
"type": "redacted_thinking",
}
if b, err := json.Marshal(block); err == nil {
@@ -54,26 +54,26 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
return nil
}
func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} {
func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any {
// Separate system messages and build conversation messages
var systemBlocks []map[string]interface{}
var messages []map[string]interface{}
var systemBlocks []map[string]any
var messages []map[string]any
for _, msg := range req.Messages {
switch msg.Role {
case "system":
systemBlocks = append(systemBlocks, map[string]interface{}{
systemBlocks = append(systemBlocks, map[string]any{
"type": "text",
"text": msg.Content,
})
case "user":
if len(msg.Images) > 0 {
var blocks []map[string]interface{}
var blocks []map[string]any
for _, img := range msg.Images {
blocks = append(blocks, map[string]interface{}{
blocks = append(blocks, map[string]any{
"type": "image",
"source": map[string]interface{}{
"source": map[string]any{
"type": "base64",
"media_type": img.MimeType,
"data": img.Data,
@@ -81,17 +81,17 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
})
}
if msg.Content != "" {
blocks = append(blocks, map[string]interface{}{
blocks = append(blocks, map[string]any{
"type": "text",
"text": msg.Content,
})
}
messages = append(messages, map[string]interface{}{
messages = append(messages, map[string]any{
"role": "user",
"content": blocks,
})
} else {
messages = append(messages, map[string]interface{}{
messages = append(messages, map[string]any{
"role": "user",
"content": msg.Content,
})
@@ -103,7 +103,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
if msg.RawAssistantContent != nil {
var rawBlocks []json.RawMessage
if json.Unmarshal(msg.RawAssistantContent, &rawBlocks) == nil && len(rawBlocks) > 0 {
messages = append(messages, map[string]interface{}{
messages = append(messages, map[string]any{
"role": "assistant",
"content": rawBlocks,
})
@@ -111,30 +111,30 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
}
}
var blocks []map[string]interface{}
var blocks []map[string]any
if msg.Content != "" {
blocks = append(blocks, map[string]interface{}{
blocks = append(blocks, map[string]any{
"type": "text",
"text": msg.Content,
})
}
for _, tc := range msg.ToolCalls {
blocks = append(blocks, map[string]interface{}{
blocks = append(blocks, map[string]any{
"type": "tool_use",
"id": tc.ID,
"name": tc.Name,
"input": tc.Arguments,
})
}
messages = append(messages, map[string]interface{}{
messages = append(messages, map[string]any{
"role": "assistant",
"content": blocks,
})
case "tool":
messages = append(messages, map[string]interface{}{
messages = append(messages, map[string]any{
"role": "user",
"content": []map[string]interface{}{
"content": []map[string]any{
{
"type": "tool_result",
"tool_use_id": msg.ToolCallID,
@@ -145,11 +145,11 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
}
}
body := map[string]interface{}{
body := map[string]any{
"model": model,
"max_tokens": 4096,
"messages": messages,
"cache_control": map[string]interface{}{"type": "ephemeral"},
"cache_control": map[string]any{"type": "ephemeral"},
}
if stream {
@@ -162,10 +162,10 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
// Translate tools to Anthropic format
if len(req.Tools) > 0 {
var tools []map[string]interface{}
var tools []map[string]any
for _, t := range req.Tools {
cleanedParams := CleanSchemaForProvider("anthropic", t.Function.Parameters)
tool := map[string]interface{}{
tool := map[string]any{
"name": t.Function.Name,
"description": t.Function.Description,
"input_schema": cleanedParams,
@@ -186,7 +186,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
// Enable extended thinking if thinking_level is set
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
budget := anthropicThinkingBudget(level)
body["thinking"] = map[string]interface{}{
body["thinking"] = map[string]any{
"type": "enabled",
"budget_tokens": budget,
}
+4 -4
View File
@@ -44,8 +44,8 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
line := scanner.Text()
// Track event type
if strings.HasPrefix(line, "event: ") {
currentEvent = strings.TrimPrefix(line, "event: ")
if after, ok := strings.CutPrefix(line, "event: "); ok {
currentEvent = after
continue
}
@@ -77,7 +77,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
result.ToolCalls = append(result.ToolCalls, ToolCall{
ID: ev.ContentBlock.ID,
Name: strings.TrimSpace(ev.ContentBlock.Name),
Arguments: make(map[string]interface{}),
Arguments: make(map[string]any),
})
}
// Store raw content_block for later reconstruction
@@ -155,7 +155,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
// Parse accumulated tool call JSON arguments
for i, rawJSON := range toolCallJSON {
if rawJSON != "" {
args := make(map[string]interface{})
args := make(map[string]any)
_ = json.Unmarshal([]byte(rawJSON), &args)
result.ToolCalls[i].Arguments = args
}
+7 -7
View File
@@ -46,30 +46,30 @@ func BuildCLIHooksConfig(workspace string, restrictToWorkspace bool) (string, fu
// generateSettingsJSON creates Claude CLI settings with PreToolUse hooks.
func generateSettingsJSON(hookPath string) []byte {
settings := map[string]interface{}{
"hooks": map[string]interface{}{
"PreToolUse": []map[string]interface{}{
settings := map[string]any{
"hooks": map[string]any{
"PreToolUse": []map[string]any{
{
"matcher": "Bash",
"hooks": []map[string]interface{}{
"hooks": []map[string]any{
{"type": "command", "command": hookPath},
},
},
{
"matcher": "Write",
"hooks": []map[string]interface{}{
"hooks": []map[string]any{
{"type": "command", "command": hookPath},
},
},
{
"matcher": "Edit",
"hooks": []map[string]interface{}{
"hooks": []map[string]any{
{"type": "command", "command": hookPath},
},
},
{
"matcher": "Read",
"hooks": []map[string]interface{}{
"hooks": []map[string]any{
{"type": "command", "command": hookPath},
},
},
+11 -13
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"maps"
"os"
"path/filepath"
"strings"
@@ -33,9 +34,9 @@ type MCPServerLookup func(ctx context.Context, agentID string) []MCPServerEntry
// MCPConfigData holds the base MCP server entries built at startup.
// Per-session configs are written via WriteMCPConfig with agent context injected.
type MCPConfigData struct {
Servers map[string]interface{} // external MCP server entries (stdio/sse/http)
GatewayAddr string
GatewayToken string
Servers map[string]any // external MCP server entries (stdio/sse/http)
GatewayAddr string
GatewayToken string
AgentMCPLookup MCPServerLookup // optional: resolves per-agent MCP servers from DB
}
@@ -43,7 +44,7 @@ type MCPConfigData struct {
// Does NOT include the goclaw-bridge entry — that's added per-session
// with agent context headers in WriteMCPConfig.
func BuildCLIMCPConfigData(servers map[string]*config.MCPServerConfig, gatewayAddr string, gatewayToken ...string) *MCPConfigData {
mcpServers := make(map[string]interface{}, len(servers))
mcpServers := make(map[string]any, len(servers))
for name, srv := range servers {
if !srv.IsEnabled() {
@@ -109,10 +110,8 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
// Shallow-copy the outer map so we can add the bridge entry without mutating the shared base.
// Inner server entries are not modified, so shallow copy is sufficient.
servers := make(map[string]interface{}, len(d.Servers)+1)
for k, v := range d.Servers {
servers[k] = v
}
servers := make(map[string]any, len(d.Servers)+1)
maps.Copy(servers, d.Servers)
// Inject per-agent MCP servers from DB (if lookup is configured and agentID is set)
if d.AgentMCPLookup != nil && agentID != "" {
@@ -153,7 +152,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
headers["X-Bridge-Sig"] = SignBridgeContext(d.GatewayToken, agentID, userID, channel, chatID, peerKind)
}
bridgeEntry := map[string]interface{}{
bridgeEntry := map[string]any{
"url": fmt.Sprintf("http://%s/mcp/bridge", d.GatewayAddr),
"type": "http",
}
@@ -167,7 +166,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
return ""
}
data, err := json.MarshalIndent(map[string]interface{}{"mcpServers": servers}, "", " ")
data, err := json.MarshalIndent(map[string]any{"mcpServers": servers}, "", " ")
if err != nil {
slog.Warn("claude-cli: failed to marshal mcp config", "error", err)
return ""
@@ -202,10 +201,9 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
return path
}
// mcpServerEntryToConfig converts an MCPServerEntry to the CLI MCP config format.
func mcpServerEntryToConfig(srv MCPServerEntry) map[string]interface{} {
entry := make(map[string]interface{})
func mcpServerEntryToConfig(srv MCPServerEntry) map[string]any {
entry := make(map[string]any)
switch srv.Transport {
case "stdio":
if srv.Command != "" {
+1 -1
View File
@@ -15,7 +15,7 @@ func parseJSONResponse(data []byte) (*ChatResponse, error) {
}
// Fallback: CLI may output one JSON object per line.
for _, line := range bytes.Split(data, []byte("\n")) {
for line := range bytes.SplitSeq(data, []byte("\n")) {
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
+11 -11
View File
@@ -131,7 +131,7 @@ func extractFromMessages(msgs []Message) (systemPrompt, userMsg string, images [
}
// extractStringOpt gets a string value from Options map by key.
func extractStringOpt(opts map[string]interface{}, key string) string {
func extractStringOpt(opts map[string]any, key string) string {
if opts == nil {
return ""
}
@@ -144,7 +144,7 @@ func extractStringOpt(opts map[string]interface{}, key string) string {
}
// extractBoolOpt gets a bool value from Options map by key.
func extractBoolOpt(opts map[string]interface{}, key string) bool {
func extractBoolOpt(opts map[string]any, key string) bool {
if opts == nil {
return false
}
@@ -157,7 +157,7 @@ func extractBoolOpt(opts map[string]interface{}, key string) bool {
}
// bridgeContextFromOpts builds a BridgeContext from the Options map.
func bridgeContextFromOpts(opts map[string]interface{}) BridgeContext {
func bridgeContextFromOpts(opts map[string]any) BridgeContext {
return BridgeContext{
AgentID: extractStringOpt(opts, OptAgentID),
UserID: extractStringOpt(opts, OptUserID),
@@ -209,12 +209,12 @@ func sessionFileExists(workDir string, sessionID uuid.UUID) bool {
// buildStreamJSONInput creates stream-json stdin for vision (images + text).
func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader {
var contentBlocks []map[string]interface{}
var contentBlocks []map[string]any
for _, img := range images {
contentBlocks = append(contentBlocks, map[string]interface{}{
contentBlocks = append(contentBlocks, map[string]any{
"type": "image",
"source": map[string]interface{}{
"source": map[string]any{
"type": "base64",
"media_type": img.MimeType,
"data": img.Data,
@@ -223,15 +223,15 @@ func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader {
}
if text != "" {
contentBlocks = append(contentBlocks, map[string]interface{}{
contentBlocks = append(contentBlocks, map[string]any{
"type": "text",
"text": text,
})
}
msg := map[string]interface{}{
msg := map[string]any{
"type": "user",
"message": map[string]interface{}{
"message": map[string]any{
"role": "user",
"content": contentBlocks,
},
@@ -246,8 +246,8 @@ func filterCLIEnv(environ []string) []string {
var filtered []string
for _, e := range environ {
key := e
if idx := strings.IndexByte(e, '='); idx >= 0 {
key = e[:idx]
if before, _, ok := strings.Cut(e, "="); ok {
key = before
}
// Filter out variables that could cause nested CLI conflicts
if strings.HasPrefix(key, "CLAUDE") {
+2 -2
View File
@@ -44,7 +44,7 @@ func NewCodexProvider(name string, tokenSource TokenSource, apiBase, defaultMode
}
}
func (p *CodexProvider) Name() string { return p.name }
func (p *CodexProvider) Name() string { return p.name }
func (p *CodexProvider) DefaultModel() string { return p.defaultModel }
func (p *CodexProvider) SupportsThinking() bool { return true }
@@ -97,7 +97,7 @@ func (p *CodexProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk
if acc.name == "" {
continue
}
args := make(map[string]interface{})
args := make(map[string]any)
_ = json.Unmarshal([]byte(acc.rawArgs), &args)
result.ToolCalls = append(result.ToolCalls, ToolCall{
ID: acc.callID,
+16 -16
View File
@@ -11,14 +11,14 @@ import (
)
// buildRequestBody converts internal ChatRequest to Responses API format.
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]interface{} {
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]any {
model := req.Model
if model == "" {
model = p.defaultModel
}
var instructions string
var input []interface{}
var input []any
for _, m := range req.Messages {
switch m.Role {
@@ -31,25 +31,25 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
case "user":
if len(m.Images) > 0 {
var parts []map[string]interface{}
var parts []map[string]any
for _, img := range m.Images {
parts = append(parts, map[string]interface{}{
parts = append(parts, map[string]any{
"type": "input_image",
"image_url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data),
})
}
if m.Content != "" {
parts = append(parts, map[string]interface{}{
parts = append(parts, map[string]any{
"type": "input_text",
"text": m.Content,
})
}
input = append(input, map[string]interface{}{
input = append(input, map[string]any{
"role": "user",
"content": parts,
})
} else {
input = append(input, map[string]interface{}{
input = append(input, map[string]any{
"role": "user",
"content": m.Content,
})
@@ -60,7 +60,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
for _, tc := range m.ToolCalls {
argsJSON, _ := json.Marshal(tc.Arguments)
callID := toFcID(tc.ID)
input = append(input, map[string]interface{}{
input = append(input, map[string]any{
"type": "function_call",
"id": callID,
"call_id": callID,
@@ -70,10 +70,10 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
}
}
if m.Content != "" {
item := map[string]interface{}{
item := map[string]any{
"type": "message",
"role": "assistant",
"content": []map[string]interface{}{
"content": []map[string]any{
{"type": "output_text", "text": m.Content},
},
}
@@ -84,7 +84,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
}
case "tool":
input = append(input, map[string]interface{}{
input = append(input, map[string]any{
"type": "function_call_output",
"call_id": toFcID(m.ToolCallID),
"output": m.Content,
@@ -92,7 +92,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
}
}
body := map[string]interface{}{
body := map[string]any{
"model": model,
"input": input,
"stream": stream,
@@ -105,9 +105,9 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
body["instructions"] = instructions
if len(req.Tools) > 0 {
var tools []map[string]interface{}
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]interface{}{
tools = append(tools, map[string]any{
"type": "function",
"name": t.Function.Name,
"description": t.Function.Description,
@@ -118,13 +118,13 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
}
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
body["reasoning"] = map[string]interface{}{"effort": level}
body["reasoning"] = map[string]any{"effort": level}
}
return body
}
func (p *CodexProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
func (p *CodexProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("%s: marshal request: %w", p.name, err)
+17 -18
View File
@@ -20,7 +20,7 @@ func (s *staticTokenSource) Token() (string, error) {
}
// mustJSON marshals v to JSON string; panics on error.
func mustJSON(v interface{}) string {
func mustJSON(v any) string {
b, err := json.Marshal(v)
if err != nil {
panic(err)
@@ -68,7 +68,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) {
{Role: "assistant", Content: "Hi there!"},
{Role: "user", Content: "How are you?"},
},
Options: map[string]interface{}{
Options: map[string]any{
OptMaxTokens: 1024,
OptTemperature: 0.7,
},
@@ -97,7 +97,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) {
}
// Check input items (should exclude system messages)
input, ok := body["input"].([]interface{})
input, ok := body["input"].([]any)
if !ok {
t.Fatalf("input is not []interface{}: %T", body["input"])
}
@@ -124,10 +124,10 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) {
Function: ToolFunctionSchema{
Name: "get_weather",
Description: "Get current weather",
Parameters: map[string]interface{}{
Parameters: map[string]any{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{"type": "string"},
"properties": map[string]any{
"city": map[string]any{"type": "string"},
},
"required": []string{"city"},
},
@@ -138,7 +138,7 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) {
body := p.buildRequestBody(req, true)
tools, ok := body["tools"].([]map[string]interface{})
tools, ok := body["tools"].([]map[string]any)
if !ok {
t.Fatalf("tools is not []map[string]interface{}: %T", body["tools"])
}
@@ -167,7 +167,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
{
Role: "assistant",
ToolCalls: []ToolCall{
{ID: "call_123", Name: "get_weather", Arguments: map[string]interface{}{"city": "London"}},
{ID: "call_123", Name: "get_weather", Arguments: map[string]any{"city": "London"}},
},
},
{Role: "tool", ToolCallID: "call_123", Content: `{"temp": 15}`},
@@ -176,7 +176,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
body := p.buildRequestBody(req, false)
input, ok := body["input"].([]interface{})
input, ok := body["input"].([]any)
if !ok {
t.Fatalf("input is not []interface{}: %T", body["input"])
}
@@ -185,7 +185,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
}
// Second item should be function_call
fc, ok := input[1].(map[string]interface{})
fc, ok := input[1].(map[string]any)
if !ok {
t.Fatalf("input[1] is not map: %T", input[1])
}
@@ -197,7 +197,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
}
// Third item should be function_call_output
fco, ok := input[2].(map[string]interface{})
fco, ok := input[2].(map[string]any)
if !ok {
t.Fatalf("input[2] is not map: %T", input[2])
}
@@ -214,12 +214,12 @@ func TestCodexProviderBuildRequestBodyThinking(t *testing.T) {
req := ChatRequest{
Messages: []Message{{Role: "user", Content: "Think about this"}},
Options: map[string]interface{}{OptThinkingLevel: "high"},
Options: map[string]any{OptThinkingLevel: "high"},
}
body := p.buildRequestBody(req, false)
reasoning, ok := body["reasoning"].(map[string]interface{})
reasoning, ok := body["reasoning"].(map[string]any)
if !ok {
t.Fatalf("reasoning is not map: %T", body["reasoning"])
}
@@ -245,7 +245,7 @@ func TestCodexProviderChat(t *testing.T) {
}
// Verify request body
var body map[string]interface{}
var body map[string]any
json.NewDecoder(r.Body).Decode(&body)
if body["model"] != "gpt-4o" {
t.Errorf("request model = %v, want gpt-4o", body["model"])
@@ -599,17 +599,17 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) {
body := p.buildRequestBody(req, false)
input, ok := body["input"].([]interface{})
input, ok := body["input"].([]any)
if !ok || len(input) != 1 {
t.Fatalf("expected 1 input item, got %v", body["input"])
}
item, ok := input[0].(map[string]interface{})
item, ok := input[0].(map[string]any)
if !ok {
t.Fatalf("input[0] is not map: %T", input[0])
}
content, ok := item["content"].([]map[string]interface{})
content, ok := item["content"].([]map[string]any)
if !ok {
t.Fatalf("content is not []map: %T", item["content"])
}
@@ -625,4 +625,3 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) {
t.Errorf("content[1] type = %v, want input_text", content[1]["type"])
}
}
+4 -5
View File
@@ -3,6 +3,7 @@ package providers
import (
"context"
"log/slog"
"maps"
)
const (
@@ -29,7 +30,7 @@ func NewDashScopeProvider(apiKey, apiBase, defaultModel string) *DashScopeProvid
}
}
func (p *DashScopeProvider) Name() string { return "dashscope" }
func (p *DashScopeProvider) Name() string { return "dashscope" }
func (p *DashScopeProvider) SupportsThinking() bool { return true }
// ChatStream handles DashScope's limitation: tools + streaming cannot coexist.
@@ -39,10 +40,8 @@ func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onC
// Map thinking_level to DashScope-specific params before passing to OpenAI base
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
// Clone Options to avoid mutating caller's map
opts := make(map[string]interface{}, len(req.Options)+2)
for k, v := range req.Options {
opts[k] = v
}
opts := make(map[string]any, len(req.Options)+2)
maps.Copy(opts, req.Options)
opts[OptEnableThinking] = true
opts[OptThinkingBudget] = dashscopeThinkingBudget(level)
delete(opts, OptThinkingLevel) // don't pass generic key to OpenAI buildRequestBody
+20 -21
View File
@@ -47,11 +47,11 @@ func (p *OpenAIProvider) WithChatPath(path string) *OpenAIProvider {
return p
}
func (p *OpenAIProvider) Name() string { return p.name }
func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel }
func (p *OpenAIProvider) SupportsThinking() bool { return true }
func (p *OpenAIProvider) APIKey() string { return p.apiKey }
func (p *OpenAIProvider) APIBase() string { return p.apiBase }
func (p *OpenAIProvider) Name() string { return p.name }
func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel }
func (p *OpenAIProvider) SupportsThinking() bool { return true }
func (p *OpenAIProvider) APIKey() string { return p.apiKey }
func (p *OpenAIProvider) APIBase() string { return p.apiBase }
// resolveModel returns the model ID to use for a request.
// For OpenRouter, model IDs require a provider prefix (e.g. "anthropic/claude-sonnet-4-5-20250929").
@@ -189,7 +189,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun
// Parse accumulated tool call arguments
for i := 0; i < len(accumulators); i++ {
acc := accumulators[i]
args := make(map[string]interface{})
args := make(map[string]any)
_ = json.Unmarshal([]byte(acc.rawArgs), &args)
acc.Arguments = args
if acc.thoughtSig != "" {
@@ -209,7 +209,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun
return result, nil
}
func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} {
func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any {
// Gemini 2.5+: collapse tool_call cycles missing thought_signature.
// Gemini requires thought_signature echoed back on every tool_call; models that
// don't return it (e.g. gemini-3-flash) will cause HTTP 400 if sent as-is.
@@ -223,9 +223,9 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
// This is necessary because our internal Message/ToolCall structs don't match
// the OpenAI API format (tool_calls need type+function wrapper, arguments as JSON string).
// Also omits empty content on assistant messages with tool_calls (Gemini compatibility).
msgs := make([]map[string]interface{}, 0, len(inputMessages))
msgs := make([]map[string]any, 0, len(inputMessages))
for _, m := range inputMessages {
msg := map[string]interface{}{
msg := map[string]any{
"role": m.Role,
}
@@ -237,17 +237,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
// Include content; omit empty content for assistant messages with tool_calls
// (Gemini rejects empty content → "must include at least one parts field").
if m.Role == "user" && len(m.Images) > 0 {
var parts []map[string]interface{}
var parts []map[string]any
for _, img := range m.Images {
parts = append(parts, map[string]interface{}{
parts = append(parts, map[string]any{
"type": "image_url",
"image_url": map[string]interface{}{
"image_url": map[string]any{
"url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data),
},
})
}
if m.Content != "" {
parts = append(parts, map[string]interface{}{
parts = append(parts, map[string]any{
"type": "text",
"text": m.Content,
})
@@ -260,17 +260,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
// Convert tool_calls to OpenAI wire format:
// {id, type: "function", function: {name, arguments: "<json string>"}}
if len(m.ToolCalls) > 0 {
toolCalls := make([]map[string]interface{}, len(m.ToolCalls))
toolCalls := make([]map[string]any, len(m.ToolCalls))
for i, tc := range m.ToolCalls {
argsJSON, _ := json.Marshal(tc.Arguments)
fn := map[string]interface{}{
fn := map[string]any{
"name": tc.Name,
"arguments": string(argsJSON),
}
if sig := tc.Metadata["thought_signature"]; sig != "" {
fn["thought_signature"] = sig
}
toolCalls[i] = map[string]interface{}{
toolCalls[i] = map[string]any{
"id": tc.ID,
"type": "function",
"function": fn,
@@ -286,7 +286,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
msgs = append(msgs, msg)
}
body := map[string]interface{}{
body := map[string]any{
"model": model,
"messages": msgs,
"stream": stream,
@@ -298,7 +298,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
}
if stream {
body["stream_options"] = map[string]interface{}{
body["stream_options"] = map[string]any{
"include_usage": true,
}
}
@@ -327,7 +327,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
return body
}
func (p *OpenAIProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
func (p *OpenAIProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("%s: marshal request: %w", p.name, err)
@@ -370,7 +370,7 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse {
result.FinishReason = resp.Choices[0].FinishReason
for _, tc := range msg.ToolCalls {
args := make(map[string]interface{})
args := make(map[string]any)
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
call := ToolCall{
ID: tc.ID,
@@ -404,4 +404,3 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse {
return result
}
+11 -14
View File
@@ -1,5 +1,7 @@
package providers
import "slices"
import "strings"
// Unsupported schema keys by provider.
@@ -34,7 +36,7 @@ func CleanToolSchemas(providerName string, tools []ToolDefinition) []ToolDefinit
}
// CleanSchemaForProvider cleans a single parameters map for a provider.
func CleanSchemaForProvider(providerName string, params map[string]interface{}) map[string]interface{} {
func CleanSchemaForProvider(providerName string, params map[string]any) map[string]any {
removeKeys := unsupportedKeysForProvider(providerName)
if removeKeys == nil {
return params
@@ -54,21 +56,21 @@ func unsupportedKeysForProvider(name string) []string {
}
// cleanSchema recursively removes unsupported keys from a JSON Schema map.
func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string]interface{} {
func cleanSchema(schema map[string]any, removeKeys []string) map[string]any {
if schema == nil {
return nil
}
result := make(map[string]interface{}, len(schema))
result := make(map[string]any, len(schema))
for k, v := range schema {
if shouldRemoveKey(k, removeKeys) {
continue
}
switch val := v.(type) {
case map[string]interface{}:
case map[string]any:
result[k] = cleanSchema(val, removeKeys)
case []interface{}:
case []any:
result[k] = cleanSchemaSlice(val, removeKeys)
default:
result[k] = v
@@ -78,10 +80,10 @@ func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string]
}
// cleanSchemaSlice recurses into arrays (e.g. "anyOf", "oneOf", "allOf").
func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} {
result := make([]interface{}, len(items))
func cleanSchemaSlice(items []any, removeKeys []string) []any {
result := make([]any, len(items))
for i, item := range items {
if m, ok := item.(map[string]interface{}); ok {
if m, ok := item.(map[string]any); ok {
result[i] = cleanSchema(m, removeKeys)
} else {
result[i] = item
@@ -91,10 +93,5 @@ func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} {
}
func shouldRemoveKey(key string, removeKeys []string) bool {
for _, rk := range removeKeys {
if key == rk {
return true
}
}
return false
return slices.Contains(removeKeys, key)
}
+30 -30
View File
@@ -10,17 +10,17 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
Function: ToolFunctionSchema{
Name: "test",
Description: "desc",
Parameters: map[string]interface{}{
Parameters: map[string]any{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"properties": map[string]any{
"name": map[string]any{
"type": "string",
"default": "world",
},
},
"$defs": map[string]interface{}{"Foo": "bar"},
"$defs": map[string]any{"Foo": "bar"},
"additionalProperties": false,
"examples": []interface{}{"a"},
"examples": []any{"a"},
},
},
}}
@@ -43,8 +43,8 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
}
// Nested "default" should be removed
props := params["properties"].(map[string]interface{})
nameSchema := props["name"].(map[string]interface{})
props := params["properties"].(map[string]any)
nameSchema := props["name"].(map[string]any)
if _, ok := nameSchema["default"]; ok {
t.Error("expected nested 'default' to be removed for gemini")
}
@@ -54,15 +54,15 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
}
func TestCleanToolSchemas_Anthropic(t *testing.T) {
params := map[string]interface{}{
params := map[string]any{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]interface{}{
"properties": map[string]any{
"url": map[string]any{
"type": "string",
"$ref": "#/$defs/URL",
},
},
"$defs": map[string]interface{}{"URL": "..."},
"$defs": map[string]any{"URL": "..."},
"additionalProperties": false,
"default": "x",
}
@@ -73,8 +73,8 @@ func TestCleanToolSchemas_Anthropic(t *testing.T) {
if _, ok := cleaned["$defs"]; ok {
t.Error("expected $defs removed for anthropic")
}
props := cleaned["properties"].(map[string]interface{})
urlSchema := props["url"].(map[string]interface{})
props := cleaned["properties"].(map[string]any)
urlSchema := props["url"].(map[string]any)
if _, ok := urlSchema["$ref"]; ok {
t.Error("expected nested $ref removed for anthropic")
}
@@ -93,7 +93,7 @@ func TestCleanToolSchemas_Unknown(t *testing.T) {
Type: "function",
Function: ToolFunctionSchema{
Name: "test",
Parameters: map[string]interface{}{
Parameters: map[string]any{
"$ref": "something",
"default": "val",
},
@@ -122,13 +122,13 @@ func TestCleanSchema_NilParams(t *testing.T) {
}
func TestCleanSchema_NestedArray(t *testing.T) {
params := map[string]interface{}{
"anyOf": []interface{}{
map[string]interface{}{
params := map[string]any{
"anyOf": []any{
map[string]any{
"type": "string",
"default": "x",
},
map[string]interface{}{
map[string]any{
"type": "number",
"$ref": "#/defs/Num",
"default": 42,
@@ -137,12 +137,12 @@ func TestCleanSchema_NestedArray(t *testing.T) {
}
cleaned := CleanSchemaForProvider("gemini", params)
anyOf := cleaned["anyOf"].([]interface{})
anyOf := cleaned["anyOf"].([]any)
if len(anyOf) != 2 {
t.Fatalf("expected 2 items, got %d", len(anyOf))
}
first := anyOf[0].(map[string]interface{})
first := anyOf[0].(map[string]any)
if _, ok := first["default"]; ok {
t.Error("expected 'default' removed in array item")
}
@@ -150,7 +150,7 @@ func TestCleanSchema_NestedArray(t *testing.T) {
t.Error("expected 'type' to remain in array item")
}
second := anyOf[1].(map[string]interface{})
second := anyOf[1].(map[string]any)
if _, ok := second["$ref"]; ok {
t.Error("expected '$ref' removed in array item")
}
@@ -160,13 +160,13 @@ func TestCleanSchema_NestedArray(t *testing.T) {
}
func TestCleanSchema_DeepNesting(t *testing.T) {
params := map[string]interface{}{
params := map[string]any{
"type": "object",
"properties": map[string]interface{}{
"config": map[string]interface{}{
"properties": map[string]any{
"config": map[string]any{
"type": "object",
"properties": map[string]interface{}{
"nested": map[string]interface{}{
"properties": map[string]any{
"nested": map[string]any{
"type": "string",
"default": "deep",
"$ref": "#/deep",
@@ -177,10 +177,10 @@ func TestCleanSchema_DeepNesting(t *testing.T) {
}
cleaned := CleanSchemaForProvider("gemini", params)
props := cleaned["properties"].(map[string]interface{})
config := props["config"].(map[string]interface{})
innerProps := config["properties"].(map[string]interface{})
nested := innerProps["nested"].(map[string]interface{})
props := cleaned["properties"].(map[string]any)
config := props["config"].(map[string]any)
innerProps := config["properties"].(map[string]any)
nested := innerProps["nested"].(map[string]any)
if _, ok := nested["default"]; ok {
t.Error("expected deeply nested 'default' removed")
+23 -23
View File
@@ -7,12 +7,12 @@ import (
// Options keys used in ChatRequest.Options across providers.
const (
OptMaxTokens = "max_tokens"
OptTemperature = "temperature"
OptThinkingLevel = "thinking_level"
OptMaxTokens = "max_tokens"
OptTemperature = "temperature"
OptThinkingLevel = "thinking_level"
OptReasoningEffort = "reasoning_effort"
OptEnableThinking = "enable_thinking"
OptThinkingBudget = "thinking_budget"
OptEnableThinking = "enable_thinking"
OptThinkingBudget = "thinking_budget"
)
// TokenSource provides an OAuth access token (with auto-refresh).
@@ -45,10 +45,10 @@ type ThinkingCapable interface {
// ChatRequest contains the input for a Chat/ChatStream call.
type ChatRequest struct {
Messages []Message `json:"messages"`
Tools []ToolDefinition `json:"tools,omitempty"`
Model string `json:"model,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
Messages []Message `json:"messages"`
Tools []ToolDefinition `json:"tools,omitempty"`
Model string `json:"model,omitempty"`
Options map[string]any `json:"options,omitempty"`
}
// ChatResponse is the result from an LLM call.
@@ -70,9 +70,9 @@ type ChatResponse struct {
// StreamChunk is a piece of a streaming response.
type StreamChunk struct {
Content string `json:"content,omitempty"`
Thinking string `json:"thinking,omitempty"`
Done bool `json:"done,omitempty"`
Content string `json:"content,omitempty"`
Thinking string `json:"thinking,omitempty"`
Done bool `json:"done,omitempty"`
}
// ImageContent represents a base64-encoded image for vision-capable models.
@@ -92,11 +92,11 @@ type MediaRef struct {
// Message represents a conversation message.
type Message struct {
Role string `json:"role"` // "system", "user", "assistant", "tool"
Role string `json:"role"` // "system", "user", "assistant", "tool"
Content string `json:"content"`
Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.)
Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted)
MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references
Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.)
Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted)
MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"` // for role="tool" responses
@@ -113,10 +113,10 @@ type Message struct {
// ToolCall represents a tool invocation requested by the LLM.
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature)
ID string `json:"id"`
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature)
}
// ToolDefinition describes a tool available to the LLM.
@@ -127,9 +127,9 @@ type ToolDefinition struct {
// ToolFunctionSchema is the schema for a function tool.
type ToolFunctionSchema struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters"`
}
// Usage tracks token consumption.
+4 -5
View File
@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"log/slog"
"maps"
"os/exec"
"strings"
"sync"
@@ -282,9 +283,7 @@ func (m *DockerManager) Release(ctx context.Context, key string) error {
func (m *DockerManager) ReleaseAll(ctx context.Context) error {
m.mu.Lock()
sbs := make(map[string]*DockerSandbox, len(m.sandboxes))
for k, v := range m.sandboxes {
sbs[k] = v
}
maps.Copy(sbs, m.sandboxes)
m.sandboxes = make(map[string]*DockerSandbox)
m.mu.Unlock()
@@ -297,7 +296,7 @@ func (m *DockerManager) ReleaseAll(ctx context.Context) error {
}
// Stats returns information about active sandboxes.
func (m *DockerManager) Stats() map[string]interface{} {
func (m *DockerManager) Stats() map[string]any {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -306,7 +305,7 @@ func (m *DockerManager) Stats() map[string]interface{} {
containers[key] = sb.containerID
}
return map[string]interface{}{
return map[string]any{
"mode": m.config.Mode,
"image": m.config.Image,
"active": len(m.sandboxes),
+18 -18
View File
@@ -67,36 +67,36 @@ type Config struct {
// Security hardening (matching TS buildSandboxCreateArgs)
ReadOnlyRoot bool `json:"read_only_root"`
CapDrop []string `json:"cap_drop,omitempty"`
Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m"
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default)
Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m"
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default)
PidsLimit int `json:"pids_limit,omitempty"`
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec stdout+stderr capture (default 1MB, 0 = unlimited)
SetupCommand string `json:"setup_command,omitempty"`
ContainerPrefix string `json:"container_prefix,omitempty"`
Workdir string `json:"workdir,omitempty"` // container workdir (default "/workspace")
// Pruning (matching TS SandboxPruneSettings)
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7)
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7)
PruneIntervalMin int `json:"prune_interval_min,omitempty"` // check interval in minutes (default 5)
}
// DefaultConfig returns sensible defaults matching TS sandbox defaults.
func DefaultConfig() Config {
return Config{
Mode: ModeOff,
Image: "goclaw-sandbox:bookworm-slim",
WorkspaceAccess: AccessRW,
Scope: ScopeSession,
MemoryMB: 512,
CPUs: 1.0,
TimeoutSec: 300,
NetworkEnabled: false,
ReadOnlyRoot: true,
CapDrop: []string{"ALL"},
Tmpfs: []string{"/tmp", "/var/tmp", "/run"},
MaxOutputBytes: 1 << 20, // 1MB
Mode: ModeOff,
Image: "goclaw-sandbox:bookworm-slim",
WorkspaceAccess: AccessRW,
Scope: ScopeSession,
MemoryMB: 512,
CPUs: 1.0,
TimeoutSec: 300,
NetworkEnabled: false,
ReadOnlyRoot: true,
CapDrop: []string{"ALL"},
Tmpfs: []string{"/tmp", "/var/tmp", "/run"},
MaxOutputBytes: 1 << 20, // 1MB
ContainerPrefix: "goclaw-sbx-",
Workdir: "/workspace",
IdleHours: 24,
@@ -183,7 +183,7 @@ type Manager interface {
Stop()
// Stats returns info about active sandboxes.
Stats() map[string]interface{}
Stats() map[string]any
}
// ErrSandboxDisabled is returned when sandbox mode is "off".
+2 -2
View File
@@ -18,7 +18,7 @@ func TestLane_ConcurrencyLimit(t *testing.T) {
var maxActive atomic.Int32
var wg sync.WaitGroup
for i := 0; i < 6; i++ {
for range 6 {
wg.Add(1)
err := lane.Submit(context.Background(), func() {
defer wg.Done()
@@ -139,7 +139,7 @@ func TestScheduler_SessionSerialization(t *testing.T) {
sessionKey := "agent:default:test-session"
var outcomes []<-chan RunOutcome
for i := 0; i < 3; i++ {
for i := range 3 {
ch := sched.Schedule(ctx, "main", agent.RunRequest{
SessionKey: sessionKey,
Message: "hello",
+1 -1
View File
@@ -463,7 +463,7 @@ func stripFrontmatter(content string) string {
func parseSimpleYAML(content string) map[string]string {
result := make(map[string]string)
for _, line := range strings.Split(content, "\n") {
for line := range strings.SplitSeq(content, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
+4 -4
View File
@@ -19,7 +19,7 @@ type CronJob struct {
// CronSchedule defines when a job should run.
type CronSchedule struct {
Kind string `json:"kind"` // "at", "every", "cron"
Kind string `json:"kind"` // "at", "every", "cron"
AtMS *int64 `json:"atMs,omitempty"`
EveryMS *int64 `json:"everyMs,omitempty"`
Expr string `json:"expr,omitempty"`
@@ -79,10 +79,10 @@ type CronJobPatch struct {
// CronEvent represents a job lifecycle event sent to subscribers.
type CronEvent struct {
Action string `json:"action"` // "running", "completed", "error"
Action string `json:"action"` // "running", "completed", "error"
JobID string `json:"jobId"`
JobName string `json:"jobName,omitempty"`
Status string `json:"status,omitempty"` // final status for completed/error
Status string `json:"status,omitempty"` // final status for completed/error
Error string `json:"error,omitempty"`
}
@@ -95,7 +95,7 @@ type CronStore interface {
UpdateJob(jobID string, patch CronJobPatch) (*CronJob, error)
EnableJob(jobID string, enabled bool) error
GetRunLog(jobID string, limit, offset int) ([]CronRunLogEntry, int)
Status() map[string]interface{}
Status() map[string]any
// Lifecycle
Start() error
+11 -10
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"log/slog"
"strings"
"time"
"github.com/google/uuid"
@@ -318,7 +319,7 @@ func (s *PGAgentStore) ListAccessible(ctx context.Context, userID string) ([]sto
// --- Scan helpers ---
type agentRowScanner interface {
Scan(dest ...interface{}) error
Scan(dest ...any) error
}
func scanAgentRow(row agentRowScanner) (*store.AgentData, error) {
@@ -380,7 +381,7 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m
return nil
}
var setClauses []string
var args []interface{}
var args []any
i := 1
for col, val := range updates {
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i))
@@ -405,25 +406,25 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m
}
func joinStrings(s []string, sep string) string {
result := ""
var result strings.Builder
for i, v := range s {
if i > 0 {
result += sep
result.WriteString(sep)
}
result += v
result.WriteString(v)
}
return result
return result.String()
}
func replaceIDX(s, replacement string) string {
result := ""
var result strings.Builder
for i := 0; i < len(s); i++ {
if i+4 <= len(s) && s[i:i+4] == "$IDX" {
result += replacement
result.WriteString(replacement)
i += 3
} else {
result += string(s[i])
result.WriteString(string(s[i]))
}
}
return result
return result.String()
}
+2 -2
View File
@@ -66,7 +66,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma
switch sv := v.(type) {
case json.RawMessage:
allowed["settings"] = []byte(sv)
case map[string]interface{}:
case map[string]any:
b, err := json.Marshal(sv)
if err != nil {
return fmt.Errorf("marshal settings: %w", err)
@@ -85,7 +85,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma
// Build UPDATE with name as PK (not UUID)
var setClauses []string
var args []interface{}
var args []any
i := 1
for col, val := range allowed {
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i))
+2 -2
View File
@@ -192,9 +192,9 @@ func (s *PGChannelInstanceStore) ListAll(ctx context.Context) ([]store.ChannelIn
return s.scanInstances(rows)
}
func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []interface{}) {
func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []any) {
var conditions []string
var args []interface{}
var args []any
argIdx := 1
if opts.Search != "" {
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff. Show more