Skip to content

Clean up classes #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Clean up classes #15

wants to merge 12 commits into from

Conversation

xyOz-dev
Copy link
Owner

@xyOz-dev xyOz-dev commented Aug 23, 2025

Summary by CodeRabbit

  • New Features
    • Configurable sub‑agent options: model, temperature, max tokens, top‑p, tool usage, and optional system prompt override.
    • Exposed multi‑agent status, tasks, reviews, and results for richer status displays and integrations.
  • Improvements
    • More consistent initial Git commit behavior across platforms.
    • More consistent markdown rendering via shared formatted-text handling.
  • Refactor
    • Centralized shared data models for agents, tools, configuration, and UI to improve consistency.

- Move `SubAgentContext`, `ReviewerContext`, `ReviewDecision`, `AgentTask`, `AgentTaskResult`, `AgentStatusInfo` to `Agents/MultiAgent/Objects`
- Move `AgentStatus`, `TaskStatus`, `ReviewStatus` to `Agents/MultiAgent`
- This improves project structure and organization.

Commit generated by The Saturn Pipeline
- Move `SubAgentConfiguration` to `Config/Objects`
- This improves project structure and organization.

Commit generated by The Saturn Pipeline
- Move `PersistedAgentConfiguration` to `Configuration/Objects`
- This improves project structure and organization.

Commit generated by The Saturn Pipeline
- Remove windows specific git add command
- The previous implementation caused errors.

Commit generated by The Saturn Pipeline
- Remove unused comment in CreateAgentTool.cs

Commit generated by The Saturn Pipeline
- Introduce new objects for tool results
- These objects will be used to standardize tool outputs

Commit generated by The Saturn Pipeline
- Remove internal classes from tools
- Use the new objects instead

Commit generated by The Saturn Pipeline
- Add StreamingToolCall object
- This object will be used to store streaming tool calls

Commit generated by The Saturn Pipeline
- Remove StreamingToolCall class from AgentBase
- Use the new StreamingToolCall object instead

Commit generated by The Saturn Pipeline
- Add FormattedText object
- This object will be used to store formatted text

Commit generated by The Saturn Pipeline
- Remove FormattedText class from MarkdownRenderer
- Use the new FormattedText object instead

Commit generated by The Saturn Pipeline
Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

Relocates many DTOs, enums, and helper classes into dedicated Objects namespaces across Agents, Tools, Config, Configuration, and UI. Files add corresponding using directives and remove in-file type definitions. GitManager replaces platform-specific add with a single git add .. No control-flow or behavior changes.

Changes

