Skip to content

Commit 759b75d

Browse files
committed
fix: tighten cli runtime safeguards
1 parent f3f13ab commit 759b75d

File tree

6 files changed

+159
-55
lines changed

6 files changed

+159
-55
lines changed

YCode.CLI/Managers/MemoryManager.cs

Lines changed: 121 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ internal class MemoryManager
55
{
66
private const int DailyRetentionDays = 30;
77
private const int MaxItemsPerList = 80;
8+
private const int MaxContextChars = 12000;
9+
private const int MaxContextLineChars = 280;
10+
private const int MaxAgentInstructionsChars = 3500;
811
private readonly string _rootDir;
912
private readonly string _profilePath;
1013
private readonly string _dailyDir;
@@ -249,66 +252,73 @@ public bool MaybeSaveHeartbeat(string userInput, int roundNumber)
249252

250253
var projectKey = ResolveProjectKey(null)!;
251254
var projectMemories = LoadProjectMemories(projectKey);
252-
var projectAgents = LoadProjectAgents(projectKey);
255+
var workspaceAgents = LoadWorkspaceAgents();
256+
var projectAgents = string.IsNullOrWhiteSpace(workspaceAgents)
257+
? LoadProjectAgents(projectKey)
258+
: string.Empty;
253259

254260
if (profile.Count == 0 && dailyList.Count == 0 && relatedNotes.Count == 0 && relatedDaily.Count == 0
255-
&& projectMemories.Count == 0 && string.IsNullOrWhiteSpace(projectAgents))
261+
&& projectMemories.Count == 0 && string.IsNullOrWhiteSpace(workspaceAgents) && string.IsNullOrWhiteSpace(projectAgents))
256262
{
257263
return null;
258264
}
259265

260266
var sb = new StringBuilder();
267+
var remainingChars = MaxContextChars - ("<memory>\n</memory>\n".Length);
261268
sb.AppendLine("<memory>");
262269

263270
if (profile.Count > 0)
264271
{
265-
sb.AppendLine("profile:");
266-
foreach (var item in profile.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(maxProfile))
267-
{
268-
sb.AppendLine($"- {item.Content}");
269-
}
272+
AppendSection(
273+
sb,
274+
"profile:",
275+
profile.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(maxProfile).Select(x => x.Content),
276+
ref remainingChars);
270277
}
271278

272279
if (projectMemories.Count > 0)
273280
{
274-
sb.AppendLine($"project ({projectKey}):");
275-
foreach (var item in projectMemories.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(20))
276-
{
277-
sb.AppendLine($"- {item.Content}");
278-
}
281+
AppendSection(
282+
sb,
283+
$"project ({projectKey}):",
284+
projectMemories.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(20).Select(x => x.Content),
285+
ref remainingChars);
279286
}
280287

281-
if (!string.IsNullOrWhiteSpace(projectAgents))
288+
if (!string.IsNullOrWhiteSpace(workspaceAgents))
282289
{
283-
sb.AppendLine($"project-agents ({projectKey}/AGENTS.md):");
284-
sb.AppendLine(projectAgents);
290+
AppendBlock(sb, "workspace-agents (AGENTS.md):", workspaceAgents, MaxAgentInstructionsChars, ref remainingChars);
291+
}
292+
else if (!string.IsNullOrWhiteSpace(projectAgents))
293+
{
294+
AppendBlock(sb, $"project-agents (.ycode/projects/{projectKey}/AGENTS.md):", projectAgents, MaxAgentInstructionsChars, ref remainingChars);
285295
}
286296

287297
if (dailyList.Count > 0)
288298
{
289-
sb.AppendLine($"daily ({todayKey}):");
290-
foreach (var item in dailyList.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(30))
291-
{
292-
sb.AppendLine($"- {item.Content}");
293-
}
299+
AppendSection(
300+
sb,
301+
$"daily ({todayKey}):",
302+
dailyList.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(30).Select(x => x.Content),
303+
ref remainingChars);
294304
}
295305

