diff --git a/k3s/configmap.yaml b/k3s/configmap.yaml index 02369782b..75f4bf1ac 100644 --- a/k3s/configmap.yaml +++ b/k3s/configmap.yaml @@ -622,7 +622,7 @@ data: "api_key": "picoclaw-secret-123", "chat_enabled": true, "hot_reload": true, - "log_level": "info" + "log_level": "debug" }, "hooks": { "enabled": true, @@ -669,6 +669,8 @@ data: "weather": true, "summarize": true, "github": true, + "monday": true, + "harvest": true, "freeride": true, "hdn-server": true } @@ -786,6 +788,8 @@ data: "weather", "summarize", "github", + "monday", + "harvest", "freeride", "hdn-server" ], diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index 5bcb83087..f3ec2cf87 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -102,11 +102,13 @@ func NewAgentInstance( toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths)) } if cfg.Tools.IsToolEnabled("exec") { - execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths) + execTool, err := tools.NewExecToolWithDenyPaths(workspace, restrict, [][]*regexp.Regexp{allowReadPaths}, denyWritePaths, cfg) if err != nil { logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec", map[string]any{"error": err.Error()}) - } else { + execTool = nil + } + if execTool != nil { toolsRegistry.Register(execTool) } } diff --git a/pkg/agent/loop_init.go b/pkg/agent/loop_init.go index 5bee267b2..5de12636d 100644 --- a/pkg/agent/loop_init.go +++ b/pkg/agent/loop_init.go @@ -26,11 +26,20 @@ func NewAgentLoop( msgBus *bus.MessageBus, provider providers.LLMProvider, ) *AgentLoop { + logger.Debug("Initializing AgentRegistry...") registry := NewAgentRegistry(cfg, provider) // Set up shared fallback chain with rate limiting. - cooldownPath := filepath.Join(filepath.Dir(filepath.Clean(registry.GetDefaultAgent().Workspace)), "cooldowns.json") + // Harden: ensure we have a valid workspace for the cooldown file even if the default agent isn't ready. + baseDir := filepath.Dir(configPath) + if defaultAgent := registry.GetDefaultAgent(); defaultAgent != nil { + baseDir = filepath.Dir(filepath.Clean(defaultAgent.Workspace)) + } + + cooldownPath := filepath.Join(baseDir, "cooldowns.json") + logger.Debugf("Initializing CooldownTracker at %s", cooldownPath) cooldown := providers.NewCooldownTracker(cooldownPath) + rl := providers.NewRateLimiterRegistry() // Register rate limiters for all agents' candidates so that RPM limits // configured in ModelConfig are enforced before each LLM call. @@ -46,6 +55,7 @@ func NewAgentLoop( defaultAgent := registry.GetDefaultAgent() var stateManager *state.Manager if defaultAgent != nil { + logger.Debugf("Initializing State Manager for agent %s", defaultAgent.ID) stateManager = state.NewManager(defaultAgent.Workspace) } diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index c005eef2a..32a140ad5 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -114,24 +114,46 @@ func (p *startupBlockedProvider) GetDefaultModel() string { } // Run starts the gateway runtime using the configuration loaded from configPath. -func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runErr error) { - panicPath := filepath.Join(homePath, logPath, panicFile) - panicFunc, err := logger.InitPanic(panicPath) - if err != nil { - return fmt.Errorf("error initializing panic log: %w", err) - } - defer panicFunc() +func Run( + debug bool, + homePath string, + configPath string, + allowEmptyStartup bool, +) (runErr error) { + fmt.Printf("šŸ“‚ Home Path: %s\n", homePath) + fmt.Printf("šŸ“„ Config Path: %s\n", configPath) - if err = logger.EnableFileLogging(filepath.Join(homePath, logPath, logFile)); err != nil { + // Ensure home directory exists early + if err := os.MkdirAll(homePath, 0o755); err != nil { + return fmt.Errorf("failed to create home directory: %w", err) + } + + // Initialize panic logging + panicPath := filepath.Join(homePath, logPath, panicFile) + fmt.Printf("šŸ”§ Initializing panic log: %s\n", panicPath) + panicCleanup, err := logger.InitPanic(panicPath) + if err != nil { + return fmt.Errorf("failed to initialize panic log: %w", err) + } + defer panicCleanup() + fmt.Println("āœ“ Panic log initialized") + + // Enable main file logging + mainLogPath := filepath.Join(homePath, logPath, logFile) + fmt.Printf("šŸ”§ Enabling file logging: %s\n", mainLogPath) + if err = logger.EnableFileLogging(mainLogPath); err != nil { logger.Fatal(fmt.Sprintf("error enabling file logging: %v", err)) } defer logger.DisableFileLogging() + fmt.Println("āœ“ File logging enabled") + // Set initial log level from config if possible, otherwise default to INFO if debug { logger.SetLevel(logger.DEBUG) } else { logger.SetLevelFromString(config.ResolveGatewayLogLevel(configPath)) } + defer func() { if runErr != nil { logger.ErrorCF("gateway", "Gateway startup failed", map[string]any{ @@ -144,10 +166,12 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr } }() + fmt.Println("šŸ” Loading configuration...") cfg, err := config.LoadConfig(configPath) if err != nil { return fmt.Errorf("error loading config: %w", err) } + logger.Info("āœ“ Configuration loaded") if err = preCheckConfig(cfg); err != nil { return fmt.Errorf("config pre-check failed: %w", err) @@ -156,6 +180,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr // Debug mode permanently overrides the config log level to DEBUG. if debug { fmt.Println("šŸ” Debug mode enabled") + logger.SetLevel(logger.DEBUG) } else { effectiveLogLevel := config.EffectiveGatewayLogLevel(cfg) logger.SetLevelFromString(effectiveLogLevel) @@ -167,7 +192,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr return fmt.Errorf("error opening gateway listeners: %w", err) } - // Enforce singleton: write PID file with generated token. + // Enforce singleton and generate auth token pidData, err := pid.WritePidFile(homePath, bindPlan.ProbeHost, cfg.Gateway.Port) if err != nil { logger.Warnf("write pid file failed: %v", err) @@ -177,6 +202,8 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr return fmt.Errorf("singleton check failed: %w", err) } defer pid.RemovePidFile(homePath) + + logger.Info("āœ“ PID file and auth token initialized") closeListeners := true defer func() { if !closeListeners { @@ -187,17 +214,26 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr } }() + msgBus := bus.NewMessageBus() + provider, modelID, err := createStartupProvider(cfg, allowEmptyStartup) if err != nil { return fmt.Errorf("error creating provider: %w", err) } + logger.Infof("āœ“ LLM Provider initialized (Model: %s)", modelID) if modelID != "" { cfg.Agents.Defaults.ModelName = modelID } - msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, configPath, msgBus, provider) + logger.Info("āœ“ Agent loop initialized") + + runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult) + if err != nil { + return fmt.Errorf("error setting up services: %w", err) + } + logger.Info("āœ“ Core services started") fmt.Println("\nšŸ“¦ Agent Status:") startupInfo := agentLoop.GetStartupInfo() @@ -212,11 +248,6 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr "skills_total": skillsInfo["total"], "skills_available": skillsInfo["available"], }) - - runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult) - if err != nil { - return err - } closeListeners = false // Setup manual reload channel for /reload endpoint diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 408fca170..99945166a 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -58,6 +58,29 @@ var protocolMetaByName = map[string]protocolMeta{ "longcat": {defaultAPIBase: "https://api.longcat.chat/openai"}, "modelscope": {defaultAPIBase: "https://api-inference.modelscope.cn/v1"}, "mimo": {defaultAPIBase: "https://api.xiaomimimo.com/v1"}, + + // Specialty and Custom Protocols + "anthropic": {defaultAPIBase: "https://api.anthropic.com"}, + "google": {defaultAPIBase: "https://openrouter.ai/api/v1"}, // Alias for OpenRouter/OpenAI-compatible + "elevenlabs": {}, + "claude-cli": {}, + "codex-cli": {}, + "antigravity": {}, + "cli": {}, + "fs": {}, + "memory": {}, + "dummy": {}, + "openai-tts": {}, + "gpt-4-v": {defaultAPIBase: "https://api.openai.com/v1"}, + "gpt-4o": {defaultAPIBase: "https://api.openai.com/v1"}, + "gpt-4-turbo": {defaultAPIBase: "https://api.openai.com/v1"}, + "azure": {}, + "azure-openai": {}, + "bedrock": {}, + "github-copilot": {}, + "github-copilot-chat": {}, + "copilot": {}, + "claude": {defaultAPIBase: "https://api.anthropic.com"}, } // createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store. @@ -88,15 +111,15 @@ func createCodexAuthProvider() (LLMProvider, error) { // If no prefix is specified, it defaults to "openai". // Examples: // - "openai/gpt-4o" -> ("openai", "gpt-4o") -// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6") -// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol +// - "anthropic/claude-3-opus" -> ("anthropic", "claude-3-opus") +// - "gpt-4o" -> ("openai", "gpt-4o") func ExtractProtocol(model string) (protocol, modelID string) { model = strings.TrimSpace(model) - protocol, modelID, found := strings.Cut(model, "/") + p, m, found := strings.Cut(model, "/") if !found { return "openai", model } - return protocol, modelID + return p, m } // ResolveAPIBase returns the configured API base, or the protocol default when @@ -133,7 +156,13 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err protocol, modelID := ExtractProtocol(cfg.Model) if cfg.Protocol != "" { protocol = cfg.Protocol - modelID = cfg.Model + // If protocol was explicitly set, modelID should be the full model string + // unless it was already prefixed with the SAME protocol. + if p, m, found := strings.Cut(cfg.Model, "/"); found && strings.EqualFold(p, protocol) { + modelID = m + } else { + modelID = cfg.Model + } } userAgent := cfg.UserAgent diff --git a/pkg/providers/httpapi/http_provider.go b/pkg/providers/httpapi/http_provider.go index a84962622..8124b204d 100644 --- a/pkg/providers/httpapi/http_provider.go +++ b/pkg/providers/httpapi/http_provider.go @@ -74,6 +74,10 @@ func (p *HTTPProvider) GetDefaultModel() string { return "" } +func (p *HTTPProvider) SupportsThinking() bool { + return p.delegate.SupportsThinking() +} + func (p *HTTPProvider) SupportsNativeSearch() bool { return p.delegate.SupportsNativeSearch() } diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 98a70cfd2..736bb494e 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -180,6 +180,32 @@ func (p *Provider) buildRequestBody( } } + // Thinking support for OpenRouter + if level, ok := options["thinking_level"].(string); ok && level != "" && level != "off" { + if u, err := url.Parse(p.apiBase); err == nil && u.Hostname() == "openrouter.ai" { + // Map level to budget tokens (using same scale as Anthropic) + budget := 0 + switch level { + case "low": + budget = 4096 + case "medium": + budget = 16384 + case "high": + budget = 32000 + case "xhigh": + budget = 64000 + } + if budget > 0 { + requestBody["thinking"] = map[string]any{ + "type": "enabled", + "budget_tokens": budget, + } + // Remove temperature when thinking is enabled (strict requirement for some models) + delete(requestBody, "temperature") + } + } + } + // Merge extra body fields configured per-provider/model. // These are injected last so they take precedence over defaults. maps.Copy(requestBody, p.extraBody) @@ -466,6 +492,16 @@ func buildToolsList(tools []ToolDefinition, nativeSearch bool) []any { return result } +func (p *Provider) SupportsThinking() bool { + // Enable thinking support for OpenRouter. + // OpenRouter acts as a passthrough for Anthropic and DeepSeek-R1 models that support thinking. + u, err := url.Parse(p.apiBase) + if err != nil { + return false + } + return u.Hostname() == "openrouter.ai" +} + func (p *Provider) SupportsNativeSearch() bool { return isNativeSearchHost(p.apiBase) } diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index a570ac9ec..7a5770145 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -41,6 +41,7 @@ type ExecTool struct { allowPatterns []*regexp.Regexp customAllowPatterns []*regexp.Regexp allowedPathPatterns []*regexp.Regexp + denyWritePaths []*regexp.Regexp restrictToWorkspace bool allowRemote bool sessionManager *SessionManager @@ -115,7 +116,7 @@ var ( ) func NewExecTool(workingDir string, restrict bool, allowPaths ...[]*regexp.Regexp) (*ExecTool, error) { - return NewExecToolWithConfig(workingDir, restrict, nil, allowPaths...) + return NewExecToolWithDenyPaths(workingDir, restrict, allowPaths, nil, nil) } func NewExecToolWithConfig( @@ -123,6 +124,16 @@ func NewExecToolWithConfig( restrict bool, cfg *config.Config, allowPaths ...[]*regexp.Regexp, +) (*ExecTool, error) { + return NewExecToolWithDenyPaths(workingDir, restrict, allowPaths, nil, cfg) +} + +func NewExecToolWithDenyPaths( + workingDir string, + restrict bool, + allowPaths [][]*regexp.Regexp, + denyWritePaths []*regexp.Regexp, + cfg *config.Config, ) (*ExecTool, error) { denyPatterns := make([]*regexp.Regexp, 0) customAllowPatterns := make([]*regexp.Regexp, 0) @@ -149,7 +160,6 @@ func NewExecToolWithConfig( } } } else { - // If deny patterns are disabled, we won't add any patterns, allowing all commands. fmt.Println("Warning: deny patterns are disabled. All commands will be allowed.") } for _, pattern := range execConfig.CustomAllowPatterns { @@ -175,6 +185,7 @@ func NewExecToolWithConfig( allowPatterns: nil, customAllowPatterns: customAllowPatterns, allowedPathPatterns: allowedPathPatterns, + denyWritePaths: denyWritePaths, restrictToWorkspace: restrict, allowRemote: allowRemote, sessionManager: getSessionManager(), @@ -1038,6 +1049,45 @@ func (t *ExecTool) guardCommand(command, cwd string) string { return "Command blocked by safety guard (dangerous pattern detected)" } } + + // Check deny write paths - block commands that write to protected directories + if len(t.denyWritePaths) > 0 { + words := strings.Fields(cmd) + for i, word := range words { + // Skip flags but check their argument (next word) + if word == "-p" || word == "-rf" || word == "-r" || word == "-f" || word == "-d" { + // Check the next word as the actual path + if i+1 < len(words) { + nextWord := words[i+1] + for _, pattern := range t.denyWritePaths { + if pattern.MatchString(nextWord) { + return fmt.Sprintf("Command blocked: cannot write to %s (access denied)", nextWord) + } + // Also check path components + pathParts := strings.Split(nextWord, "/") + for _, part := range pathParts { + if pattern.MatchString(part) { + return fmt.Sprintf("Command blocked: cannot write to %s (access denied)", part) + } + } + } + } + continue + } + for _, pattern := range t.denyWritePaths { + if pattern.MatchString(word) { + return fmt.Sprintf("Command blocked: cannot write to %s (access denied)", word) + } + // Also check path components like "skills" in "mkdir -p skills/my_skill" + pathParts := strings.Split(word, "/") + for _, part := range pathParts { + if pattern.MatchString(part) { + return fmt.Sprintf("Command blocked: cannot write to %s (access denied)", part) + } + } + } + } + } } if len(t.allowPatterns) > 0 { diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go index a8de2f4c9..f5284139e 100644 --- a/pkg/tools/shell_test.go +++ b/pkg/tools/shell_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "regexp" "runtime" "strings" "testing" @@ -1613,3 +1614,54 @@ func TestEncodeKeyTokenWithPtyKeyMode(t *testing.T) { }) } } + +func TestShellTool_DenyWritePaths(t *testing.T) { + tests := []struct { + name string + command string + denyPaths []*regexp.Regexp + expectBlock bool + }{ + { + name: "mkdir blocked", + command: "mkdir -p skills", + denyPaths: []*regexp.Regexp{regexp.MustCompile(`^skills(/.*)?$`)}, + expectBlock: true, + }, + { + name: "mkdir -p blocked", + command: "mkdir -p skills/my_skill", + denyPaths: []*regexp.Regexp{regexp.MustCompile(`^skills(/.*)?$`)}, + expectBlock: true, + }, + { + name: "mkdir allowed", + command: "mkdir -p workspace/data", + denyPaths: []*regexp.Regexp{regexp.MustCompile(`^skills(/.*)?$`)}, + expectBlock: false, + }, + { + name: "touch skills file blocked", + command: "touch skills/test.txt", + denyPaths: []*regexp.Regexp{regexp.MustCompile(`^skills(/.*)?$`)}, + expectBlock: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tool, err := NewExecToolWithDenyPaths("", false, nil, tt.denyPaths, nil) + require.NoError(t, err) + result := tool.Execute(context.Background(), map[string]any{ + "action": "run", + "command": tt.command, + }) + if tt.expectBlock { + require.True(t, result.IsError, "expected block for command: %s", tt.command) + require.Contains(t, result.ForLLM, "access denied") + } else { + require.False(t, result.IsError, "expected allow for command: %s, got: %s", tt.command, result.ForLLM) + } + }) + } +} diff --git a/test_results.txt b/test_results.txt new file mode 100644 index 000000000..3f3d67bc2 --- /dev/null +++ b/test_results.txt @@ -0,0 +1,124 @@ +# v3.466 +no required module provides package v3.466; to add it: + go get v3.466 +FAIL v3.466 [setup failed] +# Balancing +package Balancing is not in std (/usr/local/go/src/Balancing) +FAIL Balancing [setup failed] +# Makefile +package Makefile is not in std (/usr/local/go/src/Makefile) +FAIL Makefile [setup failed] +# across +package across is not in std (/usr/local/go/src/across) +FAIL across [setup failed] +# components. +malformed import path "components.": trailing dot in path element +FAIL components. [setup failed] +? github.com/sipeed/picoclaw/cmd/freeride-diag [no test files] +ok github.com/sipeed/picoclaw/cmd/membench (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron (cached) +? github.com/sipeed/picoclaw/cmd/picoclaw/internal/freeride [no test files] +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/model (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/onboard (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/skills (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/status (cached) +ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/version (cached) +? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui [no test files] +? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config [no test files] +? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/ui [no test files] +? github.com/sipeed/picoclaw/examples/pico-echo-server [no test files] +? github.com/sipeed/picoclaw/pkg [no test files] +ok github.com/sipeed/picoclaw/pkg/agent (cached) +ok github.com/sipeed/picoclaw/pkg/audio (cached) +ok github.com/sipeed/picoclaw/pkg/audio/asr (cached) +ok github.com/sipeed/picoclaw/pkg/audio/tts (cached) +ok github.com/sipeed/picoclaw/pkg/auth (cached) +ok github.com/sipeed/picoclaw/pkg/bus (cached) +ok github.com/sipeed/picoclaw/pkg/channels (cached) +ok github.com/sipeed/picoclaw/pkg/channels/dingtalk (cached) +ok github.com/sipeed/picoclaw/pkg/channels/discord (cached) +ok github.com/sipeed/picoclaw/pkg/channels/feishu (cached) +ok github.com/sipeed/picoclaw/pkg/channels/irc (cached) +ok github.com/sipeed/picoclaw/pkg/channels/line (cached) +? github.com/sipeed/picoclaw/pkg/channels/maixcam [no test files] +ok github.com/sipeed/picoclaw/pkg/channels/matrix (cached) +? github.com/sipeed/picoclaw/pkg/channels/onebot [no test files] +ok github.com/sipeed/picoclaw/pkg/channels/pico (cached) +ok github.com/sipeed/picoclaw/pkg/channels/qq (cached) +ok github.com/sipeed/picoclaw/pkg/channels/slack (cached) +ok github.com/sipeed/picoclaw/pkg/channels/teams_webhook (cached) +ok github.com/sipeed/picoclaw/pkg/channels/telegram (cached) +ok github.com/sipeed/picoclaw/pkg/channels/vk (cached) +ok github.com/sipeed/picoclaw/pkg/channels/wecom (cached) +ok github.com/sipeed/picoclaw/pkg/channels/weixin (cached) +ok github.com/sipeed/picoclaw/pkg/channels/whatsapp (cached) +? github.com/sipeed/picoclaw/pkg/channels/whatsapp_native [no test files] +ok github.com/sipeed/picoclaw/pkg/commands (cached) +ok github.com/sipeed/picoclaw/pkg/config (cached) +? github.com/sipeed/picoclaw/pkg/constants [no test files] +ok github.com/sipeed/picoclaw/pkg/credential (cached) +ok github.com/sipeed/picoclaw/pkg/cron (cached) +? github.com/sipeed/picoclaw/pkg/devices [no test files] +? github.com/sipeed/picoclaw/pkg/devices/events [no test files] +? github.com/sipeed/picoclaw/pkg/devices/sources [no test files] +ok github.com/sipeed/picoclaw/pkg/fileutil (cached) +ok github.com/sipeed/picoclaw/pkg/gateway (cached) +ok github.com/sipeed/picoclaw/pkg/health (cached) +ok github.com/sipeed/picoclaw/pkg/heartbeat (cached) +ok github.com/sipeed/picoclaw/pkg/identity (cached) +ok github.com/sipeed/picoclaw/pkg/isolation (cached) +ok github.com/sipeed/picoclaw/pkg/logger (cached) +ok github.com/sipeed/picoclaw/pkg/mcp (cached) +ok github.com/sipeed/picoclaw/pkg/media (cached) +ok github.com/sipeed/picoclaw/pkg/memory (cached) +ok github.com/sipeed/picoclaw/pkg/migrate (cached) +ok github.com/sipeed/picoclaw/pkg/migrate/internal (cached) +ok github.com/sipeed/picoclaw/pkg/migrate/sources/openclaw (cached) +ok github.com/sipeed/picoclaw/pkg/netbind (cached) +ok github.com/sipeed/picoclaw/pkg/pid (cached) +ok github.com/sipeed/picoclaw/pkg/providers (cached) +ok github.com/sipeed/picoclaw/pkg/providers/anthropic (cached) +ok github.com/sipeed/picoclaw/pkg/providers/anthropic_messages (cached) +ok github.com/sipeed/picoclaw/pkg/providers/azure (cached) +ok github.com/sipeed/picoclaw/pkg/providers/bedrock (cached) +ok github.com/sipeed/picoclaw/pkg/providers/cli (cached) +ok github.com/sipeed/picoclaw/pkg/providers/common (cached) +ok github.com/sipeed/picoclaw/pkg/providers/httpapi (cached) +ok github.com/sipeed/picoclaw/pkg/providers/oauth (cached) +ok github.com/sipeed/picoclaw/pkg/providers/openai_compat (cached) +ok github.com/sipeed/picoclaw/pkg/providers/openai_responses_common (cached) +? github.com/sipeed/picoclaw/pkg/providers/protocoltypes [no test files] +ok github.com/sipeed/picoclaw/pkg/routing (cached) +ok github.com/sipeed/picoclaw/pkg/seahorse (cached) +ok github.com/sipeed/picoclaw/pkg/security (cached) +ok github.com/sipeed/picoclaw/pkg/security/behavior (cached) +ok github.com/sipeed/picoclaw/pkg/security/canary (cached) +ok github.com/sipeed/picoclaw/pkg/security/ipia (cached) +ok github.com/sipeed/picoclaw/pkg/security/pii (cached) +ok github.com/sipeed/picoclaw/pkg/security/policy (cached) +ok github.com/sipeed/picoclaw/pkg/session (cached) +ok github.com/sipeed/picoclaw/pkg/skills (cached) +ok github.com/sipeed/picoclaw/pkg/state (cached) +? github.com/sipeed/picoclaw/pkg/tokenizer [no test files] +ok github.com/sipeed/picoclaw/pkg/tools (cached) +ok github.com/sipeed/picoclaw/pkg/tools/fs (cached) +? github.com/sipeed/picoclaw/pkg/tools/hardware [no test files] +ok github.com/sipeed/picoclaw/pkg/tools/integration (cached) +? github.com/sipeed/picoclaw/pkg/tools/shared [no test files] +ok github.com/sipeed/picoclaw/pkg/updater (cached) +ok github.com/sipeed/picoclaw/pkg/utils (cached) +ok github.com/sipeed/picoclaw/web/backend (cached) +ok github.com/sipeed/picoclaw/web/backend/api (cached) +? github.com/sipeed/picoclaw/web/backend/dashboardauth [no test files] +ok github.com/sipeed/picoclaw/web/backend/launcherconfig (cached) +ok github.com/sipeed/picoclaw/web/backend/middleware (cached) +? github.com/sipeed/picoclaw/web/backend/model [no test files] +ok github.com/sipeed/picoclaw/web/backend/utils (cached) +FAIL