Cohort / File(s) Summary
Agents Core streaming helper
Agents/Core/AgentBase.cs, Agents/Core/Objects/StreamingToolCall.cs
Removes private nested StreamingToolCall from AgentBase.cs; adds using Saturn.Agents.Core.Objects; and introduces internal class StreamingToolCall in Agents/Core/Objects/StreamingToolCall.cs. No logic changes.
MultiAgent objects extraction
Agents/MultiAgent/AgentManager.cs, Agents/MultiAgent/Objects/*
Removes several public classes/enums from AgentManager.cs and adds them under Saturn.Agents.MultiAgent.Objects (SubAgentContext, ReviewerContext, ReviewDecision, AgentTask, AgentTaskResult, AgentStatusInfo, and enums). Signatures preserved; AgentManager now imports the objects namespace.
Config: SubAgentConfiguration relocation
Config/SubAgentPreferences.cs, Config/Objects/SubAgentConfiguration.cs
Moves SubAgentConfiguration into Saturn.Config.Objects; SubAgentPreferences imports it via using. Behavior unchanged.
Configuration: PersistedAgentConfiguration relocation
Configuration/ConfigurationManager.cs, Configuration/Objects/PersistedAgentConfiguration.cs
Moves PersistedAgentConfiguration into Saturn.Configuration.Objects; adds using; usage unchanged.
Git init behavior tweak
Core/GitManager.cs
Replaces OS-conditional git-add logic with a single unconditional git add . (previous Windows-specific branch removed/commented). Commit flow otherwise unchanged.
Tools: switch to shared models (callers)
Tools/ApplyDiffTool.cs, Tools/DeleteFileTool.cs, Tools/ExecuteCommandTool.cs, Tools/GlobTool.cs, Tools/GrepTool.cs, Tools/ListFilesTool.cs, Tools/ReadFileTool.cs, Tools/SearchAndReplaceTool.cs, Tools/WebFetchTool.cs, Tools/MultiAgent/CreateAgentTool.cs
Adds using Saturn.Tools.Objects; and removes in-file type declarations; code now references shared types from Saturn.Tools.Objects. No control-flow changes; one comment removed in CreateAgentTool.
Tools: shared object model additions
Tools/Objects/*
Adds many DTOs/enums under Saturn.Tools.Objects (e.g., CommandExecutorConfig, SecurityMode, CommandResult, CommandHistory, GlobMatch, GrepResult, patch/commit models, file-system/tree models, caching types, search/replace results, etc.). These are plain data holders; no logic.
UI: formatted text extraction
UI/MarkdownRenderer.cs, UI/Objects/FormattedText.cs
Removes local FormattedText from renderer; adds Saturn.UI.Objects.FormattedText and updates renderer to use the external type. API preserved; rendering unchanged.

Sequence Diagram(s)

(omitted — changes are structural/type relocations without new control-flow interactions)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hopped through files with tidy paws,
Moving nouns to Object drawers;
Types found new homes, neat and small,
No logic bent, no runtime fall.
One git add . — a single call 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db0d0de and 3e53486.

📒 Files selected for processing (1)
  • Tools/SearchAndReplaceTool.cs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tools/SearchAndReplaceTool.cs
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Clean-up-classes

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (19)
Tools/WebFetchTool.cs (1)

161-169: Harden cache key and avoid serving header-authenticated content across calls

The cache key omits includeMetadata and maxLength, so callers can receive mismatched content (metadata present/absent, different truncation). Also, caching responses when custom headers are supplied (e.g., Authorization, Cookie) risks leaking header-scoped content across unrelated calls. Lastly, returning the same ToolResult instance invites cross-call mutation.

Apply:

-                var cacheKey = $"{url}|{extractionMode}|{selector}";
-                if (useCache && _cache.TryGetValue(cacheKey, out var cached))
+                var hasCustomHeaders = headers != null && headers.Count > 0;
+                var canCache = useCache && !hasCustomHeaders;
+                var cacheKey = $"{url}|{extractionMode}|{selector}|meta={includeMetadata}|max={maxLength}";
+                if (canCache && _cache.TryGetValue(cacheKey, out var cached))
                 {
-                    if (DateTime.UtcNow - cached.CachedAt < CacheDuration)
+                    if (DateTime.UtcNow - cached.CachedAt < CacheDuration)
                     {
-                        return cached.Result;
+                        var r = cached.Result; // defensive copy to avoid shared mutable state
+                        return new ToolResult
+                        {
+                            Success = r.Success,
+                            FormattedOutput = r.FormattedOutput,
+                            RawData = r.RawData,
+                            Error = r.Error
+                        };
                     }
-                    _cache.TryRemove(cacheKey, out _);
+                    _cache.TryRemove(cacheKey, out _);
                 }

And when storing:

-                if (useCache)
+                if (canCache)
                 {
                     _cache[cacheKey] = new CachedContent
                     {
                         Result = result,
                         CachedAt = DateTime.UtcNow
                     };
                 }
Tools/GlobTool.cs (2)

37-39: Pattern parsing contradicts examples; comma-splitting breaks {json,xml,config} and yields invalid includes

The example shows brace expansion: **/*.{json,xml,config}, but the implementation naïvely splits the entire pattern string on commas, producing invalid fragments like **/*.{json. Microsoft’s globbing does not support brace expansion; multiple patterns should be passed separately (e.g., with ;). This likely confuses users and produces wrong results.

Update the parsing to split only on ; and fix the example:

-            var patterns = pattern.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
-                                 .Select(p => p.Trim())
-                                 .Where(p => !string.IsNullOrEmpty(p));
+            var patterns = pattern.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+                                  .Select(p => p.Trim())
+                                  .Where(p => !string.IsNullOrEmpty(p));
-// - Find config files: pattern='**/*.{json,xml,config}'
+// - Find config files: pattern='**/*.json;**/*.xml;**/*.config'

Optionally, expand the parameter help text to explicitly instruct semicolon-separated multiple patterns.

I can update parameter docs/examples across the repo for consistency if you want.

Also applies to: 165-168


347-361: Harden path containment check to avoid prefix-bypass (StartsWith is unsafe for paths)

StartsWith(currentDirectory, OrdinalIgnoreCase) allows escapes like C:\work2 when currentDirectory is C:\work. Use Path.GetRelativePath and validate it doesn’t escape upward or cross volumes.

Apply:

-            var fullPath = Path.GetFullPath(path);
-            var currentDirectory = Path.GetFullPath(Directory.GetCurrentDirectory());
-            
-            if (!fullPath.StartsWith(currentDirectory, StringComparison.OrdinalIgnoreCase))
-            {
-                throw new SecurityException($"Access denied: Path '{path}' is outside the working directory.");
-            }
+            var fullPath = Path.GetFullPath(path);
+            var currentDirectory = Path.GetFullPath(Directory.GetCurrentDirectory());
+            var rel = Path.GetRelativePath(currentDirectory, fullPath);
+            // Outside if it climbs up (..), or if RelativePath returns rooted (different drive/volume)
+            if (Path.IsPathRooted(rel) || rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal))
+            {
+                throw new SecurityException($"Access denied: Path '{path}' is outside the working directory.");
+            }
Tools/DeleteFileTool.cs (2)

31-34: Advertised safety invariant isn’t enforced: allowlisting working directory

The description promises “Prevents deletion outside working directory”, but there’s no check after GetFullPath. Add a guard to block paths outside the allowed root.