296306
if (relatedDaily.Count > 0)
297307
{
298-
sb.AppendLine("daily-related:");
299-
foreach (var item in relatedDaily)
300-
{
301-
sb.AppendLine($"- [{item.DateKey}] {item.Content}");
302-
}
308+
AppendSection(
309+
sb,
310+
"daily-related:",
311+
relatedDaily.Select(x => $"[{x.DateKey}] {x.Content}"),
312+
ref remainingChars);
303313
}
304314

305315
if (relatedNotes.Count > 0)
306316
{
307-
sb.AppendLine("notes:");
308-
foreach (var note in relatedNotes)
309-
{
310-
sb.AppendLine($"- {note.Title}: {note.Preview}");
311-
}
317+
AppendSection(
318+
sb,
319+
"notes:",
320+
relatedNotes.Select(x => $"{x.Title}: {x.Preview}"),
321+
ref remainingChars);
312322
}
313323

314324
sb.AppendLine("</memory>");
@@ -338,6 +348,14 @@ private string LoadProjectAgents(string projectKey)
338348
return string.IsNullOrWhiteSpace(content) ? string.Empty : content;
339349
}
340350

351+
private string LoadWorkspaceAgents()
352+
{
353+
var path = Path.Combine(_workDir, "AGENTS.md");
354+
if (!File.Exists(path)) return string.Empty;
355+
var content = File.ReadAllText(path).Trim();
356+
return string.IsNullOrWhiteSpace(content) ? string.Empty : content;
357+
}
358+
341359
private List<MemoryItem> LoadProjectMemories(string projectKey)
342360
{
343361
var path = Path.Combine(_projectsDir, projectKey, "memory.json");
@@ -552,6 +570,79 @@ private static string CompactText(string text, int maxChars)
552570
return compact.Length <= maxChars ? compact : compact[..maxChars] + "...";
553571
}
554572

573+
private static string TrimMultiline(string text, int maxChars)
574+
{
575+
var normalized = text.Replace("\r\n", "\n").Trim();
576+
if (normalized.Length <= maxChars)
577+
{
578+
return normalized;
579+
}
580+
581+
return normalized[..Math.Max(0, maxChars - 3)].TrimEnd() + "...";
582+
}
583+
584+
private static void AppendSection(StringBuilder sb, string header, IEnumerable<string> items, ref int remainingChars)
585+
{
586+
var materialized = items
587+
.Select(x => CompactText(x, MaxContextLineChars))
588+
.Where(x => !string.IsNullOrWhiteSpace(x))
589+
.ToList();
590+
591+
if (materialized.Count == 0 || !TryAppendLine(sb, header, ref remainingChars))
592+
{
593+
return;
594+
}
595+
596+
foreach (var item in materialized)
597+
{
598+
if (!TryAppendLine(sb, $"- {item}", ref remainingChars))
599+
{
600+
break;
601+
}
602+
}
603+
}
604+
605+
private static void AppendBlock(StringBuilder sb, string header, string content, int maxChars, ref int remainingChars)
606+
{
607+
if (string.IsNullOrWhiteSpace(content) || !TryAppendLine(sb, header, ref remainingChars))
608+
{
609+
return;
610+
}
611+
612+
foreach (var line in TrimMultiline(content, maxChars).Split('\n'))
613+
{
614+
if (!TryAppendLine(sb, line, ref remainingChars))
615+
{
616+
break;
617+
}
618+
}
619+
}
620+
621+
private static bool TryAppendLine(StringBuilder sb, string line, ref int remainingChars)
622+
{
623+
if (remainingChars <= 0)
624+
{
625+
return false;
626+
}
627+
628+
var normalized = line.Replace("\r", string.Empty);
629+
var required = normalized.Length + Environment.NewLine.Length;
630+
if (required > remainingChars)
631+
{
632+
if (remainingChars <= 3 + Environment.NewLine.Length)
633+
{
634+
return false;
635+
}
636+
637+
normalized = normalized[..Math.Max(0, remainingChars - Environment.NewLine.Length - 3)].TrimEnd() + "...";
638+
required = normalized.Length + Environment.NewLine.Length;
639+
}
640+
641+
sb.AppendLine(normalized);
642+
remainingChars -= required;
643+
return true;
644+
}
645+
555646
private static string? ResolveDateKey(string? date)
556647
{
557648
if (string.IsNullOrWhiteSpace(date)) return DateTime.Now.ToString("yyyy-MM-dd");
@@ -590,7 +681,3 @@ internal class MemoryItem
590681
}
591682
}
592683

