diff --git a/.golangci.yml b/.golangci.yml index 88c6bb78..fe5fd96a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -92,24 +92,6 @@ linters: - path: cmd/ linters: - noctx - - path: internal/intelligence/memory/ - linters: - - errcheck - - path: internal/intelligence/repomap/ - linters: - - errcheck - - path: internal/codegraph/ - linters: - - errcheck - - path: internal/tool/codegraph.go - linters: - - errcheck - - path: internal/tool/lsp.go - linters: - - errcheck - - path: internal/magicdocs/ - linters: - - errcheck issues: max-issues-per-linter: 0 diff --git a/internal/auth/auth.go b/internal/auth/auth.go index e18ea1c1..65978dfd 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -26,14 +26,21 @@ func NewTokenStore() *TokenStore { } // Load loads tokens from secure storage. +// Deprecated: Use SecureStorage directly to load tokens. This stub always +// returns an empty token map. Migrate callers to SecureStorage.Get/Set. func (t *TokenStore) Load() error { - // Try to load from keychain/keyring + // Stub: no-op. Existing callers that relied on this get an empty token + // map. New code should use SecureStorage directly. t.tokens = make(map[string]string) return nil } // Save saves tokens to secure storage. +// Deprecated: Use SecureStorage directly to persist tokens. This stub is a +// no-op. Migrate callers to SecureStorage.Set. func (t *TokenStore) Save() error { + // Stub: no-op. Tokens held in-memory only; they are lost on process exit. + // Use SecureStorage for persistent, OS-keychain-backed storage. return nil } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index f21d1743..f8514ff1 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -172,11 +172,12 @@ func New(cfg Config, factory SessionFactory) *Server { // for poll-based gateways at Start time. s.gateways = newGatewayManager(cfg.Gateways, "http://"+s.addr, cfg.APIKey, s) s.server = &http.Server{ - Addr: s.addr, - Handler: s.mux, - ReadTimeout: 30 * time.Second, - WriteTimeout: 300 * time.Second, - IdleTimeout: 60 * time.Second, + Addr: s.addr, + Handler: s.mux, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 300 * time.Second, + IdleTimeout: 60 * time.Second, } return s } @@ -196,6 +197,11 @@ func (s *Server) Start() (string, error) { s.addr = actualAddr go func() { + defer func() { + if r := recover(); r != nil { + slog.Error("daemon server goroutine panicked", "recover", r) + } + }() if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed { slog.Error("daemon server error", "error", err) } diff --git a/internal/daemon/discord.go b/internal/daemon/discord.go index a0e59b0a..18e67980 100644 --- a/internal/daemon/discord.go +++ b/internal/daemon/discord.go @@ -155,8 +155,8 @@ func (g *DiscordGateway) handleMessage(ctx context.Context, senderID, channelID, if err != nil { resp = fmt.Sprintf("Error: %v", err) } - if len(resp) > 1900 { // Discord 2000-char limit, leave headroom - resp = resp[:1900] + "\n\n... (truncated)" + if len([]rune(resp)) > 1900 { // Discord 2000-char limit, leave headroom + resp = string([]rune(resp)[:1900]) + "\n\n... (truncated)" } reply(resp) } diff --git a/internal/daemon/telegram.go b/internal/daemon/telegram.go index c69480b4..2c0a2c7d 100644 --- a/internal/daemon/telegram.go +++ b/internal/daemon/telegram.go @@ -246,7 +246,7 @@ func (tg *TelegramGateway) sendMessage(ctx context.Context, chatID int64, text s data := url.Values{ "chat_id": {fmt.Sprintf("%d", chatID)}, "text": {text}, - "parse_mode": {"Markdown"}, + "parse_mode": {""}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(data.Encode())) if err != nil { diff --git a/internal/permissions/egress.go b/internal/permissions/egress.go index c5833a1b..5da59d31 100644 --- a/internal/permissions/egress.go +++ b/internal/permissions/egress.go @@ -13,14 +13,17 @@ import ( // Pre-compiled patterns for exfiltration detection. var ( - curlPostWithData = regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`) - curlDataWithPost = regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`) - curlDataBinary = regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`) - pipeToNetwork = regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`) - base64PipeNetwork = regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`) - base64Network = regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`) - envVarInURL = regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`) - curlFileUpload = regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`) + cachedRegex = &sync.Map{} // string(pattern) -> *regexp.Regexp + + // Pre-compiled patterns for exfiltration detection. + curlPostWithData = regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`) + curlDataWithPost = regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`) + curlDataBinary = regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`) + pipeToNetwork = regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`) + base64PipeNet = regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`) + base64Net = regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`) + envVarInURL = regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`) + curlFileUpload = regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`) ) // EgressInspector detects and blocks data exfiltration attempts in shell commands @@ -30,6 +33,7 @@ type EgressInspector struct { BlockedDomains []string AllowedProtocols []string mu sync.RWMutex + compiledPatterns sync.Map // string pattern -> *regexp.Regexp } // EgressAttempt represents the result of inspecting a command for egress activity. @@ -75,6 +79,26 @@ func NewEgressInspector() *EgressInspector { } } +// compilePattern pre-compiles a wildcard glob pattern to a compiled regexp and +// caches it in the inspector's compiledPatterns map for reuse. +func (e *EgressInspector) compilePattern(pattern string) *regexp.Regexp { + if cached, ok := e.compiledPatterns.Load(pattern); ok { + return cached.(*regexp.Regexp) + } + + regexStr := "^" + regexp.QuoteMeta(pattern) + "$" + regexStr = strings.ReplaceAll(regexStr, `\*`, `[A-Za-z0-9._-]*`) + re, err := regexp.Compile(regexStr) + if err != nil { + return nil + } + + if val, loaded := e.compiledPatterns.LoadOrStore(pattern, re); loaded { + return val.(*regexp.Regexp) + } + return re +} + // Inspect analyzes a command for network egress destinations and returns // an EgressAttempt indicating whether the command is allowed. func (e *EgressInspector) Inspect(command string) *EgressAttempt { @@ -236,8 +260,8 @@ func (e *EgressInspector) IsSuspicious(command string) bool { } // Base64 encoding combined with network send - if base64PipeNetwork.MatchString(command) || - base64Network.MatchString(command) { + if base64PipeNet.MatchString(command) || + base64Net.MatchString(command) { return true } @@ -381,16 +405,16 @@ func matchDomain(pattern, host string) bool { return true } - // Wildcard matching - if strings.Contains(pattern, "*") { - // Convert glob pattern to regex +// Wildcard matching +if strings.Contains(pattern, "*") { + // Convert glob pattern to regex with caching regexStr := "^" + regexp.QuoteMeta(pattern) + "$" regexStr = strings.ReplaceAll(regexStr, `\*`, `[A-Za-z0-9._-]*`) - re, err := regexp.Compile(regexStr) - if err != nil { - return false + re, loaded := cachedRegex.LoadOrStore(regexStr, regexp.MustCompile(regexStr)) + if loaded { + return re.(*regexp.Regexp).MatchString(host) } - return re.MatchString(host) + return re.(*regexp.Regexp).MatchString(host) } // Subdomain match: pattern "example.com" matches "sub.example.com" @@ -405,8 +429,8 @@ func matchDomain(pattern, host string) bool { func (e *EgressInspector) describeSuspicious(command string) []string { var patterns []string - if regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`).MatchString(command) || - regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`).MatchString(command) { + if curlPostWithData.MatchString(command) || + curlDataWithPost.MatchString(command) { // Try to extract the file name fileRe := regexp.MustCompile(`-d\s+@([^\s"']+)`) if m := fileRe.FindStringSubmatch(command); len(m) >= 2 { @@ -416,24 +440,24 @@ func (e *EgressInspector) describeSuspicious(command string) []string { } } - if regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`).MatchString(command) { + if curlDataBinary.MatchString(command) { patterns = append(patterns, "Binary file upload") } - if regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`).MatchString(command) { + if pipeToNetwork.MatchString(command) { patterns = append(patterns, "Pipe to network command") } - if regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`).MatchString(command) || - regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`).MatchString(command) { + if base64PipeNet.MatchString(command) || + base64Net.MatchString(command) { patterns = append(patterns, "Base64 encoding with network send") } - if regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`).MatchString(command) { + if envVarInURL.MatchString(command) { patterns = append(patterns, "Environment variable in URL") } - if regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`).MatchString(command) { + if curlFileUpload.MatchString(command) { patterns = append(patterns, "File upload via form data") }