Apply this diff near path resolution:

-                var fullPath = Path.GetFullPath(path);
+                var fullPath = Path.GetFullPath(path);
+                var root = Directory.GetCurrentDirectory();
+                if (!IsSubPathOf(fullPath, root))
+                {
+                    return CreateErrorResult($"Refusing to delete outside working directory: {fullPath}");
+                }

Add helper in the class:

private static bool IsSubPathOf(string childPath, string parentPath)
{
    var child = Path.GetFullPath(childPath)
        .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
    var parent = Path.GetFullPath(parentPath)
        .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

    if (child.Equals(parent, StringComparison.OrdinalIgnoreCase))
        return true;

    return child.StartsWith(parent + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

Also applies to: 100-113


171-173: Pattern + recursive can attempt to delete non-empty directories → IOException

With recursive=true and a restrictive pattern, you schedule all directories for deletion regardless of whether non-matching files remain. Directory.Delete(dir, false) then throws for non-empty dirs and aborts the whole operation.

Minimally fix in PerformDeletion by skipping non-empty directories and respecting force for read-only dirs:

-            foreach (var item in items.Where(i => i.Type == "directory").OrderByDescending(i => i.Path.Length))
+            foreach (var item in items.Where(i => i.Type == "directory").OrderByDescending(i => i.Path.Length))
             {
-                if (Directory.Exists(item.Path))
-                {
-                    Directory.Delete(item.Path, false);
-                    deleted.Add(item);
-                }
+                if (!Directory.Exists(item.Path))
+                    continue;
+
+                // Clear read-only attributes on directories when forced
+                if (force)
+                {
+                    var attrs = File.GetAttributes(item.Path);
+                    if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
+                    {
+                        File.SetAttributes(item.Path, attrs & ~FileAttributes.ReadOnly);
+                    }
+                }
+
+                // Only delete if empty (patterned deletions may leave residual files)
+                if (!Directory.EnumerateFileSystemEntries(item.Path).Any())
+                {
+                    Directory.Delete(item.Path, false);
+                    deleted.Add(item);
+                }
             }

Optionally, avoid adding directories at all when a pattern is provided and recursive=true, and instead decide directory cleanup post-file deletions. I can provide that refactor if preferred.

Also applies to: 204-238, 255-262

Tools/SearchAndReplaceTool.cs (5)

259-263: Fix compile error: Matches type should be List

CI failure shows a type mismatch: List vs List. FileResult.Matches is List, so initialize accordingly.

 var result = new FileResult
 {
     Path = filePath,
-    Matches = new List<MatchInfo>()
+    Matches = new List<SearchMatchInfo>()
 };

341-354: Align GeneratePreview signature with SearchMatchInfo and highlight the exact occurrence

  • Signature still uses List while callers pass List.
  • Replacing via string.Replace highlights all occurrences; use Column + MatchedText length to bracket only the matched span.
-private string GeneratePreview(string original, string modified, List<MatchInfo> matches)
+private string GeneratePreview(string original, string modified, List<SearchMatchInfo> matches)
 {
     var preview = new StringBuilder();
     var maxMatches = Math.Min(matches.Count, 3);
     
     for (int i = 0; i < maxMatches; i++)
     {
         var match = matches[i];
         preview.AppendLine($"  Line {match.Line}:");
         preview.AppendLine($"    - {match.LineContent}");
-        
-        var modifiedLine = match.LineContent.Replace(match.MatchedText, $"[{match.MatchedText}]");
-        preview.AppendLine($"    + {modifiedLine}");
+
+        // Bracket exactly the matched occurrence using 1-based Column
+        var col0 = Math.Max(0, match.Column - 1);
+        var len = Math.Max(0, match.MatchedText?.Length ?? 0);
+        col0 = Math.Min(col0, match.LineContent.Length);
+        var safeLen = Math.Min(len, Math.Max(0, match.LineContent.Length - col0));
+        var before = match.LineContent.Substring(0, col0);
+        var after = match.LineContent.Substring(col0 + safeLen);
+        var highlighted = $"{before}[{match.MatchedText}]{after}";
+        preview.AppendLine($"    + {highlighted}");
     }
     
     if (matches.Count > 3)
     {
         preview.AppendLine($"  ... and {matches.Count - 3} more matches");
     }
     
     return preview.ToString();
 }

327-331: Guard against index==0: LastIndexOf with negative startIndex throws

When a match starts at index 0, LastIndexOf('\n', index - 1) passes -1 and throws ArgumentOutOfRangeException. Guard and handle “no prior newline”.

 private int GetColumnNumber(string content, int index)
 {
-    var lastNewline = content.LastIndexOf('\n', index - 1);
-    return index - lastNewline;
+    if (index <= 0) return 1;
+    var lastNewline = content.LastIndexOf('\n', index - 1);
+    if (lastNewline < 0) return index + 1;
+    return index - lastNewline;
 }

333-339: Avoid exception on index==0 and strip CR from CRLF lines in previews

Same negative startIndex issue here; also trim trailing '\r' so previews don’t show stray carriage returns.

 private string GetLineContent(string content, int index)
 {
-    var start = content.LastIndexOf('\n', index - 1) + 1;
+    var prevNewline = index > 0 ? content.LastIndexOf('\n', index - 1) : -1;
+    var start = prevNewline + 1;
     var end = content.IndexOf('\n', index);
     if (end == -1) end = content.Length;
-    return content.Substring(start, end - start);
+    return content.Substring(start, end - start).TrimEnd('\r');
 }

424-438: Harden path containment check to prevent prefix-bypass

StartsWith(currentDirectory) allows siblings like C:\repo2 when currentDirectory is C:\repo. Normalize with trailing separators or use a relative-path check.

 private void ValidatePathSecurity(string path)
 {
     if (string.IsNullOrWhiteSpace(path))
     {
         throw new ArgumentException("Path cannot be null or empty");
     }
     
-    var fullPath = Path.GetFullPath(path);
-    var currentDirectory = Path.GetFullPath(Directory.GetCurrentDirectory());
-    
-    if (!fullPath.StartsWith(currentDirectory, StringComparison.OrdinalIgnoreCase))
+    var fullPath = Path.GetFullPath(path);
+    var currentDirectory = Path.GetFullPath(Directory.GetCurrentDirectory());
+    var currentWithSep = currentDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) 
+                         + Path.DirectorySeparatorChar;
+    var fullWithSep = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) 
+                      + Path.DirectorySeparatorChar;
+    if (!fullWithSep.StartsWith(currentWithSep, StringComparison.OrdinalIgnoreCase))
     {
         throw new SecurityException($"Access denied: Path '{path}' is outside the working directory");
     }
 }