593-
594-
595-
596-

YCode.CLI/Program.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
var agentManager = provider.GetRequiredService<AgentManager>();
99
var agentContext = provider.GetRequiredService<AgentContext>();
1010
var config = provider.GetRequiredService<AppConfig>();
11+
var appVersion =
12+
typeof(AppConfig).Assembly.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
13+
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
14+
.FirstOrDefault()?.InformationalVersion?.Split('+')[0]
15+
?? typeof(AppConfig).Assembly.GetName().Version?.ToString()
16+
?? "dev";
1117

1218
var initial_reminder = $"""
1319
'<reminder source="system" topic="todos">'
@@ -290,7 +296,7 @@ void Banner()
290296
AnsiConsole.MarkupLine($"[dim]{topBorder}[/]");
291297
var emptyLine = "│" + new string(' ', bannerWidth - 2) + "│";
292298
AnsiConsole.MarkupLine($"[dim]{emptyLine}[/]");
293-
var titleText = "YCode v1.0.0";
299+
var titleText = $"YCode v{appVersion}";
294300
var titlePadding = (bannerWidth - 2 - titleText.Length) / 2;
295301
var titleLine = "│" + new string(' ', titlePadding) + $"[bold cyan]{titleText}[/]" + new string(' ', bannerWidth - 2 - titlePadding - titleText.Length) + "│";
296302
AnsiConsole.MarkupLine($"[dim]{titleLine}[/]");
@@ -460,4 +466,3 @@ public void Dispose()
460466
}
461467

462468
#endregion
463-

YCode.CLI/ServiceRegistration.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,9 @@ internal static class ServiceRegistration
66
{
77
public static IServiceProvider Register(this IServiceCollection services)
88
{
9-
// Register ConfigManager first to ensure it's available
10-
services.AddSingleton<ConfigManager>();
11-
12-
// Build a temporary provider to get ConfigManager
13-
var tempProvider = services.BuildServiceProvider();
14-
var configManager = tempProvider.GetRequiredService<ConfigManager>();
15-
16-
// Ensure configuration is set up
9+
var configManager = new ConfigManager();
1710
configManager.EnsureConfiguration();
1811

19-
// Get configuration values from ConfigManager
2012
var key = configManager.GetEnvironmentVariable("YCODE_AUTH_TOKEN")
2113
?? throw new InvalidOperationException("YCODE_AUTH_TOKEN is required but not configured");
2214
var uri = configManager.GetEnvironmentVariable("YCODE_API_BASE_URI")
@@ -34,6 +26,7 @@ public static IServiceProvider Register(this IServiceCollection services)
3426
? "macOS"
3527
: "Unknown";
3628

29+
services.AddSingleton(configManager);
3730
services.AddSingleton(new AppConfig(key, uri, model, workDir, osPlatform, osDescription));
3831

3932
services.RegisterAttributedServices();
@@ -60,6 +53,14 @@ private static void RegisterAttributedServices(this IServiceCollection services)
6053
}
6154

6255
var serviceType = attr.ServiceType ?? type;
56+
var alreadyRegistered = services.Any(descriptor =>
57+
descriptor.ServiceType == serviceType &&
58+
(descriptor.ImplementationType == type || descriptor.ImplementationInstance?.GetType() == type));
59+
60+
if (alreadyRegistered)
61+
{
62+
continue;
63+
}
6364

6465
switch (attr.Lifetime)
6566
{
@@ -107,4 +108,3 @@ public InjectAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton) : b
107108
}
108109
}
109110

110-

YCode.CLI/Tools/FileTools.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,21 @@ public static string NormalizePath(string path, AppConfig config)
472472
throw new Exception("path is required.");
473473
}
474474

475+
var workspaceRoot = Path.GetFullPath(config.WorkDir)
476+
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
475477
var fullPath = Path.IsPathRooted(path)
476478
? Path.GetFullPath(path)
477479
: Path.GetFullPath(Path.Combine(config.WorkDir, path));
480+
var comparison = config.OsPlatform == "Windows"
481+
? StringComparison.OrdinalIgnoreCase
482+
: StringComparison.Ordinal;
483+
var workspacePrefix = workspaceRoot + Path.DirectorySeparatorChar;
484+
485+
if (!fullPath.Equals(workspaceRoot, comparison) &&
486+
!fullPath.StartsWith(workspacePrefix, comparison))
487+
{
488+
throw new Exception("Path is outside the workspace.");
489+
}
478490

479491
return fullPath;
480492
}
@@ -483,4 +495,3 @@ public static string NormalizePath(string path, AppConfig config)
483495

484496

485497

486-

YCode.CLI/Tools/SkillTool.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,19 @@ public SkillTool(SkillsManager skills)
3131
public bool IsEnable => true;
3232
public Delegate Handler => this.Run;
3333

34-
private string Run(string skillName)
34+
private string Run(string skill)
3535
{
36-
var content = _skills.GetSkillContent(skillName);
36+
var content = _skills.GetSkillContent(skill);
3737

3838
if (String.IsNullOrWhiteSpace(content))
3939
{
4040
var available = String.Join(',', _skills.GetSkills()) ?? "none";
4141

42-
return $"Error: Unknown skill '{skillName}'. Available: {available}";
42+
return $"Error: Unknown skill '{skill}'. Available: {available}";
4343
}
4444

4545
return $"""
46-
<skill-loaded name="{skillName}">
46+
<skill-loaded name="{skill}">
4747
{content}
4848
</skill-loaded>
4949
@@ -55,4 +55,3 @@ Follow the instructions in the skill above to complete the user's task.
5555

