Forge
csharp3407750f
1using AgentForge.Abstractions;
2using Tomlyn;
3using Tomlyn.Model;
4
5namespace AgentForge.Plugins;
6
7public static class ForgePluginManifestLoader
8{
9 internal const string EmbeddedManifestResourceName = "AgentForge.forge.plugins.defaults.toml";
10
11 public static ForgePluginManifest Load(string path)
12 {
13 if (!path.EndsWith(".toml", StringComparison.OrdinalIgnoreCase))
14 {
15 throw new InvalidOperationException(
16 $"Forge plugin manifest must be TOML (forge.plugins.toml). Got: '{path}'.");
17 }
18
19 return LoadTomlText(File.ReadAllText(path));
20 }
21
22 public static ForgePluginManifest LoadEmbedded()
23 {
24 var assembly = typeof(ForgePluginManifestLoader).Assembly;
25 using var stream = assembly.GetManifestResourceStream(EmbeddedManifestResourceName)
26 ?? throw new InvalidOperationException(
27 $"Embedded plugin manifest '{EmbeddedManifestResourceName}' not found.");
28
29 using var reader = new StreamReader(stream);
30 return LoadTomlText(reader.ReadToEnd());
31 }
32
33 internal static ForgePluginManifest LoadTomlText(string toml)
34 {
35 var model = Toml.ToModel(toml);
36 var manifest = new ForgePluginManifest();
37
38 if (model.TryGetValue("active_bundle", out var activeObj)
39 && activeObj is string active
40 && !string.IsNullOrWhiteSpace(active))
41 {
42 manifest.ActiveBundle = active.Trim();
43 }
44
45 if (model.TryGetValue("import", out var importObj) && importObj is TomlTable importTable)
46 manifest.Import = ReadImportSettings(importTable);
47
48 if (model.TryGetValue("bundles", out var bundlesObj) && bundlesObj is TomlTableArray bundlesArray)
49 {
50 manifest.Bundles.Clear();
51 foreach (var bundleTable in bundlesArray)
52 {
53 if (!bundleTable.TryGetValue("name", out var nameObj) || nameObj is not string name)
54 continue;
55
56 var plugins = ReadStringArray(bundleTable, "plugins");
57 manifest.Bundles[name.Trim()] = plugins;
58 }
59 }
60
61 if (model.TryGetValue("plugins", out var pluginsObj) && pluginsObj is TomlTable pluginsTable)
62 {
63 manifest.Host = ReadPluginHostSettings(pluginsTable);
64 manifest.Plugins.Clear();
65 foreach (var (pluginId, entryObj) in pluginsTable)
66 {
67 if (IsPluginHostSubsection(pluginId) || entryObj is not TomlTable entryTable)
68 continue;
69
70 manifest.Plugins[pluginId] = ReadPluginEntry(entryTable);
71 }
72 }
73
74 return manifest;
75 }
76
77 private static bool IsPluginHostSubsection(string key) =>
78 string.Equals(key, "marketplace", StringComparison.OrdinalIgnoreCase)
79 || string.Equals(key, "auto", StringComparison.OrdinalIgnoreCase);
80
81 private static ForgePluginHostSettings ReadPluginHostSettings(TomlTable pluginsTable)
82 {
83 var host = new ForgePluginHostSettings();
84 if (pluginsTable.TryGetValue("discovery", out var discoveryObj) && discoveryObj is string discovery)
85 host.Discovery = ParseDiscovery(discovery);
86
87 if (pluginsTable.TryGetValue("auto", out var autoObj) && autoObj is TomlTable autoTable)
88 host.Auto = ReadAutoDiscoverySettings(autoTable);
89
90 if (pluginsTable.TryGetValue("marketplace", out var marketplaceObj) && marketplaceObj is TomlTable marketplaceTable)
91 host.Marketplace = ReadMarketplaceSettings(marketplaceTable);
92
93 return host;
94 }
95
96 private static ForgePluginDiscovery ParseDiscovery(string value) =>
97 value.Trim().ToLowerInvariant() switch
98 {
99 "auto" => ForgePluginDiscovery.Auto,
100 _ => ForgePluginDiscovery.Manual,
101 };
102
103 private static ForgePluginAutoDiscoverySettings ReadAutoDiscoverySettings(TomlTable table)
104 {
105 var settings = new ForgePluginAutoDiscoverySettings
106 {
107 Exclude = ReadStringArray(table, "exclude"),
108 };
109
110 if (table.TryGetValue("require_manifest_entry", out var requireObj) && requireObj is bool require)
111 settings.RequireManifestEntry = require;
112
113 return settings;
114 }
115
116 private static ForgePluginMarketplaceSettings ReadMarketplaceSettings(TomlTable table)
117 {
118 var settings = new ForgePluginMarketplaceSettings();
119 if (table.TryGetValue("registry_url", out var urlObj) && urlObj is string url && !string.IsNullOrWhiteSpace(url))
120 settings.RegistryUrl = url.Trim();
121
122 if (table.TryGetValue("cache_path", out var cacheObj) && cacheObj is string cache && !string.IsNullOrWhiteSpace(cache))
123 settings.CachePath = cache.Trim();
124
125 return settings;
126 }
127
128 internal static ForgePluginHostSettings MergeHostSettings(
129 ForgePluginHostSettings embedded,
130 ForgePluginHostSettings overlay)
131 {
132 return new ForgePluginHostSettings
133 {
134 Discovery = overlay.Discovery ?? embedded.Discovery,
135 Auto = new ForgePluginAutoDiscoverySettings
136 {
137 Exclude = overlay.Auto.Exclude.Length > 0 ? overlay.Auto.Exclude : embedded.Auto.Exclude,
138 RequireManifestEntry = overlay.Auto.RequireManifestEntry || embedded.Auto.RequireManifestEntry,
139 },
140 Marketplace = new ForgePluginMarketplaceSettings
141 {
142 RegistryUrl = string.IsNullOrWhiteSpace(overlay.Marketplace.RegistryUrl)
143 ? embedded.Marketplace.RegistryUrl
144 : overlay.Marketplace.RegistryUrl,
145 CachePath = string.IsNullOrWhiteSpace(overlay.Marketplace.CachePath)
146 ? embedded.Marketplace.CachePath
147 : overlay.Marketplace.CachePath,
148 },
149 };
150 }
151
152 private static ForgeImportManifestSettings ReadImportSettings(TomlTable table)
153 {
154 var settings = new ForgeImportManifestSettings();
155 if (table.TryGetValue("default_visibility_policy", out var policyObj)
156 && policyObj is string policy
157 && !string.IsNullOrWhiteSpace(policy))
158 {
159 settings.DefaultVisibilityPolicy = policy.Trim();
160 }
161
162 if (table.TryGetValue("sync_visibility", out var syncObj) && syncObj is bool sync)
163 settings.SyncVisibility = sync;
164
165 return settings;
166 }
167
168 internal static ForgeImportManifestSettings MergeImportSettings(
169 ForgeImportManifestSettings embedded,
170 ForgeImportManifestSettings overlay)
171 {
172 return new ForgeImportManifestSettings
173 {
174 DefaultVisibilityPolicy = string.IsNullOrWhiteSpace(overlay.DefaultVisibilityPolicy)
175 ? embedded.DefaultVisibilityPolicy
176 : overlay.DefaultVisibilityPolicy,
177 SyncVisibility = overlay.SyncVisibility,
178 };
179 }
180
181 private static ForgePluginEntry ReadPluginEntry(TomlTable entryTable)
182 {
183 var entry = new ForgePluginEntry
184 {
185 Assembly = entryTable.TryGetValue("assembly", out var asmObj) && asmObj is string asm
186 ? asm.Trim()
187 : "",
188 Type = entryTable.TryGetValue("type", out var typeObj) && typeObj is string type
189 ? type.Trim()
190 : null,
191 Tier = entryTable.TryGetValue("tier", out var tierObj) && tierObj is string tier
192 ? tier.Trim()
193 : null,
194 VisibilityPolicy = entryTable.TryGetValue("visibility_policy", out var visObj) && visObj is string vis
195 ? vis.Trim()
196 : null,
197 };
198
199 if (entryTable.TryGetValue("sync_visibility", out var syncObj) && syncObj is bool sync)
200 entry.SyncVisibility = sync;
201
202 return entry;
203 }
204
205 private static string[] ReadStringArray(TomlTable table, string key)
206 {
207 if (!table.TryGetValue(key, out var obj))
208 return [];
209
210 return obj switch
211 {
212 TomlArray array => array.OfType<string>().Select(s => s.Trim()).Where(s => s.Length > 0).ToArray(),
213 string single when single.Length > 0 => [single.Trim()],
214 _ => [],
215 };
216 }
217}
218
View only · write via MCP/CIDE