Merge branch 'master'

This commit is contained in:
stevef 2026-04-20 08:48:41 +02:00
commit 0b5285aab3
10 changed files with 368 additions and 26 deletions

View file

@ -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"
],

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}
})
}
}

124
test_results.txt Normal file
View file

@ -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