style: fix gofumpt/gci formatting and bodyclose lint warning

Run gofumpt and gci to fix import ordering and formatting across
all files flagged by golangci-lint. Add nolint:bodyclose directive
for streaming HTTP response (body is closed in goroutine).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-28 23:48:27 +09:00
parent 9885bc1f36
commit 5801176dfe
27 changed files with 182 additions and 194 deletions

View file

@ -44,7 +44,7 @@ When results come back, synthesize and decide the next fork.`
type ContextBuilder struct { type ContextBuilder struct {
workspace string workspace string
workDir string // session-specific working directory (worktree or project subdir) workDir string // session-specific working directory (worktree or project subdir)
skillsLoader *skills.SkillsLoader skillsLoader *skills.SkillsLoader
memory *MemoryStore memory *MemoryStore
tools *tools.ToolRegistry // Direct reference to tool registry tools *tools.ToolRegistry // Direct reference to tool registry

View file

@ -19,30 +19,30 @@ import (
// AgentInstance represents a fully configured agent with its own workspace, // AgentInstance represents a fully configured agent with its own workspace,
// session manager, context builder, and tool registry. // session manager, context builder, and tool registry.
type AgentInstance struct { type AgentInstance struct {
ID string ID string
Name string Name string
Model string Model string
Fallbacks []string Fallbacks []string
Workspace string Workspace string
MaxIterations int MaxIterations int
TaskReminderInterval int TaskReminderInterval int
MaxTokens int MaxTokens int
Temperature float64 Temperature float64
ContextWindow int ContextWindow int
Provider providers.LLMProvider Provider providers.LLMProvider
Sessions *session.SessionManager Sessions *session.SessionManager
ContextBuilder *ContextBuilder ContextBuilder *ContextBuilder
Tools *tools.ToolRegistry Tools *tools.ToolRegistry
Subagents *config.SubagentsConfig Subagents *config.SubagentsConfig
SkillsFilter []string SkillsFilter []string
Candidates []providers.FallbackCandidate Candidates []providers.FallbackCandidate
PlanModel string PlanModel string
PlanFallbacks []string PlanFallbacks []string
PlanCandidates []providers.FallbackCandidate PlanCandidates []providers.FallbackCandidate
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated. // Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
interviewStaleCount int interviewStaleCount int
interviewMemoryLen int interviewMemoryLen int
// Per-session worktree isolation // Per-session worktree isolation
worktrees map[string]*git.WorktreeInfo // sessionKey → worktree worktrees map[string]*git.WorktreeInfo // sessionKey → worktree
@ -189,26 +189,26 @@ func NewAgentInstance(
} }
return &AgentInstance{ return &AgentInstance{
ID: agentID, ID: agentID,
Name: agentName, Name: agentName,
Model: model, Model: model,
Fallbacks: fallbacks, Fallbacks: fallbacks,
Workspace: workspace, Workspace: workspace,
MaxIterations: maxIter, MaxIterations: maxIter,
TaskReminderInterval: reminderInterval, TaskReminderInterval: reminderInterval,
MaxTokens: maxTokens, MaxTokens: maxTokens,
Temperature: temperature, Temperature: temperature,
ContextWindow: maxTokens, ContextWindow: maxTokens,
Provider: provider, Provider: provider,
Sessions: sessionsManager, Sessions: sessionsManager,
ContextBuilder: contextBuilder, ContextBuilder: contextBuilder,
Tools: toolsRegistry, Tools: toolsRegistry,
Subagents: subagents, Subagents: subagents,
SkillsFilter: skillsFilter, SkillsFilter: skillsFilter,
Candidates: candidates, Candidates: candidates,
PlanModel: planModel, PlanModel: planModel,
PlanFallbacks: planFallbacks, PlanFallbacks: planFallbacks,
PlanCandidates: planCandidates, PlanCandidates: planCandidates,
} }
} }

View file

@ -41,24 +41,24 @@ import (
// activeTask tracks a running agent task for live status and intervention. // activeTask tracks a running agent task for live status and intervention.
type activeTask struct { type activeTask struct {
Description string Description string
Iteration int Iteration int
MaxIter int MaxIter int
StartedAt time.Time StartedAt time.Time
cancel context.CancelFunc cancel context.CancelFunc
interrupt chan string // buffered 1, for user message injection interrupt chan string // buffered 1, for user message injection
toolLog []toolLogEntry toolLog []toolLogEntry
lastError *toolLogEntry // sticky: most recent error, persists across iterations lastError *toolLogEntry // sticky: most recent error, persists across iterations
projectDir string // detected from exec cd target (authoritative) projectDir string // detected from exec cd target (authoritative)
fileCommonDir string // LCP of file paths relative to workspace (fallback) fileCommonDir string // LCP of file paths relative to workspace (fallback)
mu sync.Mutex mu sync.Mutex
} }
// toolLogEntry records a single tool call for the live terminal view. // toolLogEntry records a single tool call for the live terminal view.
type toolLogEntry struct { type toolLogEntry struct {
Name string Name string
ArgsSnip string // first ~80 chars of args ArgsSnip string // first ~80 chars of args
Result string // "✓ 4.9s" or "✗ 3.2s" Result string // "✓ 4.9s" or "✗ 3.2s"
ErrDetail string // non-empty on error — e.g. "Exit code: exit status 1" ErrDetail string // non-empty on error — e.g. "Exit code: exit status 1"
} }
@ -78,26 +78,26 @@ func newSessionSemaphore() *sessionSemaphore {
} }
type AgentLoop struct { type AgentLoop struct {
bus *bus.MessageBus bus *bus.MessageBus
cfg *config.Config cfg *config.Config
registry *AgentRegistry registry *AgentRegistry
state *state.Manager state *state.Manager
stats *stats.Tracker // nil when --stats not passed stats *stats.Tracker // nil when --stats not passed
running atomic.Bool running atomic.Bool
summarizing sync.Map summarizing sync.Map
fallback *providers.FallbackChain fallback *providers.FallbackChain
channelManager *channels.Manager channelManager *channels.Manager
mediaStore media.MediaStore mediaStore media.MediaStore
providerCache map[string]providers.LLMProvider providerCache map[string]providers.LLMProvider
planStartPending bool // set by /plan start to trigger LLM execution planStartPending bool // set by /plan start to trigger LLM execution
planClearHistory bool // set by /plan start clear to wipe history on transition planClearHistory bool // set by /plan start clear to wipe history on transition
sessionLocks sync.Map // sessionKey → *sessionSemaphore sessionLocks sync.Map // sessionKey → *sessionSemaphore
activeTasks sync.Map // sessionKey → *activeTask activeTasks sync.Map // sessionKey → *activeTask
sessions *SessionTracker sessions *SessionTracker
lastSystemPrompt atomic.Value // string — last system prompt sent to LLM lastSystemPrompt atomic.Value // string — last system prompt sent to LLM
promptDirty atomic.Bool // true = rebuild needed on next GetSystemPrompt read promptDirty atomic.Bool // true = rebuild needed on next GetSystemPrompt read
OnStateChange func() // called on plan/session/skills mutations OnStateChange func() // called on plan/session/skills mutations
OnUserMessage func() // called when a real user message is processed OnUserMessage func() // called when a real user message is processed
orchBroadcaster *orch.Broadcaster // nil when --orchestration not set orchBroadcaster *orch.Broadcaster // nil when --orchestration not set
orchReporter orch.AgentReporter // always non-nil (Noop when disabled) orchReporter orch.AgentReporter // always non-nil (Noop when disabled)
} }
@ -601,8 +601,8 @@ func (al *AgentLoop) ProcessHeartbeat(ctx context.Context, content, channel, cha
DefaultResponse: defaultResponse, DefaultResponse: defaultResponse,
EnableSummary: false, EnableSummary: false,
SendResponse: false, SendResponse: false,
NoHistory: true, // Don't load session history for heartbeat NoHistory: true, // Don't load session history for heartbeat
Background: true, // Enable live task notifications on Telegram Background: true, // Enable live task notifications on Telegram
}) })
} }
@ -976,8 +976,8 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
if removedCount > 0 { if removedCount > 0 {
logger.WarnCF("agent", "Sanitized session history: removed orphaned messages", logger.WarnCF("agent", "Sanitized session history: removed orphaned messages",
map[string]any{ map[string]any{
"session_key": opts.SessionKey, "session_key": opts.SessionKey,
"removed_count": removedCount, "removed_count": removedCount,
}) })
// Persist the sanitized history // Persist the sanitized history
agent.Sessions.SetHistory(opts.SessionKey, history) agent.Sessions.SetHistory(opts.SessionKey, history)
@ -1174,8 +1174,10 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
} }
// Task reminder constants and helpers. // Task reminder constants and helpers.
const taskReminderMaxChars = 500 const (
const blockerMaxChars = 200 taskReminderMaxChars = 500
blockerMaxChars = 200
)
func shouldInjectReminder(iteration, interval int) bool { func shouldInjectReminder(iteration, interval int) bool {
if interval <= 0 { if interval <= 0 {
@ -1361,7 +1363,7 @@ func buildArgsSnippet(toolName string, args map[string]interface{}, workspace st
if runes := []rune(path); len(runes) > maxPath { if runes := []rune(path); len(runes) > maxPath {
// Find last slash to extract filename // Find last slash to extract filename
if lastSlash := strings.LastIndex(path, "/"); lastSlash >= 0 { if lastSlash := strings.LastIndex(path, "/"); lastSlash >= 0 {
filename := path[lastSlash:] // includes "/" filename := path[lastSlash:] // includes "/"
dirBudget := maxPath - len([]rune(filename)) - 1 // 1 for "…" dirBudget := maxPath - len([]rune(filename)) - 1 // 1 for "…"
if dirBudget > 0 { if dirBudget > 0 {
dir := []rune(path[:lastSlash]) dir := []rune(path[:lastSlash])
@ -1509,10 +1511,10 @@ func compressRepeats(s string) string {
// Display layout constants. // Display layout constants.
const ( const (
displayPastEntries = 4 // number of compact 1-line past entries displayPastEntries = 4 // number of compact 1-line past entries
displayErrorLines = 5 // content lines inside the error code block displayErrorLines = 5 // content lines inside the error code block
statusSeparator = "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" statusSeparator = "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"
streamingDisplayLines = 17 // line count matching buildRichStatus output streamingDisplayLines = 17 // line count matching buildRichStatus output
) )
// buildRichStatus builds a fixed-height terminal-like status display. // buildRichStatus builds a fixed-height terminal-like status display.
@ -3338,16 +3340,16 @@ func isPlanPreExecution(status string) bool {
// filterInterviewTools uses this to strip tool *definitions* before the LLM call, // filterInterviewTools uses this to strip tool *definitions* before the LLM call,
// while isToolAllowedDuringInterview adds argument-level checks as a second gate. // while isToolAllowedDuringInterview adds argument-level checks as a second gate.
var interviewAllowedTools = map[string]bool{ var interviewAllowedTools = map[string]bool{
"readfile": true, "readfile": true,
"listdir": true, "listdir": true,
"websearch": true, "websearch": true,
"webfetch": true, "webfetch": true,
"message": true, "message": true,
"editfile": true, "editfile": true,
"appendfile": true, "appendfile": true,
"writefile": true, "writefile": true,
"exec": true, "exec": true,
"logs": true, "logs": true,
} }
// filterInterviewTools removes tool definitions that are not in the // filterInterviewTools removes tool definitions that are not in the

View file

@ -1664,8 +1664,8 @@ func TestBuildRichStatus(t *testing.T) {
mustContain := []string{ mustContain := []string{
"Task in progress (3/20)", "Task in progress (3/20)",
"my-projects", "my-projects",
"read_file", // latest entry "read_file", // latest entry
"No errors", // no error yet "No errors", // no error yet
} }
for _, s := range mustContain { for _, s := range mustContain {
if !strings.Contains(got, s) { if !strings.Contains(got, s) {
@ -1833,15 +1833,18 @@ func TestBuildRichStatus_FixedHeight(t *testing.T) {
lines0 := countLines(buildRichStatus(task0, true, "/ws/p")) lines0 := countLines(buildRichStatus(task0, true, "/ws/p"))
// 1 entry // 1 entry
task1 := &activeTask{Iteration: 1, MaxIter: 10, task1 := &activeTask{
toolLog: []toolLogEntry{{Name: "exec", ArgsSnip: "ls", Result: "⏳"}}} Iteration: 1, MaxIter: 10,
toolLog: []toolLogEntry{{Name: "exec", ArgsSnip: "ls", Result: "⏳"}},
}
lines1 := countLines(buildRichStatus(task1, true, "/ws/p")) lines1 := countLines(buildRichStatus(task1, true, "/ws/p"))
// 5 entries // 5 entries
task5 := &activeTask{Iteration: 5, MaxIter: 10} task5 := &activeTask{Iteration: 5, MaxIter: 10}
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
task5.toolLog = append(task5.toolLog, toolLogEntry{ task5.toolLog = append(task5.toolLog, toolLogEntry{
Name: fmt.Sprintf("[%d] exec", i), ArgsSnip: "cmd", Result: "✓ 1.0s"}) Name: fmt.Sprintf("[%d] exec", i), ArgsSnip: "cmd", Result: "✓ 1.0s",
})
} }
lines5 := countLines(buildRichStatus(task5, true, "/ws/p")) lines5 := countLines(buildRichStatus(task5, true, "/ws/p"))
@ -1849,10 +1852,13 @@ func TestBuildRichStatus_FixedHeight(t *testing.T) {
task5err := &activeTask{Iteration: 5, MaxIter: 10} task5err := &activeTask{Iteration: 5, MaxIter: 10}
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
task5err.toolLog = append(task5err.toolLog, toolLogEntry{ task5err.toolLog = append(task5err.toolLog, toolLogEntry{
Name: fmt.Sprintf("[%d] exec", i), ArgsSnip: "cmd", Result: "✓ 1.0s"}) Name: fmt.Sprintf("[%d] exec", i), ArgsSnip: "cmd", Result: "✓ 1.0s",
})
}
errEntry := toolLogEntry{
Name: "[3] exec", ArgsSnip: "pytest", Result: "✗ 2.0s",
ErrDetail: "FAILED test\nExit code: 1",
} }
errEntry := toolLogEntry{Name: "[3] exec", ArgsSnip: "pytest", Result: "✗ 2.0s",
ErrDetail: "FAILED test\nExit code: 1"}
task5err.lastError = &errEntry task5err.lastError = &errEntry
lines5err := countLines(buildRichStatus(task5err, true, "/ws/p")) lines5err := countLines(buildRichStatus(task5err, true, "/ws/p"))
@ -2433,9 +2439,9 @@ func TestConsumeStream_OnChunkWithRepetitionDetection(t *testing.T) {
// modelCapturingMockProvider records which model was passed to Chat. // modelCapturingMockProvider records which model was passed to Chat.
type modelCapturingMockProvider struct { type modelCapturingMockProvider struct {
mu sync.Mutex mu sync.Mutex
models []string models []string
response string response string
} }
func (m *modelCapturingMockProvider) Chat( func (m *modelCapturingMockProvider) Chat(

View file

@ -171,16 +171,16 @@ type SessionConfig struct {
} }
type AgentDefaults struct { type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead
ModelFallbacks []string `json:"model_fallbacks,omitempty"` ModelFallbacks []string `json:"model_fallbacks,omitempty"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
PlanModel string `json:"plan_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_PLAN_MODEL"` PlanModel string `json:"plan_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_PLAN_MODEL"`
PlanModelFallbacks []string `json:"plan_model_fallbacks,omitempty"` PlanModelFallbacks []string `json:"plan_model_fallbacks,omitempty"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`

