From c58c4cf11aea37e086015356350843e86ded4794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E7=94=9F=E6=B4=BECoder=EF=BD=9E?= Date: Sat, 14 Feb 2026 01:02:41 +0800 Subject: [PATCH 1/6] fix shell risk gate dry-run flow for destructive git clean --- pkg/tools/risk.go | 2 +- pkg/tools/shell.go | 2 +- pkg/tools/shell_test.go | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 pkg/tools/shell_test.go diff --git a/pkg/tools/risk.go b/pkg/tools/risk.go index 3f86c67..4ceee87 100644 --- a/pkg/tools/risk.go +++ b/pkg/tools/risk.go @@ -28,11 +28,11 @@ var destructivePatterns = []*regexp.Regexp{ regexp.MustCompile(`\bchown\b.+\s+/`), regexp.MustCompile(`\bclawgo\s+uninstall\b`), regexp.MustCompile(`\bdbt\s+drop\b`), + regexp.MustCompile(`\bgit\s+clean\b`), } var moderatePatterns = []*regexp.Regexp{ regexp.MustCompile(`\bgit\s+reset\s+--hard\b`), - regexp.MustCompile(`\bgit\s+clean\b`), regexp.MustCompile(`\bdocker\s+system\s+prune\b`), regexp.MustCompile(`\bapt(-get)?\s+install\b`), regexp.MustCompile(`\byum\s+install\b`), diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 3e8ebdb..25ff5ee 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -254,7 +254,7 @@ func (t *ExecTool) applyRiskGate(command string, force bool) (string, string) { return "Error: destructive command is disabled by policy (tools.shell.risk.allow_destructive=false).", "" } - if t.riskCfg.RequireDryRun { + if t.riskCfg.RequireDryRun && !force { if dryRunCmd, ok := buildDryRunCommand(command); ok { return "Risk gate: dry-run required first. Review output, then execute intentionally with force=true.", dryRunCmd } diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go new file mode 100644 index 0000000..2ececbb --- /dev/null +++ b/pkg/tools/shell_test.go @@ -0,0 +1,45 @@ +package tools + +import ( + "testing" + + "clawgo/pkg/config" +) + +func TestApplyRiskGate_DryRunCanBeBypassedWithForce(t *testing.T) { + tool := &ExecTool{riskCfg: config.RiskConfig{ + Enabled: true, + AllowDestructive: true, + RequireDryRun: true, + RequireForceFlag: false, + }} + + msg, dryRun := tool.applyRiskGate("git clean -fd", true) + if msg != "" || dryRun != "" { + t.Fatalf("expected force=true to allow execution after dry-run step, got msg=%q dryRun=%q", msg, dryRun) + } +} + +func TestApplyRiskGate_RequiresDryRunWithoutForce(t *testing.T) { + tool := &ExecTool{riskCfg: config.RiskConfig{ + Enabled: true, + AllowDestructive: true, + RequireDryRun: true, + RequireForceFlag: false, + }} + + msg, dryRun := tool.applyRiskGate("git clean -fd", false) + if msg == "" { + t.Fatal("expected dry-run block message") + } + if dryRun == "" { + t.Fatal("expected dry-run command") + } +} + +func TestAssessCommandRisk_GitCleanIsDestructive(t *testing.T) { + assessment := assessCommandRisk("git clean -fd") + if assessment.Level != RiskDestructive { + t.Fatalf("expected git clean to be destructive, got %s", assessment.Level) + } +} From 4bdc25c1272d7a7e53ee6fab1350b6a89d4a77a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E7=94=9F=E6=B4=BECoder=EF=BD=9E?= Date: Sat, 14 Feb 2026 09:49:48 +0800 Subject: [PATCH 2/6] fix shell allowlist initialization and add guard tests --- pkg/tools/shell.go | 8 +++++++- pkg/tools/shell_test.go | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 25ff5ee..26e735c 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -29,15 +29,21 @@ type ExecTool struct { } func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool { - denyPatterns := make([]*regexp.Regexp, 0) + denyPatterns := make([]*regexp.Regexp, 0, len(cfg.DeniedCmds)) for _, p := range cfg.DeniedCmds { denyPatterns = append(denyPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`)) } + allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds)) + for _, p := range cfg.AllowedCmds { + allowPatterns = append(allowPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`)) + } + return &ExecTool{ workingDir: workspace, timeout: cfg.Timeout, denyPatterns: denyPatterns, + allowPatterns: allowPatterns, restrictToWorkspace: cfg.RestrictPath, sandboxEnabled: cfg.Sandbox.Enabled, sandboxImage: cfg.Sandbox.Image, diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go index 2ececbb..2d7fef2 100644 --- a/pkg/tools/shell_test.go +++ b/pkg/tools/shell_test.go @@ -43,3 +43,21 @@ func TestAssessCommandRisk_GitCleanIsDestructive(t *testing.T) { t.Fatalf("expected git clean to be destructive, got %s", assessment.Level) } } + +func TestNewExecTool_LoadsAllowedCmdsIntoAllowPatterns(t *testing.T) { + tool := NewExecTool(config.ShellConfig{AllowedCmds: []string{"echo"}}, ".") + if len(tool.allowPatterns) != 1 { + t.Fatalf("expected one allow pattern, got %d", len(tool.allowPatterns)) + } +} + +func TestGuardCommand_BlocksCommandNotInAllowlist(t *testing.T) { + tool := NewExecTool(config.ShellConfig{AllowedCmds: []string{"echo"}}, ".") + if msg := tool.guardCommand("ls -la", "."); msg == "" { + t.Fatal("expected allowlist to block command not in allowed_cmds") + } + + if msg := tool.guardCommand("echo hi", "."); msg != "" { + t.Fatalf("expected allowed command to pass guard, got %q", msg) + } +} From 64f84678cb5cf9cd299508bc6a3524488503dd45 Mon Sep 17 00:00:00 2001 From: lpf Date: Sat, 14 Feb 2026 11:33:06 +0800 Subject: [PATCH 3/6] fix bug --- cmd/clawgo/main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index ead58f2..fe1c104 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -115,7 +115,9 @@ func main() { case "agent": agentCmd() case "gateway": - maybePromptAndEscalateRoot("gateway") + if shouldPromptGatewayRoot(os.Args) { + maybePromptAndEscalateRoot("gateway") + } gatewayCmd() case "status": statusCmd() @@ -906,6 +908,11 @@ func maybePromptAndEscalateRoot(command string) { os.Exit(0) } +func shouldPromptGatewayRoot(args []string) bool { + // Only prompt on plain `clawgo gateway` registration flow. + return len(args) == 2 && args[1] == "gateway" +} + func isInteractiveStdin() bool { info, err := os.Stdin.Stat() if err != nil { From af6da309ecfcd6aff36e32f00b889e40cc875114 Mon Sep 17 00:00:00 2001 From: lpf Date: Sat, 14 Feb 2026 11:50:21 +0800 Subject: [PATCH 4/6] fix timeout --- config.example.json | 3 ++- pkg/agent/loop.go | 7 ++++--- pkg/config/config.go | 10 ++++++---- pkg/config/validate.go | 3 +++ pkg/providers/http_provider.go | 15 +++++++++------ 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/config.example.json b/config.example.json index 72e86db..14f5279 100644 --- a/config.example.json +++ b/config.example.json @@ -50,7 +50,8 @@ "proxy": { "api_key": "YOUR_CLIPROXYAPI_KEY", "api_base": "http://localhost:8080/v1", - "auth": "bearer" + "auth": "bearer", + "timeout_sec": 90 } }, "tools": { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0ef5888..58f1363 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -31,7 +31,6 @@ import ( var errGatewayNotRunningSlash = errors.New("gateway not running") -const llmCallTimeout = 90 * time.Second const perSessionQueueSize = 64 type sessionWorker struct { @@ -53,6 +52,7 @@ type AgentLoop struct { orchestrator *tools.Orchestrator running atomic.Bool compactionCfg config.ContextCompactionConfig + llmCallTimeout time.Duration workersMu sync.Mutex workers map[string]*sessionWorker } @@ -137,6 +137,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers tools: toolsRegistry, orchestrator: orchestrator, compactionCfg: cfg.Agents.Defaults.ContextCompaction, + llmCallTimeout: time.Duration(cfg.Providers.Proxy.TimeoutSec) * time.Second, workers: make(map[string]*sessionWorker), } @@ -561,7 +562,7 @@ func (al *AgentLoop) runLLMToolLoop( }) llmStart := time.Now() - llmCtx, cancelLLM := context.WithTimeout(ctx, llmCallTimeout) + llmCtx, cancelLLM := context.WithTimeout(ctx, al.llmCallTimeout) response, err := al.callLLMWithModelFallback(llmCtx, messages, providerToolDefs, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, @@ -680,7 +681,7 @@ func (al *AgentLoop) runLLMToolLoop( }) finalizeMessages = sanitizeMessagesForToolCalling(finalizeMessages) - llmCtx, cancelLLM := context.WithTimeout(ctx, llmCallTimeout) + llmCtx, cancelLLM := context.WithTimeout(ctx, al.llmCallTimeout) finalResp, err := al.callLLMWithModelFallback(llmCtx, finalizeMessages, nil, map[string]interface{}{ "max_tokens": 1024, "temperature": 0.3, diff --git a/pkg/config/config.go b/pkg/config/config.go index eb7f299..07eed03 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -107,9 +107,10 @@ type ProvidersConfig struct { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"` - APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"` - Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"` + APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"` + APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"` + Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"` + TimeoutSec int `json:"timeout_sec" env:"CLAWGO_PROVIDERS_PROXY_TIMEOUT_SEC"` } type GatewayConfig struct { @@ -276,7 +277,8 @@ func DefaultConfig() *Config { }, Providers: ProvidersConfig{ Proxy: ProviderConfig{ - APIBase: "http://localhost:8080/v1", + APIBase: "http://localhost:8080/v1", + TimeoutSec: 90, }, }, Gateway: GatewayConfig{ diff --git a/pkg/config/validate.go b/pkg/config/validate.go index ee7acb6..bef09d1 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -39,6 +39,9 @@ func Validate(cfg *Config) []error { if cfg.Providers.Proxy.APIBase == "" { errs = append(errs, fmt.Errorf("providers.proxy.api_base is required")) } + if cfg.Providers.Proxy.TimeoutSec <= 0 { + errs = append(errs, fmt.Errorf("providers.proxy.timeout_sec must be > 0")) + } if cfg.Gateway.Port <= 0 || cfg.Gateway.Port > 65535 { errs = append(errs, fmt.Errorf("gateway.port must be in 1..65535")) diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index a09d4c1..45b9308 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -24,18 +24,18 @@ type HTTPProvider struct { apiKey string apiBase string authMode string + timeout time.Duration httpClient *http.Client } -const defaultChatTimeout = 90 * time.Second - -func NewHTTPProvider(apiKey, apiBase, authMode string) *HTTPProvider { +func NewHTTPProvider(apiKey, apiBase, authMode string, timeout time.Duration) *HTTPProvider { return &HTTPProvider{ apiKey: apiKey, apiBase: apiBase, authMode: authMode, + timeout: timeout, httpClient: &http.Client{ - Timeout: defaultChatTimeout, + Timeout: timeout, }, } } @@ -50,7 +50,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too "model": model, "messages_count": len(messages), "tools_count": len(tools), - "timeout": defaultChatTimeout.String(), + "timeout": p.timeout.String(), }) requestBody := map[string]interface{}{ @@ -208,6 +208,9 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) { if apiBase == "" { return nil, fmt.Errorf("no API base (CLIProxyAPI) configured") } + if cfg.Providers.Proxy.TimeoutSec <= 0 { + return nil, fmt.Errorf("invalid providers.proxy.timeout_sec: %d", cfg.Providers.Proxy.TimeoutSec) + } - return NewHTTPProvider(apiKey, apiBase, authMode), nil + return NewHTTPProvider(apiKey, apiBase, authMode, time.Duration(cfg.Providers.Proxy.TimeoutSec)*time.Second), nil } From de2c9150f1776e117b7b0996b1f427775336b484 Mon Sep 17 00:00:00 2001 From: lpf Date: Sat, 14 Feb 2026 11:53:54 +0800 Subject: [PATCH 5/6] fix telegram anim --- pkg/channels/telegram.go | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 5560769..4844b50 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -63,6 +63,13 @@ func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*Telegr }, nil } +func withTelegramAPITimeout(ctx context.Context) (context.Context, context.CancelFunc) { + if ctx == nil { + ctx = context.Background() + } + return context.WithTimeout(ctx, telegramAPICallTimeout) +} + func (c *TelegramChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { c.transcriber = transcriber } @@ -84,7 +91,9 @@ func (c *TelegramChannel) Start(ctx context.Context) error { c.setRunning(true) - botInfo, err := c.bot.GetMe(context.Background()) + getMeCtx, cancelGetMe := withTelegramAPITimeout(ctx) + botInfo, err := c.bot.GetMe(getMeCtx) + cancelGetMe() if err != nil { return fmt.Errorf("failed to get bot info: %w", err) } @@ -205,13 +214,12 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err } chatID := telegoutil.ID(chatIDInt) - // Stop thinking animation - if stop, ok := c.stopThinking.Load(msg.ChatID); ok { + // Stop thinking animation first to avoid animation/update races. + if stop, ok := c.stopThinking.LoadAndDelete(msg.ChatID); ok { logger.DebugCF("telegram", "Telegram thinking stop signal", map[string]interface{}{ logger.FieldChatID: msg.ChatID, }) safeCloseSignal(stop) - c.stopThinking.Delete(msg.ChatID) } else { logger.DebugCF("telegram", "Telegram thinking stop skipped (not found)", map[string]interface{}{ logger.FieldChatID: msg.ChatID, @@ -222,18 +230,21 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // Try to edit placeholder if pID, ok := c.placeholders.Load(msg.ChatID); ok { - c.placeholders.Delete(msg.ChatID) + // Always reset placeholder state even when edit/send fails. + defer c.placeholders.Delete(msg.ChatID) logger.DebugCF("telegram", "Telegram editing thinking placeholder", map[string]interface{}{ logger.FieldChatID: msg.ChatID, "message_id": pID.(int), }) - _, err := c.bot.EditMessageText(ctx, &telego.EditMessageTextParams{ + editCtx, cancelEdit := withTelegramAPITimeout(ctx) + _, err := c.bot.EditMessageText(editCtx, &telego.EditMessageTextParams{ ChatID: chatID, MessageID: pID.(int), Text: htmlContent, ParseMode: telego.ModeHTML, }) + cancelEdit() if err == nil { logger.DebugCF("telegram", "Telegram placeholder updated", map[string]interface{}{ @@ -252,14 +263,18 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err }) } - _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, htmlContent).WithParseMode(telego.ModeHTML)) + sendCtx, cancelSend := withTelegramAPITimeout(ctx) + _, err = c.bot.SendMessage(sendCtx, telegoutil.Message(chatID, htmlContent).WithParseMode(telego.ModeHTML)) + cancelSend() if err != nil { logger.WarnCF("telegram", "HTML parse failed, fallback to plain text", map[string]interface{}{ logger.FieldError: err.Error(), }) plain := plainTextFromTelegramHTML(htmlContent) - _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, plain)) + sendPlainCtx, cancelSendPlain := withTelegramAPITimeout(ctx) + _, err = c.bot.SendMessage(sendPlainCtx, telegoutil.Message(chatID, plain)) + cancelSendPlain() if err != nil { logger.ErrorCF("telegram", "Telegram plain-text fallback send failed", map[string]interface{}{ logger.FieldChatID: msg.ChatID, From e201bab0643c804fa541aaf855da331621604115 Mon Sep 17 00:00:00 2001 From: lpf Date: Sat, 14 Feb 2026 12:01:25 +0800 Subject: [PATCH 6/6] fix path --- cmd/clawgo/main.go | 10 +++++----- pkg/skills/installer.go | 2 +- skills/context7/SKILL.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index fe1c104..5026c0d 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -428,11 +428,11 @@ Ultra-lightweight personal AI assistant written in Go, inspired by nanobot. MIT License - Free and open source ## Repository -https://github.com/sipeed/clawgo +https://github.com/YspCoder/clawgo ## Contact -Issues: https://github.com/sipeed/clawgo/issues -Discussions: https://github.com/sipeed/clawgo/discussions +Issues: https://github.com/YspCoder/clawgo/issues +Discussions: https://github.com/YspCoder/clawgo/discussions --- @@ -1658,7 +1658,7 @@ func skillsHelp() { fmt.Println() fmt.Println("Examples:") fmt.Println(" clawgo skills list") - fmt.Println(" clawgo skills install sipeed/clawgo-skills/weather") + fmt.Println(" clawgo skills install YspCoder/clawgo-skills/weather") fmt.Println(" clawgo skills install-builtin") fmt.Println(" clawgo skills list-builtin") fmt.Println(" clawgo skills remove weather") @@ -1685,7 +1685,7 @@ func skillsListCmd(loader *skills.SkillsLoader) { func skillsInstallCmd(installer *skills.SkillInstaller) { if len(os.Args) < 4 { fmt.Println("Usage: clawgo skills install ") - fmt.Println("Example: clawgo skills install sipeed/clawgo-skills/weather") + fmt.Println("Example: clawgo skills install YspCoder/clawgo-skills/weather") return } diff --git a/pkg/skills/installer.go b/pkg/skills/installer.go index f616cd6..f644c54 100644 --- a/pkg/skills/installer.go +++ b/pkg/skills/installer.go @@ -93,7 +93,7 @@ func (si *SkillInstaller) Uninstall(skillName string) error { } func (si *SkillInstaller) ListAvailableSkills(ctx context.Context) ([]AvailableSkill, error) { - url := "https://raw.githubusercontent.com/sipeed/clawgo-skills/main/skills.json" + url := "https://raw.githubusercontent.com/YspCoder/clawgo-skills/main/skills.json" client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", url, nil) diff --git a/skills/context7/SKILL.md b/skills/context7/SKILL.md index e9a283e..8ce322e 100644 --- a/skills/context7/SKILL.md +++ b/skills/context7/SKILL.md @@ -15,5 +15,5 @@ npx tsx /root/.clawgo/skills/context7/query.ts context Example: ```bash -npx tsx /root/.clawgo/skills/context7/query.ts context sipeed/clawgo "How does the skill system work?" +npx tsx /root/.clawgo/skills/context7/query.ts context YspCoder/clawgo "How does the skill system work?" ```