V18 / 8d896b8a86e5
ProviderClient.Addons.cs
Lilith/Core/Addons/ProviderClient.Addons.cs
Changed in this versionCS
using System.Reflection;
using System.Runtime.Loader;
using System.Security.Cryptography;
using System.Text.Json;
using Lilith.Core.Addons;
using Lilith.Core.Scenarios;
using Lilith.Core.Tools;
using Lilith.Core.Workspace.Extensions;
namespace Lilith.Core.Providers;
public abstract partial class ProviderClient
{
internal ScenarioFileService? AddonScenarioFiles {get;private set;}
private void SetAddonCatalog(ScenarioFileService files) { AddonScenarioFiles=files;Schedules?.UpdateScenarioCatalog(files); }
private readonly List<AssemblyLoadContext> _addonContexts=[];
private readonly List<string> _addonTools=[];
private string? _bundledScenarioRoot;
public bool AddonRestartPending {get;private set;}
public IReadOnlyList<string> ActiveAddonScenarios {get;private set;}=[];
internal bool IsAddonTool(string name)=>_addonTools.Contains(name,StringComparer.Ordinal);
public string ReviewAddon(string id)
{
using var operation=EnterOperation();var p=new AddonStore(Workspace??throw new InvalidOperationException("Workspace unavailable.")).Load(id);
return JsonSerializer.Serialize(new{package=p,signature=AddonStore.Signature(p)},AddonStore.Json);
}
public string PendingAddons()
{
using var operation=EnterOperation();var store=new AddonStore(Workspace??throw new InvalidOperationException("Workspace unavailable."));
return JsonSerializer.Serialize(store.List().Where(p=>p.Status=="pending").Select(p=>new{Id=p.Id,Package=p,Signature=AddonStore.Signature(p)}),AddonStore.Json);
}
public string RejectAddon(string id,string signature)
{
using var operation=EnterOperation();var store=new AddonStore(Workspace??throw new InvalidOperationException("Workspace unavailable."));var p=store.Load(id);
if(p.Status!="pending"||AddonStore.Signature(p)!=signature)throw new InvalidOperationException("Stale addon decision.");store.Save(p with{Status="rejected"});return "Addon rejected; no code loaded.";
}
public async Task<string> ApproveAddonAsync(string id,string signature,CancellationToken ct=default)
{
using var operation=EnterOperation();return await ActivateAddonCoreAsync(id,signature,ct);
}
internal async Task<string> ActivateAddonCoreAsync(string id,string signature,CancellationToken ct)
{
var store=new AddonStore(Workspace??throw new InvalidOperationException("Workspace unavailable."));var p=store.Load(id);
if(p.Status!="pending"||AddonStore.Signature(p)!=signature)throw new InvalidOperationException("Stale or already decided addon approval.");
string dll=await store.Build(p,ct);string hash=Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(dll,ct)));
var current=store.Load(id);if(AddonStore.Signature(current)!=signature)throw new InvalidOperationException("Addon source changed during compilation.");
string approval=ApprovalPath(id);Directory.CreateDirectory(Path.GetDirectoryName(approval)!);
(approval+".tmp").EnsureNoLinks();
await File.WriteAllTextAsync(approval+".tmp",JsonSerializer.Serialize(new{signature,assemblyHash=hash}),ct);File.Move(approval+".tmp",approval,true);
store.Save(p with{Status="active",AssemblyHash=hash});AddonRestartPending=true;
return JsonSerializer.Serialize(new{status="restart_required",addon_id=id,scenario=p.Name,assembly_sha256=hash,message="Exact approved addon compiled. The addon runtime will restart after this turn; no generated tool has executed yet."});
}
private string ApprovalPath(string id)
{
if(!Guid.TryParseExact(id,"N",out _))throw new ArgumentException("Invalid addon ID.");
string path=Path.Combine(Workspace!.Options.RootDirectory,".lilith","addon-approvals",id+".json");path.EnsureNoLinks();return path;
}
internal void LoadAddons(string? root)
{
_bundledScenarioRoot=root;
var bundled=new ScenarioFileService(root);
if(Workspace is null){SetAddonCatalog(bundled);return;}
var store=new AddonStore(Workspace);
var active=store.List().Where(p=>p.Status=="active").Take(33).ToArray();
if(active.Length>32)throw new InvalidDataException("At most 32 active addon packages are supported.");
if(active.Length==0){SetAddonCatalog(bundled);ActiveAddonScenarios=[];return;}
string merged=Path.Combine(HistoryStore.RootDirectory,"AddonCatalog",Guid.NewGuid().ToString("N"));Directory.CreateDirectory(merged);
foreach(string n in bundled.ListNames())File.WriteAllText(Path.Combine(merged,n+".xml"),bundled.LoadScenarioXml(n));
List<string> names=[];
foreach(var p in active)
{
using var approval=JsonDocument.Parse(File.ReadAllText(ApprovalPath(p.Id)));
if(approval.RootElement.GetProperty("signature").GetString()!=AddonStore.Signature(p))throw new InvalidDataException("Approved addon source was modified.");
string dll=Path.Combine(store.Folder(p.Id),"Build","bin","Release",$"net{Environment.Version.Major}.0","CraftedAddon.dll");dll.EnsureNoLinks();
byte[] bytes=File.ReadAllBytes(dll);string hash=Convert.ToHexString(SHA256.HashData(bytes));
if(hash!=p.AssemblyHash||hash!=approval.RootElement.GetProperty("assemblyHash").GetString())throw new InvalidDataException("Approved addon assembly was modified or needs rebuilding for this runtime.");
if(File.Exists(Path.Combine(merged,p.Name+".xml"))||_tools.ContainsKey(p.ToolName))throw new InvalidDataException("Addon conflicts with an existing scenario/tool.");
var context=new AssemblyLoadContext("Addon-"+p.Id,true);context.Resolving+=(_,name)=>name.Name==typeof(ILilithTool).Assembly.GetName().Name?typeof(ILilithTool).Assembly:null;
_addonContexts.Add(context);
using var stream=new MemoryStream(bytes);var assembly=context.LoadFromStream(stream);
var types=assembly.GetExportedTypes().Where(t=>typeof(ILilithTool).IsAssignableFrom(t)&&!t.IsAbstract).ToArray();
if(types.Length!=1)throw new InvalidDataException("Addon must export exactly one tool class.");
var tool=(ILilithTool)(Activator.CreateInstance(types[0])??throw new InvalidDataException("Missing tool constructor."));
if(tool.Name!=p.ToolName)throw new InvalidDataException("Generated tool name differs from approval.");
if(tool is IAddonContextAware aware){string data=Path.Combine(store.Folder(p.Id),"Data");data.EnsureNoLinks();Directory.CreateDirectory(data);aware.Configure(new(data));}
_tools.Add(tool.Name,tool);_addonTools.Add(tool.Name);
File.WriteAllText(Path.Combine(merged,p.Name+".xml"),p.ScenarioXml);names.Add(p.Name);
}
SetAddonCatalog(new(merged));ActiveAddonScenarios=names;
}
public void RestartAddonRuntime()
{
using var operation=EnterOperation();
if(!AddonRestartPending)return;
SaveCore();
foreach(string name in _addonTools)_tools.Remove(name);_addonTools.Clear();
foreach(var context in _addonContexts)context.Unload();_addonContexts.Clear();
LoadAddons(_bundledScenarioRoot);
_tools["run_scenario"]=new RunScenarioTool(this,AddonScenarioFiles!);
if(_thinking is not null)_thinking=new(this,AddonScenarioFiles!.RootDirectory);
HistoryStore.StartNewSession();ClearCore();AddonRestartPending=false;
Logger.WriteLine("Boot","Addon runtime restarted with freshly compiled tools and a fresh conversation.",Lilith.Core.Logging.LogColors.Cyan);
}
}