View file

@ -10,12 +10,12 @@ func DefaultConfig() *Config {
return &Config{ return &Config{
Agents: AgentsConfig{ Agents: AgentsConfig{
Defaults: AgentDefaults{ Defaults: AgentDefaults{
Workspace: "~/.picoclaw/workspace", Workspace: "~/.picoclaw/workspace",
RestrictToWorkspace: true, RestrictToWorkspace: true,
Provider: "", Provider: "",
Model: "", Model: "",
MaxTokens: 32768, MaxTokens: 32768,
Temperature: nil, // nil means use provider default Temperature: nil, // nil means use provider default
MaxToolIterations: 50, MaxToolIterations: 50,
TaskReminderInterval: 5, TaskReminderInterval: 5,
}, },

View file

@ -294,10 +294,10 @@ func TestUnsubscribe_ClosesChannel(t *testing.T) {
func TestSanitizeFields(t *testing.T) { func TestSanitizeFields(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input map[string]any input map[string]any
maskedK []string // keys that should be "***" maskedK []string // keys that should be "***"
safeK []string // keys that should keep original value safeK []string // keys that should keep original value
}{ }{
{ {
name: "nil fields", name: "nil fields",
@ -320,9 +320,9 @@ func TestSanitizeFields(t *testing.T) {
maskedK: []string{"Token", "API_KEY", "Secret", "PASSWORD", "Authorization", "Credential"}, maskedK: []string{"Token", "API_KEY", "Secret", "PASSWORD", "Authorization", "Credential"},
}, },
{ {
name: "safe keys preserved", name: "safe keys preserved",
input: map[string]any{"error": "something failed", "count": 42, "user_id": "12345", "component": "test"}, input: map[string]any{"error": "something failed", "count": 42, "user_id": "12345", "component": "test"},
safeK: []string{"error", "count", "user_id", "component"}, safeK: []string{"error", "count", "user_id", "component"},
}, },
{ {
name: "mixed keys", name: "mixed keys",
@ -363,9 +363,9 @@ func TestRecentLogsSanitizesFields(t *testing.T) {
SetLevel(DEBUG) SetLevel(DEBUG)
InfoCF("sanitize-test", "log with sensitive fields", map[string]any{ InfoCF("sanitize-test", "log with sensitive fields", map[string]any{
"token": "my-secret-token", "token": "my-secret-token",
"api_key": "sk-12345", "api_key": "sk-12345",
"user_id": "safe-value", "user_id": "safe-value",
}) })
got := RecentLogs(DEBUG, "sanitize-test", 100) got := RecentLogs(DEBUG, "sanitize-test", 100)

View file

@ -10,19 +10,16 @@ import (
"time" "time"
) )
func (h *Handler) apiSkills(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiSkills(w http.ResponseWriter, r *http.Request) {
skillsList := h.provider.ListSkills() skillsList := h.provider.ListSkills()
writeJSON(w, skillsList) writeJSON(w, skillsList)
} }
func (h *Handler) apiPlan(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiPlan(w http.ResponseWriter, r *http.Request) {
info := h.provider.GetPlanInfo() info := h.provider.GetPlanInfo()
writeJSON(w, info) writeJSON(w, info)
} }
func (h *Handler) apiSessions(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiSessions(w http.ResponseWriter, r *http.Request) {
sessions := h.provider.GetActiveSessions() sessions := h.provider.GetActiveSessions()
if sessions == nil { if sessions == nil {
@ -31,7 +28,6 @@ func (h *Handler) apiSessions(w http.ResponseWriter, r *http.Request) {
writeJSON(w, sessions) writeJSON(w, sessions)
} }
func (h *Handler) apiSession(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiSession(w http.ResponseWriter, r *http.Request) {
s := h.provider.GetSessionStats() s := h.provider.GetSessionStats()
if s == nil { if s == nil {
@ -41,17 +37,14 @@ func (h *Handler) apiSession(w http.ResponseWriter, r *http.Request) {
writeJSON(w, s) writeJSON(w, s)
} }
func (h *Handler) apiContext(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiContext(w http.ResponseWriter, r *http.Request) {
writeJSON(w, h.provider.GetContextInfo()) writeJSON(w, h.provider.GetContextInfo())
} }
func (h *Handler) apiPrompt(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiPrompt(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{"prompt": h.provider.GetSystemPrompt()}) writeJSON(w, map[string]string{"prompt": h.provider.GetSystemPrompt()})
} }
func (h *Handler) apiGit(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiGit(w http.ResponseWriter, r *http.Request) {
repo := r.URL.Query().Get("repo") repo := r.URL.Query().Get("repo")
if repo == "" { if repo == "" {
@ -61,7 +54,6 @@ func (h *Handler) apiGit(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *Handler) apiCommand(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiCommand(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
@ -99,7 +91,6 @@ func (h *Handler) apiCommand(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{"status": "ok"}) writeJSON(w, map[string]string{"status": "ok"})
} }
func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
@ -148,7 +139,6 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
} }
} }
func sendSSEIfChanged(w http.ResponseWriter, f http.Flusher, event string, v any, last *[]byte) { func sendSSEIfChanged(w http.ResponseWriter, f http.Flusher, event string, v any, last *[]byte) {
data, _ := json.Marshal(v) data, _ := json.Marshal(v)
if !bytes.Equal(data, *last) { if !bytes.Equal(data, *last) {
@ -158,11 +148,9 @@ func sendSSEIfChanged(w http.ResponseWriter, f http.Flusher, event string, v any
} }
} }
func writeJSON(w http.ResponseWriter, v any) { func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v) json.NewEncoder(w).Encode(v)
} }
// apiDevConsole receives console output from dev preview iframes. // apiDevConsole receives console output from dev preview iframes.

View file

@ -17,7 +17,6 @@ import (
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
// validateLocalhostURL parses and validates that a URL targets localhost. // validateLocalhostURL parses and validates that a URL targets localhost.
func validateLocalhostURL(target string) (*url.URL, error) { func validateLocalhostURL(target string) (*url.URL, error) {
u, err := url.Parse(target) u, err := url.Parse(target)
@ -33,7 +32,6 @@ func validateLocalhostURL(target string) (*url.URL, error) {
// RegisterDevTarget registers a new dev server target. Only localhost targets are allowed. // RegisterDevTarget registers a new dev server target. Only localhost targets are allowed.
// RegisterDevTarget registers a new dev server target. Only localhost targets are allowed. // RegisterDevTarget registers a new dev server target. Only localhost targets are allowed.
func (h *Handler) RegisterDevTarget(name, target string) (string, error) { func (h *Handler) RegisterDevTarget(name, target string) (string, error) {
if _, err := validateLocalhostURL(target); err != nil { if _, err := validateLocalhostURL(target); err != nil {
@ -55,7 +53,6 @@ func (h *Handler) RegisterDevTarget(name, target string) (string, error) {
// UnregisterDevTarget removes a registered target. If it was active, the proxy is disabled. // UnregisterDevTarget removes a registered target. If it was active, the proxy is disabled.
// UnregisterDevTarget removes a registered target. If it was active, the proxy is disabled. // UnregisterDevTarget removes a registered target. If it was active, the proxy is disabled.
func (h *Handler) UnregisterDevTarget(id string) error { func (h *Handler) UnregisterDevTarget(id string) error {
h.devMu.Lock() h.devMu.Lock()
@ -79,7 +76,6 @@ func (h *Handler) UnregisterDevTarget(id string) error {
// ActivateDevTarget sets the reverse proxy to the registered target with the given ID. // ActivateDevTarget sets the reverse proxy to the registered target with the given ID.
// ActivateDevTarget sets the reverse proxy to the registered target with the given ID. // ActivateDevTarget sets the reverse proxy to the registered target with the given ID.
func (h *Handler) ActivateDevTarget(id string) error { func (h *Handler) ActivateDevTarget(id string) error {
h.devMu.Lock() h.devMu.Lock()
@ -148,7 +144,6 @@ p{color:#8e8e93;font-size:14px;margin:0}
// DeactivateDevTarget disables the reverse proxy without removing registrations. // DeactivateDevTarget disables the reverse proxy without removing registrations.
// DeactivateDevTarget disables the reverse proxy without removing registrations. // DeactivateDevTarget disables the reverse proxy without removing registrations.
func (h *Handler) DeactivateDevTarget() error { func (h *Handler) DeactivateDevTarget() error {
h.devMu.Lock() h.devMu.Lock()
@ -165,7 +160,6 @@ func (h *Handler) DeactivateDevTarget() error {
// GetDevTarget returns the current dev proxy target URL, or empty string if disabled. // GetDevTarget returns the current dev proxy target URL, or empty string if disabled.
// GetDevTarget returns the current dev proxy target URL, or empty string if disabled. // GetDevTarget returns the current dev proxy target URL, or empty string if disabled.
func (h *Handler) GetDevTarget() string { func (h *Handler) GetDevTarget() string {
h.devMu.RLock() h.devMu.RLock()
@ -178,7 +172,6 @@ func (h *Handler) GetDevTarget() string {
// ListDevTargets returns all registered dev targets. // ListDevTargets returns all registered dev targets.
// ListDevTargets returns all registered dev targets. // ListDevTargets returns all registered dev targets.
func (h *Handler) ListDevTargets() []DevTarget { func (h *Handler) ListDevTargets() []DevTarget {
h.devMu.RLock() h.devMu.RLock()
@ -198,7 +191,6 @@ func (h *Handler) ListDevTargets() []DevTarget {
// "/api/items" are prefixed with "/miniapp/dev", matching the reverse proxy mount. // "/api/items" are prefixed with "/miniapp/dev", matching the reverse proxy mount.
// It also captures console.log/warn/error/info and forwards them to the server. // It also captures console.log/warn/error/info and forwards them to the server.
// devProxyScript is the JavaScript injected into HTML responses from the dev proxy. // devProxyScript is the JavaScript injected into HTML responses from the dev proxy.
// It rewrites fetch() and XMLHttpRequest.open() so that absolute paths like // It rewrites fetch() and XMLHttpRequest.open() so that absolute paths like
// "/api/items" are prefixed with "/miniapp/dev", matching the reverse proxy mount. // "/api/items" are prefixed with "/miniapp/dev", matching the reverse proxy mount.
@ -255,7 +247,6 @@ const devProxyScript = `<script data-dev-proxy>
// injectDevProxyScript inserts the dev proxy rewrite script into an HTML document. // injectDevProxyScript inserts the dev proxy rewrite script into an HTML document.
// Insertion priority: before </head>, after <body...>, or prepend to document. // Insertion priority: before </head>, after <body...>, or prepend to document.
// injectDevProxyScript inserts the dev proxy rewrite script into an HTML document. // injectDevProxyScript inserts the dev proxy rewrite script into an HTML document.
// Insertion priority: before </head>, after <body...>, or prepend to document. // Insertion priority: before </head>, after <body...>, or prepend to document.
func injectDevProxyScript(html []byte) []byte { func injectDevProxyScript(html []byte) []byte {
@ -294,7 +285,6 @@ func injectDevProxyScript(html []byte) []byte {
// escapeHTMLString escapes HTML special characters in a string. // escapeHTMLString escapes HTML special characters in a string.
// escapeHTMLString escapes HTML special characters in a string. // escapeHTMLString escapes HTML special characters in a string.
func escapeHTMLString(s string) string { func escapeHTMLString(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;") s = strings.ReplaceAll(s, "&", "&amp;")
@ -306,7 +296,6 @@ func escapeHTMLString(s string) string {
// RegisterRoutes registers Mini App routes on the given mux. // RegisterRoutes registers Mini App routes on the given mux.
func (h *Handler) apiDev(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiDev(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
@ -359,7 +348,6 @@ func (h *Handler) apiDev(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *Handler) serveDevProxy(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveDevProxy(w http.ResponseWriter, r *http.Request) {
h.devMu.RLock() h.devMu.RLock()
proxy := h.devProxy proxy := h.devProxy
@ -381,7 +369,6 @@ func (h *Handler) serveDevProxy(w http.ResponseWriter, r *http.Request) {
// extractUserFromInitData parses user.id from the initData query string. // extractUserFromInitData parses user.id from the initData query string.
// initData contains a "user" param with JSON like {"id":123456,...}. // initData contains a "user" param with JSON like {"id":123456,...}.
func (h *Handler) devStatus() map[string]any { func (h *Handler) devStatus() map[string]any {
h.devMu.RLock() h.devMu.RLock()
defer h.devMu.RUnlock() defer h.devMu.RUnlock()
@ -406,7 +393,6 @@ func (h *Handler) devStatus() map[string]any {
} }
} }
// apiDevConsole receives console output from dev preview iframes. // apiDevConsole receives console output from dev preview iframes.
func (h *Handler) apiDevConsole(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiDevConsole(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -474,4 +460,3 @@ func (h *Handler) apiDevConsole(w http.ResponseWriter, r *http.Request) {
} }
// wsLogs serves a WebSocket endpoint that streams log entries in real time. // wsLogs serves a WebSocket endpoint that streams log entries in real time.

View file

@ -14,7 +14,6 @@ import (
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
// apiLogsSnapshot creates a tar.gz snapshot of the current log buffer. // apiLogsSnapshot creates a tar.gz snapshot of the current log buffer.
func (h *Handler) apiLogsSnapshot(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiLogsSnapshot(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -86,7 +85,6 @@ func (h *Handler) apiLogsSnapshot(w http.ResponseWriter, r *http.Request) {
// apiLogsSnapshotDownload serves a snapshot tar.gz file. // apiLogsSnapshotDownload serves a snapshot tar.gz file.
// apiLogsSnapshotDownload serves a snapshot tar.gz file. // apiLogsSnapshotDownload serves a snapshot tar.gz file.
func (h *Handler) apiLogsSnapshotDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiLogsSnapshotDownload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
@ -117,7 +115,6 @@ func (h *Handler) apiLogsSnapshotDownload(w http.ResponseWriter, r *http.Request
// cleanOldSnapshots removes snapshot files older than maxAge. // cleanOldSnapshots removes snapshot files older than maxAge.
// cleanOldSnapshots removes snapshot files older than maxAge. // cleanOldSnapshots removes snapshot files older than maxAge.
func cleanOldSnapshots(dir string, maxAge time.Duration) { func cleanOldSnapshots(dir string, maxAge time.Duration) {
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
@ -140,4 +137,3 @@ func cleanOldSnapshots(dir string, maxAge time.Duration) {
} }
// initDataMaxAge is the maximum age of initData before it is considered expired. // initDataMaxAge is the maximum age of initData before it is considered expired.

View file

@ -175,24 +175,31 @@ type mockDataProvider struct{}
func (m *mockDataProvider) ListSkills() []skills.SkillInfo { func (m *mockDataProvider) ListSkills() []skills.SkillInfo {
return []skills.SkillInfo{{Name: "test-skill", Description: "A test", Source: "local"}} return []skills.SkillInfo{{Name: "test-skill", Description: "A test", Source: "local"}}
} }
func (m *mockDataProvider) GetPlanInfo() PlanInfo { func (m *mockDataProvider) GetPlanInfo() PlanInfo {
return PlanInfo{HasPlan: false, Status: "none"} return PlanInfo{HasPlan: false, Status: "none"}
} }
func (m *mockDataProvider) GetSessionStats() *stats.Stats { func (m *mockDataProvider) GetSessionStats() *stats.Stats {
return nil return nil
} }
func (m *mockDataProvider) GetActiveSessions() []SessionInfo { func (m *mockDataProvider) GetActiveSessions() []SessionInfo {
return []SessionInfo{} return []SessionInfo{}
} }
func (m *mockDataProvider) GetGitRepos() []GitRepoSummary { func (m *mockDataProvider) GetGitRepos() []GitRepoSummary {
return nil return nil
} }
func (m *mockDataProvider) GetGitRepoDetail(name string) GitInfo { func (m *mockDataProvider) GetGitRepoDetail(name string) GitInfo {
return GitInfo{Name: name} return GitInfo{Name: name}
} }
func (m *mockDataProvider) GetContextInfo() ContextInfo { func (m *mockDataProvider) GetContextInfo() ContextInfo {
return ContextInfo{Workspace: "/mock/workspace"} return ContextInfo{Workspace: "/mock/workspace"}
} }
func (m *mockDataProvider) GetSystemPrompt() string { func (m *mockDataProvider) GetSystemPrompt() string {
return "mock system prompt" return "mock system prompt"
} }
@ -464,6 +471,7 @@ type mutatingDataProvider struct {
func (m *mutatingDataProvider) ListSkills() []skills.SkillInfo { func (m *mutatingDataProvider) ListSkills() []skills.SkillInfo {
return []skills.SkillInfo{{Name: "test-skill", Description: "A test", Source: "local"}} return []skills.SkillInfo{{Name: "test-skill", Description: "A test", Source: "local"}}
} }
func (m *mutatingDataProvider) GetPlanInfo() PlanInfo { func (m *mutatingDataProvider) GetPlanInfo() PlanInfo {
if m.mutated.Load() { if m.mutated.Load() {
return PlanInfo{HasPlan: true, Status: "executing", CurrentPhase: 1, TotalPhases: 2} return PlanInfo{HasPlan: true, Status: "executing", CurrentPhase: 1, TotalPhases: 2}
@ -474,15 +482,19 @@ func (m *mutatingDataProvider) GetSessionStats() *stats.Stats { return nil }
func (m *mutatingDataProvider) GetActiveSessions() []SessionInfo { func (m *mutatingDataProvider) GetActiveSessions() []SessionInfo {
return []SessionInfo{} return []SessionInfo{}
} }
func (m *mutatingDataProvider) GetGitRepos() []GitRepoSummary { func (m *mutatingDataProvider) GetGitRepos() []GitRepoSummary {
return nil return nil
} }
func (m *mutatingDataProvider) GetGitRepoDetail(name string) GitInfo { func (m *mutatingDataProvider) GetGitRepoDetail(name string) GitInfo {
return GitInfo{Name: name} return GitInfo{Name: name}
} }
func (m *mutatingDataProvider) GetContextInfo() ContextInfo { func (m *mutatingDataProvider) GetContextInfo() ContextInfo {
return ContextInfo{Workspace: "/mock/workspace"} return ContextInfo{Workspace: "/mock/workspace"}
} }
func (m *mutatingDataProvider) GetSystemPrompt() string { func (m *mutatingDataProvider) GetSystemPrompt() string {
return "mock system prompt" return "mock system prompt"
} }

View file

@ -7,10 +7,10 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
const maxWSClients = 4 const maxWSClients = 4
const ( const (
@ -22,7 +22,6 @@ type wsClient struct {
conn *websocket.Conn conn *websocket.Conn
} }
var wsUpgrader = websocket.Upgrader{ var wsUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
@ -43,7 +42,6 @@ var wsUpgrader = websocket.Upgrader{
// NewHandler creates a new Mini App handler. // NewHandler creates a new Mini App handler.
// wsLogs serves a WebSocket endpoint that streams log entries in real time. // wsLogs serves a WebSocket endpoint that streams log entries in real time.
func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) { func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) {
// Parse filter params // Parse filter params
@ -136,7 +134,7 @@ func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
entry.Caller = "" // strip for security entry.Caller = "" // strip for security
entry.Fields = logger.SanitizeFields(entry.Fields) // mask sensitive values entry.Fields = logger.SanitizeFields(entry.Fields) // mask sensitive values
if err := conn.WriteJSON(map[string]any{"type": "entry", "entry": entry}); err != nil { if err := conn.WriteJSON(map[string]any{"type": "entry", "entry": entry}); err != nil {
return return

View file

@ -14,8 +14,8 @@ type Event struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
Task string `json:"task,omitempty"` Task string `json:"task,omitempty"`
State string `json:"state,omitempty"` // waiting | toolcall | idle State string `json:"state,omitempty"` // waiting | toolcall | idle
Tool string `json:"tool,omitempty"` // tool name during toolcall Tool string `json:"tool,omitempty"` // tool name during toolcall
From string `json:"from,omitempty"` From string `json:"from,omitempty"`
To string `json:"to,omitempty"` To string `json:"to,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`

View file

@ -272,7 +272,7 @@ func (p *Provider) ChatStream(
return nil, err return nil, err
} }
resp, err := p.httpClient.Do(req) resp, err := p.httpClient.Do(req) //nolint:bodyclose // closed in goroutine or error path below
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err) return nil, fmt.Errorf("failed to send request: %w", err)
} }
@ -626,4 +626,3 @@ type streamToolCallAcc struct {
Name string Name string
Arguments strings.Builder Arguments strings.Builder
} }

View file

@ -186,7 +186,7 @@ func findToolCallBlock(text string) (blockStart, blockEnd int, content string, f
return 0, 0, "", false return 0, 0, "", false
} }
// --- XML tool call extraction --- // ExtractXMLToolCalls extracts tool calls from XML-formatted text.
// //
// Expected format: // Expected format:
// //
@ -195,7 +195,6 @@ func findToolCallBlock(text string) (blockStart, blockEnd int, content string, f
// <parameter name="param">value</parameter> // <parameter name="param">value</parameter>
// </invoke> // </invoke>
// </ns:toolcall> // </ns:toolcall>
// ExtractXMLToolCalls is the exported version for use by the agent loop.
func ExtractXMLToolCalls(text string) []ToolCall { func ExtractXMLToolCalls(text string) []ToolCall {
return extractXMLToolCalls(text) return extractXMLToolCalls(text)
} }

View file

@ -91,7 +91,7 @@ func TestSanitizeHistory_InterleavedMessages(t *testing.T) {
{Role: "assistant", Content: "ok", ToolCalls: []providers.ToolCall{ {Role: "assistant", Content: "ok", ToolCalls: []providers.ToolCall{
{ID: "call_1", Name: "exec"}, {ID: "call_1", Name: "exec"},
}}, }},
{Role: "user", Content: "collision!"}, // ← interleaved from other session {Role: "user", Content: "collision!"}, // ← interleaved from other session
{Role: "tool", Content: "ok", ToolCallID: "call_1"}, // ← out of order {Role: "tool", Content: "ok", ToolCallID: "call_1"}, // ← out of order
{Role: "assistant", Content: "done"}, {Role: "assistant", Content: "done"},
} }

View file

@ -11,7 +11,7 @@ import (
// DayStats holds token usage for a single day. // DayStats holds token usage for a single day.
type DayStats struct { type DayStats struct {
Date string `json:"date"` // "2006-01-02" Date string `json:"date"` // "2006-01-02"
PromptTokens int64 `json:"prompt_tokens"` PromptTokens int64 `json:"prompt_tokens"`
CompletionTokens int64 `json:"completion_tokens"` CompletionTokens int64 `json:"completion_tokens"`
TotalTokens int64 `json:"total_tokens"` TotalTokens int64 `json:"total_tokens"`
@ -43,7 +43,7 @@ type Tracker struct {
// NewTracker creates a tracker that persists to {workspace}/state/stats.json. // NewTracker creates a tracker that persists to {workspace}/state/stats.json.
func NewTracker(workspace string) *Tracker { func NewTracker(workspace string) *Tracker {
stateDir := filepath.Join(workspace, "state") stateDir := filepath.Join(workspace, "state")
os.MkdirAll(stateDir, 0755) os.MkdirAll(stateDir, 0o755)
t := &Tracker{ t := &Tracker{
stateFile: filepath.Join(stateDir, "stats.json"), stateFile: filepath.Join(stateDir, "stats.json"),
@ -135,7 +135,7 @@ func (t *Tracker) save() {
return return
} }
tmp := t.stateFile + ".tmp" tmp := t.stateFile + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil { if err := os.WriteFile(tmp, data, 0o644); err != nil {
return return
} }
if err := os.Rename(tmp, t.stateFile); err != nil { if err := os.Rename(tmp, t.stateFile); err != nil {

View file

@ -202,8 +202,8 @@ func TestDevPreviewTool_UnregisterNotFound(t *testing.T) {
tool := NewDevPreviewTool(mgr) tool := NewDevPreviewTool(mgr)
result := tool.Execute(context.Background(), map[string]any{ result := tool.Execute(context.Background(), map[string]any{
"action": "unregister", "action": "unregister",
"id": "999", "id": "999",
}) })
if !result.IsError { if !result.IsError {
@ -421,7 +421,7 @@ func TestDevPreviewTool_InferName(t *testing.T) {
{"http://127.0.0.1:9000", "127.0.0.1:9000"}, {"http://127.0.0.1:9000", "127.0.0.1:9000"},
{"http://localhost", "localhost"}, {"http://localhost", "localhost"},
{"http://[::1]:5000", "::1:5000"}, {"http://[::1]:5000", "::1:5000"},
{"not-a-url", ""}, // url.Parse succeeds but Hostname() is empty {"not-a-url", ""}, // url.Parse succeeds but Hostname() is empty
} }
for _, tc := range cases { for _, tc := range cases {
got := inferName(tc.target) got := inferName(tc.target)

View file

@ -20,8 +20,8 @@ func setupTestLogs(t *testing.T) {
logger.WarnC("telegram", "webhook retry") logger.WarnC("telegram", "webhook retry")
logger.ErrorC("discord", "connection timeout") logger.ErrorC("discord", "connection timeout")
logger.WarnCF("wecom", "signature failed", map[string]any{ logger.WarnCF("wecom", "signature failed", map[string]any{
"token": "secret-value", "token": "secret-value",
"nonce": "safe-value", "nonce": "safe-value",
}) })
} }

View file

@ -27,11 +27,11 @@ type ExecPolicy struct {
// SandboxConfig describes the sandbox isolation policy for a preset. // SandboxConfig describes the sandbox isolation policy for a preset.
type SandboxConfig struct { type SandboxConfig struct {
Preset Preset Preset Preset
WriteRoot string // Path restriction for write tools; empty = no writes allowed WriteRoot string // Path restriction for write tools; empty = no writes allowed
AllowedTools map[string]bool // Tools that can be used AllowedTools map[string]bool // Tools that can be used
ExecPolicy *ExecPolicy // nil = exec not allowed ExecPolicy *ExecPolicy // nil = exec not allowed
SpawnablePresets []string // Presets that can be spawned; nil = spawn not allowed SpawnablePresets []string // Presets that can be spawned; nil = spawn not allowed
} }
// SubagentEnvironment provides context for subagent execution. // SubagentEnvironment provides context for subagent execution.
@ -65,11 +65,11 @@ var presetSpawnablePresets = map[Preset][]string{
func AllowedToolsForPreset(p Preset) map[string]bool { func AllowedToolsForPreset(p Preset) map[string]bool {
// Base tools available to all presets // Base tools available to all presets
allowed := map[string]bool{ allowed := map[string]bool{
"read_file": true, "read_file": true,
"list_dir": true, "list_dir": true,
"web_search": true, "web_search": true,
"web_fetch": true, "web_fetch": true,
"message": true, "message": true,
} }
// Add analyst+ tools (exec, git, etc.) // Add analyst+ tools (exec, git, etc.)

View file

@ -8,13 +8,13 @@ import (
// TestAllowedToolsForPreset checks that each preset has appropriate tool access. // TestAllowedToolsForPreset checks that each preset has appropriate tool access.
func TestAllowedToolsForPreset(t *testing.T) { func TestAllowedToolsForPreset(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
preset Preset preset Preset
wantRead bool wantRead bool
wantWrite bool wantWrite bool
wantExec bool wantExec bool
wantSpawn bool wantSpawn bool
wantWebSearch bool wantWebSearch bool
}{ }{
{ {
name: "scout", name: "scout",

View file

@ -808,7 +808,7 @@ func isExecutable(path string) bool {
} }
return false return false
} }
return info.Mode()&0111 != 0 return info.Mode()&0o111 != 0
} }
func (t *ExecTool) SetTimeout(timeout time.Duration) { func (t *ExecTool) SetTimeout(timeout time.Duration) {

View file

@ -370,7 +370,7 @@ func TestGuardCommand_ExecutableBinaryAllowed(t *testing.T) {
// Create a fake executable outside the workspace // Create a fake executable outside the workspace
execPath := filepath.Join(externalDir, "mybin") execPath := filepath.Join(externalDir, "mybin")
os.WriteFile(execPath, []byte("#!/bin/sh\necho ok"), 0755) os.WriteFile(execPath, []byte("#!/bin/sh\necho ok"), 0o755)
tool, _ := NewExecTool(workspace, true) tool, _ := NewExecTool(workspace, true)
@ -393,7 +393,7 @@ func TestGuardCommand_ExecutableBinaryAllowed_Windows(t *testing.T) {
// Create a fake .exe outside the workspace // Create a fake .exe outside the workspace
execPath := filepath.Join(externalDir, "tool.exe") execPath := filepath.Join(externalDir, "tool.exe")
os.WriteFile(execPath, []byte("MZ"), 0644) os.WriteFile(execPath, []byte("MZ"), 0o644)
tool, _ := NewExecTool(workspace, true) tool, _ := NewExecTool(workspace, true)
@ -416,7 +416,7 @@ func TestGuardCommand_NonExecutableOutsideBlocked(t *testing.T) {
// Create a regular (non-executable) file outside workspace // Create a regular (non-executable) file outside workspace
dataFile := filepath.Join(externalDir, "secret.txt") dataFile := filepath.Join(externalDir, "secret.txt")
os.WriteFile(dataFile, []byte("secret data"), 0644) os.WriteFile(dataFile, []byte("secret data"), 0o644)
tool, _ := NewExecTool(workspace, true) tool, _ := NewExecTool(workspace, true)
@ -477,7 +477,7 @@ func TestGuardCommand_AbsolutePathInsideWorkspace(t *testing.T) {
tool, _ := NewExecTool(workspace, true) tool, _ := NewExecTool(workspace, true)
innerDir := filepath.Join(workspace, "projects", "myapp") innerDir := filepath.Join(workspace, "projects", "myapp")
os.MkdirAll(innerDir, 0755) os.MkdirAll(innerDir, 0o755)
cmd := "ls " + innerDir cmd := "ls " + innerDir
result := tool.guardCommand(cmd, workspace) result := tool.guardCommand(cmd, workspace)
@ -514,7 +514,7 @@ func TestGuardCommand_PathTraversal(t *testing.T) {
func TestGuardCommand_CdWithAbsoluteWorkspacePath(t *testing.T) { func TestGuardCommand_CdWithAbsoluteWorkspacePath(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
innerDir := filepath.Join(workspace, "projects", "foo") innerDir := filepath.Join(workspace, "projects", "foo")
os.MkdirAll(innerDir, 0755) os.MkdirAll(innerDir, 0o755)
tool, _ := NewExecTool(workspace, true) tool, _ := NewExecTool(workspace, true)

View file

@ -52,8 +52,8 @@ func (t *SpawnTool) Parameters() map[string]any {
"description": "Optional target agent ID to delegate the task to", "description": "Optional target agent ID to delegate the task to",
}, },
"preset": map[string]any{ "preset": map[string]any{
"type": "string", "type": "string",
"enum": []string{"scout", "analyst", "coder", "worker", "coordinator"}, "enum": []string{"scout", "analyst", "coder", "worker", "coordinator"},
"description": "Optional capability tier: scout (explore), analyst (analyze), coder (code), worker (build), coordinator (orchestrate)", "description": "Optional capability tier: scout (explore), analyst (analyze), coder (code), worker (build), coordinator (orchestrate)",
}, },
}, },

View file

@ -29,6 +29,7 @@ func (r *reporterSpy) ReportStateChange(id, state, tool string) {
r.calls = append(r.calls, spyCall{state, tool}) r.calls = append(r.calls, spyCall{state, tool})
r.mu.Unlock() r.mu.Unlock()
} }
func (r *reporterSpy) snapshot() []spyCall { func (r *reporterSpy) snapshot() []spyCall {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -77,6 +78,7 @@ func (t *echoTool) Description() string { return "echo" }
func (t *echoTool) Parameters() map[string]any { func (t *echoTool) Parameters() map[string]any {
return map[string]any{"type": "object", "properties": map[string]any{}} return map[string]any{"type": "object", "properties": map[string]any{}}
} }
func (t *echoTool) Execute(_ context.Context, _ map[string]any) *ToolResult { func (t *echoTool) Execute(_ context.Context, _ map[string]any) *ToolResult {
return &ToolResult{ForLLM: "echoed"} return &ToolResult{ForLLM: "echoed"}
} }

View file

@ -6,8 +6,10 @@ import (
"strings" "strings"
) )
type workspaceOverrideKey struct{} type (
type overrideFsKey struct{} workspaceOverrideKey struct{}
overrideFsKey struct{}
)
// WithWorkspaceOverride returns a context carrying a workspace override path // WithWorkspaceOverride returns a context carrying a workspace override path
// and a pre-built sandboxFs for that workspace. Tools will resolve file // and a pre-built sandboxFs for that workspace. Tools will resolve file

View file

@ -20,7 +20,6 @@ var (
// IsAudioFile checks if a file is an audio file based on its filename extension and content type. // IsAudioFile checks if a file is an audio file based on its filename extension and content type.
func IsAudioFile(filename, contentType string) bool { func IsAudioFile(filename, contentType string) bool {
for _, ext := range audioExtensions { for _, ext := range audioExtensions {
if strings.HasSuffix(strings.ToLower(filename), ext) { if strings.HasSuffix(strings.ToLower(filename), ext) {
return true return true