Tools/GrepTool.cs (4)

29-31: Documentation parameter mismatch: 'caseSensitive' vs 'ignoreCase'.

The How to use text says 'caseSensitive', while the actual parameter is 'ignoreCase'. This is user-facing and can cause incorrect usage.

Apply:

- - Set 'caseSensitive' to false for case-insensitive search
+ - Set 'ignoreCase' to true for case-insensitive search

Also applies to: 67-71


116-117: Add a regex timeout (and optionally compile) to prevent catastrophic backtracking (ReDoS).

Patterns are user-provided; unbounded regex evaluation can hang the process.

Apply:

-            var regexOptions = ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
-            var regex = new Regex(pattern, regexOptions);
+            var regexOptions = (ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None) | RegexOptions.Compiled;
+            var regex = new Regex(pattern, regexOptions, TimeSpan.FromSeconds(2));

148-176: Global maxResults enforcement is broken inside SearchFile.

You pass the remaining budget (maxResults - results.Count) into SearchFile, but the loop condition compares the global results.Count to that remaining value. If results.Count is already > remaining (which it always is), the loop never executes for subsequent files. This silently under-reports results.

Minimal fix: compute a per-call target and compare against it.

-        private void SearchFile(string filePath, Regex regex, List<GrepResult> results, int maxResults)
+        private void SearchFile(string filePath, Regex regex, List<GrepResult> results, int maxResults)
         {
             try
             {
-                var lines = File.ReadAllLines(filePath);
-                for (int i = 0; i < lines.Length && results.Count < maxResults; i++)
+                var lines = File.ReadAllLines(filePath);
+                // Honor the remaining allowance passed in by caller
+                var targetCount = results.Count + maxResults;
+                for (int i = 0; i < lines.Length && results.Count < targetCount; i++)
                 {
                     var matches = regex.Matches(lines[i]);
                     if (matches.Count > 0)
                     {
                         results.Add(new GrepResult
                         {
                             FilePath = filePath,
                             LineNumber = i + 1,
                             Line = lines[i],
                             Matches = matches.Cast<Match>().Select(m => new MatchInfo
                             {
                                 Value = m.Value,
                                 Index = m.Index,
                                 Length = m.Length
                             }).ToList()
                         });
                     }
                 }
             }
             catch (Exception ex)
             {
-
+                System.Diagnostics.Debug.WriteLine($"[GrepTool] Failed reading '{filePath}': {ex.Message}");
             }
         }

If you’d like, I can propose a streaming version using File.ReadLines to further reduce memory.


173-176: Don’t swallow exceptions silently.

Currently, failures to read a file disappear. At minimum, log the error; ideally, propagate as a warning in the ToolResult.

If you prefer not to change the method above, at least:

-            catch (Exception ex)
-            {
-
-            }
+            catch (Exception ex)
+            {
+                System.Diagnostics.Debug.WriteLine($"[GrepTool] Failed reading '{filePath}': {ex.Message}");
+            }
Configuration/ConfigurationManager.cs (3)

28-46: Don’t swallow exceptions; fix CS0168 by logging or omitting the variable

CI warns: Line 47 CS0168 unused variable ex. More importantly, the exception is swallowed, making troubleshooting difficult.

