Forge
csharpfb8e0499
1namespace AgentNotes.Core;
2
3/// <summary>Resolves knowledge repository roots from TOML (<c>--config</c>), tool args, or legacy inference.</summary>
4public static class KnowledgeRootResolution
5{
6 /// <summary>Resolve root for read operations (<c>read_knowledge_file</c>, <c>list_knowledge_files</c>).</summary>
7 public static string ResolveForRead(string? knowledgePath, string? knowledgeRootId) =>
8 Resolve(knowledgePath, knowledgeRootId, forWrite: false);
9
10 /// <summary>Resolve root for write operations; only <see cref="LocalSettings.PrimaryKnowledgeRoot"/> is writable when configured.</summary>
11 public static string ResolveForWrite(string? knowledgePath, string? knowledgeRootId) =>
12 Resolve(knowledgePath, knowledgeRootId, forWrite: true);
13
14 private static string Resolve(string? knowledgePath, string? knowledgeRootId, bool forWrite)
15 {
16 if (!string.IsNullOrWhiteSpace(knowledgePath) && !string.IsNullOrWhiteSpace(knowledgeRootId))
17 throw new ArgumentException("Specify either knowledge_path or knowledge_root_id, not both.");
18
19 if (!string.IsNullOrWhiteSpace(knowledgePath))
20 {
21 var path = Path.GetFullPath(knowledgePath.Trim());
22 if (forWrite)
23 EnsureWritable(path);
24 return path;
25 }
26
27 if (!string.IsNullOrWhiteSpace(knowledgeRootId))
28 return ResolveById(knowledgeRootId.Trim(), forWrite);
29
30 var fallback = NotesStorage.ResolveKnowledgeRootLegacy();
31 if (forWrite)
32 EnsureWritable(fallback);
33 return fallback;
34 }
35
36 private static string ResolveById(string id, bool forWrite)
37 {
38 id = id.Trim();
39
40 if (!AgentNotesRuntime.IsConfigured)
41 throw new ArgumentException(
42 "knowledge_root_id requires --config with [knowledge] and matching [[knowledge.read_only]] or [knowledge.roots].");
43
44 var settings = AgentNotesRuntime.Settings;
45 if (settings.KnowledgeRoots.TryGetValue(id, out var named))
46 {
47 if (forWrite)
48 EnsureWritable(named);
49 return named;
50 }
51
52 var readOnly = settings.ReadOnlyKnowledgeRoots
53 .FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
54 if (readOnly is not null)
55 {
56 if (forWrite)
57 throw new InvalidOperationException(
58 $"Knowledge root '{readOnly.Id}' is read-only ({readOnly.Path}). Writes go to primary only.");
59 return readOnly.Path;
60 }
61
62 throw new ArgumentException(
63 $"Unknown knowledge_root_id '{id}'. Known: {FormatKnownRootIds(settings)}.");
64 }
65
66 private static void EnsureWritable(string resolvedRoot)
67 {
68 if (!AgentNotesRuntime.IsConfigured)
69 return;
70
71 var primary = AgentNotesRuntime.Settings.PrimaryKnowledgeRoot;
72 if (PathsEqual(resolvedRoot, primary))
73 return;
74
75 var readOnly = AgentNotesRuntime.Settings.ReadOnlyKnowledgeRoots
76 .FirstOrDefault(r => PathsEqual(r.Path, resolvedRoot));
77 if (readOnly is not null)
78 throw new InvalidOperationException(
79 $"Knowledge root '{readOnly.Id}' is read-only. Writes are allowed only on primary ({primary}).");
80
81 throw new InvalidOperationException(
82 $"Writes are allowed only on primary knowledge root ({primary}), not '{resolvedRoot}'.");
83 }
84
85 internal static bool PathsEqual(string a, string b) =>
86 string.Equals(
87 Path.GetFullPath(a.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)),
88 Path.GetFullPath(b.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)),
89 OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
90
91 private static string FormatKnownRootIds(LocalSettings settings)
92 {
93 var ids = settings.KnowledgeRoots.Keys
94 .Concat(settings.ReadOnlyKnowledgeRoots.Select(r => r.Id))
95 .Distinct(StringComparer.OrdinalIgnoreCase)
96 .OrderBy(static x => x, StringComparer.OrdinalIgnoreCase);
97 return string.Join(", ", ids);
98 }
99}
100
View only · write via MCP/CIDE