5656

5757

58-

YCode.CLI/Tools/TaskTool.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public TaskTool(AppConfig config, McpManager mcp, AgentManager manager)
1212
_config = config;
1313
_mcp = mcp;
1414
_manager = manager;
15+
var supportedAgents = String.Join(", ", _manager.Agents.Keys.Select(x => $"\"{x}\""));
1516

1617
this.Description = $$"""
1718
{
@@ -22,7 +23,7 @@ public TaskTool(AppConfig config, McpManager mcp, AgentManager manager)
2223
"properties": {
2324
"description": { "type": "string", "description": "Short task name (3-5 words) for progress display" },
2425
"prompt": { "type": "string", "description": "Detailed instructions for the subagent" },
25-
"agent_type": { "type": "string", "enum": [], "description": "Type of agent to spawn" }
26+
"agent_type": { "type": "string", "enum": [{{supportedAgents}}], "description": "Type of agent to spawn" }
2627
},
2728
"required": ["description", "prompt", "agent_type"],
2829
"additionalProperties": false
@@ -37,8 +38,10 @@ public TaskTool(AppConfig config, McpManager mcp, AgentManager manager)
3738
public bool IsEnable => true;
3839
public Delegate Handler => this.Run;
3940

40-
private async Task<string> Run(string description, string prompt, string agentType)
41+
private async Task<string> Run(string description, string prompt, string agent_type)
4142
{
43+
var agentType = agent_type?.Trim() ?? String.Empty;
44+
4245
if (!_manager.Agents.ContainsKey(agentType))
4346
{
4447
throw new NotSupportedException($"Agent type '{agentType}' is not supported.");
@@ -185,4 +188,3 @@ private static string EscapeMarkup(string text)
185188

186189

187190

188-

0 commit comments

Comments
 (0)