Apply:

         public static async Task<PersistedAgentConfiguration?> LoadConfigurationAsync()
         {
             try
             {
                 if (!File.Exists(ConfigFilePath))
                 {
                     return null;
                 }

                 var json = await File.ReadAllTextAsync(ConfigFilePath);
                 var config = JsonSerializer.Deserialize<PersistedAgentConfiguration>(json, JsonOptions);
                 
-                if (config != null && config.RequireCommandApproval == null)
+                if (config != null && config.RequireCommandApproval == null)
                 {
                     config.RequireCommandApproval = true;
                 }
                 
                 return config;
             }
-            catch (Exception ex)
+            catch (Exception ex)
             {
-
-                return null;
+                Console.Error.WriteLine($"[LoadConfigurationAsync] Failed to load {ConfigFilePath}: {ex}");
+                return null;
             }
         }

54-70: Same as above for Save; fix CS0168 and add logging

The exception is swallowed and ex is unused (CI warning at Line 66). Log the error.

Apply:

         public static async Task SaveConfigurationAsync(PersistedAgentConfiguration config)
         {
             try
             {
                 if (!Directory.Exists(AppDataPath))
                 {
                     Directory.CreateDirectory(AppDataPath);
                 }

                 var json = JsonSerializer.Serialize(config, JsonOptions);
                 await File.WriteAllTextAsync(ConfigFilePath, json);
             }
-            catch (Exception ex)
+            catch (Exception ex)
             {
-
+                Console.Error.WriteLine($"[SaveConfigurationAsync] Failed to save {ConfigFilePath}: {ex}");
             }
         }

90-103: Only override booleans when explicitly set in persisted config

Pairs with making booleans nullable in PersistedAgentConfiguration to avoid accidental feature toggles on upgrade.

Apply:

         public static void ApplyToAgentConfiguration(AgentConfiguration target, PersistedAgentConfiguration source)
         {
             target.Name = source.Name ?? target.Name;
             target.Model = source.Model ?? target.Model;
             target.Temperature = source.Temperature ?? target.Temperature;
             target.MaxTokens = source.MaxTokens ?? target.MaxTokens;
             target.TopP = source.TopP ?? target.TopP;
-            target.EnableStreaming = source.EnableStreaming;
-            target.MaintainHistory = source.MaintainHistory;
+            if (source.EnableStreaming.HasValue) target.EnableStreaming = source.EnableStreaming.Value;
+            if (source.MaintainHistory.HasValue) target.MaintainHistory = source.MaintainHistory.Value;
             target.MaxHistoryMessages = source.MaxHistoryMessages ?? target.MaxHistoryMessages;
-            target.EnableTools = source.EnableTools;
+            if (source.EnableTools.HasValue) target.EnableTools = source.EnableTools.Value;
             target.ToolNames = source.ToolNames ?? target.ToolNames;
             target.RequireCommandApproval = source.RequireCommandApproval ?? true;
         }
Tools/ApplyDiffTool.cs (2)

598-619: Path containment check is vulnerable to prefix-matching bypass; enforce safe containment

Using StartsWith on raw full paths can be bypassed with similarly prefixed directories (e.g., C:\repo vs C:\repo-bak) or different roots. Replace the prefix check with a robust containment check.

Apply this diff to harden the validation:

-            try
-            {
-                var fullPath = Path.GetFullPath(filePath);
-                var currentDirectory = Path.GetFullPath(Directory.GetCurrentDirectory());
-                
-                if (!fullPath.StartsWith(currentDirectory, StringComparison.OrdinalIgnoreCase))
-                {
-                    throw new SecurityException($"Access denied: Path '{filePath}' is outside the working directory.");
-                }
-            }
+            try
+            {
+                var fullPath = Path.GetFullPath(filePath);
+                var basePath = Path.GetFullPath(Directory.GetCurrentDirectory());
+                // Ensure base path ends with a separator for strict containment
+                if (!basePath.EndsWith(Path.DirectorySeparatorChar) && !basePath.EndsWith(Path.AltDirectorySeparatorChar))
+                {
+                    basePath += Path.DirectorySeparatorChar;
+                }
+                // Reject if not under the working directory
+                if (!fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
+                {
+                    throw new SecurityException($"Access denied: Path '{filePath}' is outside the working directory.");
+                }
+            }

490-496: Validate Remove operations against file content to prevent accidental deletions

Currently, Remove blindly deletes the line at currentIndex without confirming it matches the expected content. This can corrupt files if the file diverged slightly from the expected state.

-                            else if (change.Type == ChangeType.Remove)
-                            {
-                                if (currentIndex < lines.Count)
-                                {
-                                    lines.RemoveAt(currentIndex);
-                                }
-                            }
+                            else if (change.Type == ChangeType.Remove)
+                            {
+                                if (currentIndex >= lines.Count)
+                                {
+                                    throw new InvalidOperationException($"Remove out of range at context '{hunk.ContextLine}'.");
+                                }
+                                var expected = change.Content ?? string.Empty;
+                                var actual = lines[currentIndex];
+                                if (!string.Equals(actual, expected, StringComparison.Ordinal))
+                                {
+                                    throw new InvalidOperationException($"Remove mismatch at context '{hunk.ContextLine}'. Expected '{expected}', found '{actual}'.");
+                                }
+                                lines.RemoveAt(currentIndex);
+                            }

Comment on lines +1 to +12
using Saturn.OpenRouter.Services;

namespace Saturn.Agents.Core.Objects
{
internal class StreamingToolCall
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public JsonStreamAccumulator ArgumentsAccumulator { get; } = new JsonStreamAccumulator();
public bool IsComplete => ArgumentsAccumulator.IsComplete;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Class is not referenced; either wire it up or remove to keep the cleanup focused.

The new internal class StreamingToolCall isn’t used anywhere (AgentBase accumulates deltas directly into ToolCall). Keeping dead code contradicts the “Clean up classes” objective.

Run this to confirm usage:

Options:

  • If unused, delete the file.
  • If intended, integrate it into the streaming path to accumulate JSON arguments robustly (via JsonStreamAccumulator) instead of raw string concatenation.

Also consider sealing and documenting the type:

-    internal class StreamingToolCall
+    internal sealed class StreamingToolCall

🏁 Script executed:

#!/bin/bash
# Verify whether StreamingToolCall is referenced anywhere
rg -nP '\bStreamingToolCall\b' -g '!**/bin/**' -g '!**/obj/**' -C2

Length of output: 420


Remove unused StreamingToolCall class

The StreamingToolCall class in Agents/Core/Objects/StreamingToolCall.cs is not referenced anywhere outside its own definition and therefore constitutes dead code. Please remove it to keep the “Clean up classes” objective focused:

• Delete the file Agents/Core/Objects/StreamingToolCall.cs
• Remove its entry from the corresponding project file (e.g., Agents.Core.csproj)
• If you intended to add streaming support, consider integrating JsonStreamAccumulator into the existing ToolCall workflow instead of introducing an unused class.

🤖 Prompt for AI Agents
In Agents/Core/Objects/StreamingToolCall.cs lines 1-12 the StreamingToolCall
class is unused; delete the file, remove its <Compile
Include="Agents\Core\Objects\StreamingToolCall.cs" /> entry from
Agents.Core.csproj (or equivalent project file), and if streaming support is
required instead integrate JsonStreamAccumulator into the existing ToolCall
implementation rather than keeping this separate unused class.

Comment on lines +1 to +6
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Repo-wide sanity check for TaskStatus usage

There may be other sites where TaskStatus is referenced unqualified and could be impacted by the BCL collision. Run this quick audit and tidy up if needed (either fully-qualify your enum or add a using alias).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Scanning for potentially ambiguous TaskStatus usages…"
# 1) Find unqualified TaskStatus references in C# files (excluding obvious comments)
rg -nP --type cs -C1 '\bTaskStatus\b' | sed -n '1,200p'

echo
echo "Occurrences of using System.Threading.Tasks in Objects/ (these are likely unnecessary):"
rg -nP --type cs '^using\s+System\.Threading\.Tasks\s*;' Agents/MultiAgent/Objects -n

cat << 'TIP'
Tip:
- Prefer fully-qualifying your enum as Saturn.Agents.MultiAgent.TaskStatus in DTOs, or
- Add an alias at the top of files that need both Task (BCL) and your enum:
  using AgentTaskStatus = Saturn.Agents.MultiAgent.TaskStatus;
  // then: public AgentTaskStatus Status { get; set; }
TIP

Length of output: 2237


Audit and Resolve TaskStatus Enum Collisions

We scanned the repo for unqualified TaskStatus references and BCL imports; here’s what needs attention:

• Remove unnecessary using System.Threading.Tasks; from DTO/object files that only reference your local enum. This avoids pulling in System.Threading.Tasks.TaskStatus by accident. Specifically, delete or comment out that directive in:

  • Agents/MultiAgent/Objects/SubAgentContext.cs
  • Agents/MultiAgent/Objects/ReviewerContext.cs
  • Agents/MultiAgent/Objects/ReviewDecision.cs
  • Agents/MultiAgent/Objects/AgentTaskResult.cs
  • Agents/MultiAgent/Objects/AgentTaskEnums.cs
  • Agents/MultiAgent/Objects/AgentStatusInfo.cs
  • Agents/MultiAgent/Objects/AgentTask.cs

Example diff for AgentTask.cs:

@@ -5,7 +5,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Threading.Tasks;    // remove to avoid TaskStatus collision
 using System.Text;
 using System.Threading.Tasks;

• In Agents/MultiAgent/AgentManager.cs, unqualified usages like:

Status = TaskStatus.Running;agentContext.CurrentTask!.Status = success 
    ? TaskStatus.Completed 
    : TaskStatus.Failed;

resolve to the BCL enum when using System.Threading.Tasks; is in scope. To ensure you’re using your local enum (Saturn.Agents.MultiAgent.Objects.TaskStatus), choose one of:

  1. Add a using-alias at the top:
    using AgentTaskStatus = Saturn.Agents.MultiAgent.Objects.TaskStatus;
    And update references:
    - Status = TaskStatus.Running;
    + Status = AgentTaskStatus.Running;
  2. Fully qualify each enum member:
    - Status = TaskStatus.Running;
    + Status = Saturn.Agents.MultiAgent.Objects.TaskStatus.Running;

Please apply these changes to prevent accidental BCL collisions and ensure all TaskStatus references point to your intended enum.

🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/AgentTask.cs around lines 1 to 6, remove the
unnecessary "using System.Threading.Tasks" directive so the file does not
inadvertently import the BCL TaskStatus; ensure any TaskStatus references in
this file resolve to the local enum by either adding a using-alias for
Saturn.Agents.MultiAgent.Objects.TaskStatus at the top or by fully qualifying
the enum where used, and apply the same removal of the System.Threading.Tasks
using to the other DTO/object files listed in the comment to prevent BCL
collisions.

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unnecessary using that triggers the ambiguity

using System.Threading.Tasks; isn’t used in this DTO and is the root cause of the TaskStatus collision here.

Apply:

-using System.Threading.Tasks;
+// (removed) Avoids name clash with Saturn.Agents.MultiAgent.TaskStatus
🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/AgentTask.cs around line 5, remove the unused using
directive "using System.Threading.Tasks;" which causes an ambiguity with
TaskStatus; delete that using so the DTO no longer imports the conflicting
namespace and the correct TaskStatus (presumably from your domain or another
namespace) is used.

public string Description { get; set; } = "";
public Dictionary<string, object>? Context { get; set; }
public DateTime StartedAt { get; set; }
public TaskStatus Status { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ambiguous reference to TaskStatus

TaskStatus here is ambiguous between System.Threading.Tasks.TaskStatus and your enum in Saturn.Agents.MultiAgent. This will fail to compile on most setups.

Apply this minimal diff to disambiguate and prevent future surprises:

-        public TaskStatus Status { get; set; }
+        public Saturn.Agents.MultiAgent.TaskStatus Status { get; set; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public TaskStatus Status { get; set; }
public Saturn.Agents.MultiAgent.TaskStatus Status { get; set; }
🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/AgentTask.cs around line 15, the property type
TaskStatus is ambiguous with System.Threading.Tasks.TaskStatus; update the
declaration to reference your enum explicitly (e.g.
Saturn.Agents.MultiAgent.TaskStatus Status { get; set; }) or add a using alias
for your enum and use that alias for the property to remove ambiguity and ensure
it compiles.

Comment on lines +19 to +25
public enum TaskStatus
{
Pending,
Running,
Completed,
Failed
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Name collision: TaskStatus conflicts with System.Threading.Tasks.TaskStatus

Defining TaskStatus here collides with the BCL enum System.Threading.Tasks.TaskStatus. This already causes an ambiguity in Agents/MultiAgent/Objects/AgentTask.cs where using System.Threading.Tasks; is present and TaskStatus is referenced unqualified.

Two safe fixes:

  • Minimal: Fully-qualify usages at call sites (e.g., Saturn.Agents.MultiAgent.TaskStatus) and avoid using System.Threading.Tasks; in DTOs that don’t need it.
  • Structural: Rename this enum to AgentTaskStatus to permanently avoid collisions.

I’ve proposed a targeted fix in AgentTask.cs; consider the structural rename as a follow-up to prevent future regressions.

🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/AgentTaskEnums.cs around lines 19-25, the enum
TaskStatus collides with System.Threading.Tasks.TaskStatus; rename the enum to
AgentTaskStatus and update its declaration and XML comments, then update all
call sites (e.g., Agents/MultiAgent/AgentTask.cs and any DTOs) to use
AgentTaskStatus instead of TaskStatus, remove or keep using directives as
appropriate, and run a build to ensure no remaining ambiguities or missing
references.

Comment on lines +1 to +6
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing namespace import (compile error).

Agent is defined in Saturn.Agents; add the using directive.

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
+using Saturn.Agents;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Saturn.Agents;
🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/ReviewerContext.cs around lines 1 to 6, the file is
missing the namespace import for the Agent type which lives in Saturn.Agents;
add a using Saturn.Agents; directive to the top of the file alongside the other
using statements, then save and rebuild to resolve the compile error.

Comment on lines +1 to +6
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing namespace imports (compile error).

Agent lives in Saturn.Agents and AgentStatus is in Saturn.Agents.MultiAgent. Without these usings this file won’t compile.

Apply this diff:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
+using Saturn.Agents;
+using Saturn.Agents.MultiAgent;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Saturn.Agents;
using Saturn.Agents.MultiAgent;
🤖 Prompt for AI Agents
In Agents/MultiAgent/Objects/SubAgentContext.cs around lines 1 to 6, the file is
missing required namespace imports so it won't compile because Agent is in
Saturn.Agents and AgentStatus is in Saturn.Agents.MultiAgent; add using
Saturn.Agents; and using Saturn.Agents.MultiAgent; at the top of the file
(alongside existing System usings) so the types resolve.

Comment on lines +16 to +21
public bool EnableStreaming { get; set; } = true;
public bool MaintainHistory { get; set; } = true;
public int? MaxHistoryMessages { get; set; }
public bool EnableTools { get; set; } = true;
public List<string>? ToolNames { get; set; }
public bool? RequireCommandApproval { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid silently enabling features on legacy configs (make booleans nullable and apply conditionally)

EnableStreaming/MaintainHistory/EnableTools default to true here. When older JSON lacks these fields, System.Text.Json keeps the initializer values, potentially turning features on unexpectedly when applying to AgentConfiguration.

Apply (DTO changes):

-        public bool EnableStreaming { get; set; } = true;
-        public bool MaintainHistory { get; set; } = true;
+        public bool? EnableStreaming { get; set; }
+        public bool? MaintainHistory { get; set; }
         public int? MaxHistoryMessages { get; set; }
-        public bool EnableTools { get; set; } = true;
+        public bool? EnableTools { get; set; }

And update ApplyToAgentConfiguration (see suggested diff in ConfigurationManager below) to only override when HasValue.

This preserves backward compatibility and prevents unintended behavior shifts for users upgrading.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public bool EnableStreaming { get; set; } = true;
public bool MaintainHistory { get; set; } = true;
public int? MaxHistoryMessages { get; set; }
public bool EnableTools { get; set; } = true;
public List<string>? ToolNames { get; set; }
public bool? RequireCommandApproval { get; set; }
public bool? EnableStreaming { get; set; }
public bool? MaintainHistory { get; set; }
public int? MaxHistoryMessages { get; set; }
public bool? EnableTools { get; set; }
public List<string>? ToolNames { get; set; }
public bool? RequireCommandApproval { get; set; }
🤖 Prompt for AI Agents
In Configuration/Objects/PersistedAgentConfiguration.cs around lines 16–21,
change the boolean DTO fields that currently default to true (EnableStreaming,
MaintainHistory, EnableTools) to nullable booleans (bool?) with no initializer
so legacy JSON doesn't silently enable features; leave RequireCommandApproval
as-is if already nullable and keep MaxHistoryMessages/ToolNames nullable. Then
update ApplyToAgentConfiguration to only assign each corresponding
AgentConfiguration property when the persisted field HasValue (for bool? fields)
or is not null (for ToolNames/MaxHistoryMessages), so existing configs aren’t
overridden unless the persisted DTO explicitly specifies a value.

{
public class CommandExecutorConfig
{
public SecurityMode SecurityMode { get; set; } = SecurityMode.Unrestricted;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Defaulting to Unrestricted execution is risky; consider safer-by-default.

If “Unrestricted” allows executing arbitrary commands, defaulting to it can create a footgun. If preserving behavior is required for this PR, at least track a follow-up to switch the default to the most restrictive mode in a subsequent release, or require callers to opt-in explicitly.

Please verify current call sites rely on Unrestricted as a default before changing it. To check usage and available enum values:


🏁 Script executed:

#!/bin/bash
# Find where the config is instantiated and what SecurityMode values exist.
rg -nP -C2 $'new\\s+CommandExecutorConfig\\s*\\('
rg -nP -C1 $'SecurityMode\\.(\\w+)'

Length of output: 1363


Security Risk: Defaulting to Unrestricted Execution

We’ve verified that by default SecurityMode is set to Unrestricted in:

  • Tools/Objects/CommandExecutorConfig.cs (line 7)
  • Tools/ExecuteCommandTool.cs constructors (lines 23 and 29), which both do new CommandExecutorConfig() when no config is provided
    As a result, all existing call sites run in Unrestricted mode by default, allowing arbitrary commands without approval.

Recommendations

  • If you need to preserve backward‐compatibility in this PR, file a follow-up issue to switch the default SecurityMode to the most restrictive option (e.g., DenyAll or an allowlist) in the next release.
  • For stronger guarantees, consider requiring callers to explicitly specify SecurityMode (making Unrestricted an opt-in only) or removing the parameterless constructor altogether.
🤖 Prompt for AI Agents
In Tools/Objects/CommandExecutorConfig.cs around line 7, the SecurityMode
property is defaulting to SecurityMode.Unrestricted which causes callers that
use new CommandExecutorConfig() (e.g., Tools/ExecuteCommandTool.cs constructors
at lines 23 and 29) to run in Unrestricted mode by default; change the default
to a safer setting (e.g., SecurityMode.DenyAll or SecurityMode.Allowlist) or
remove the parameterless default by making the property required via constructor
injection so callers must explicitly pass a SecurityMode, and if preserving
backward compatibility in this PR, create a follow-up issue to flip the default
to the most restrictive option in the next release and/or require explicit
opt-in for Unrestricted.

- Rename `MatchInfo` to `SearchMatchInfo` for clarity.
- This change improves code readability and maintainability.

Commit generated by The Saturn Pipeline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant