summaryrefslogtreecommitdiff
path: root/src/Agent.App/Program.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.App/Program.cs')
-rw-r--r--src/Agent.App/Program.cs130
1 files changed, 130 insertions, 0 deletions
diff --git a/src/Agent.App/Program.cs b/src/Agent.App/Program.cs
new file mode 100644
index 0000000..3fae93f
--- /dev/null
+++ b/src/Agent.App/Program.cs
@@ -0,0 +1,130 @@
+using Agent.App.Cli;
+using Agent.App.Configuration;
+using Agent.App.Logging;
+using Agent.Core.Localization;
+using Agent.Core.Prompts;
+using Agent.Mcp;
+using Agent.Core.Runtime;
+using Agent.Windows.Diagnostics;
+using Agent.Core.Tools;
+using Agent.Windows.Recovery;
+using Agent.Windows.Security;
+using Agent.Windows.Tools;
+
+namespace Agent.App;
+
+internal static class Program
+{
+ public static async Task<int> Main(string[] args)
+ {
+ try
+ {
+ CommandLine commandLine = CommandLine.Parse(args);
+ string configPath = commandLine.ConfigPath is null
+ ? Path.Combine(AppContext.BaseDirectory, "agent.yaml")
+ : Path.GetFullPath(commandLine.ConfigPath);
+ AgentConfiguration configuration = ConfigurationLoader.Load(configPath);
+ if (commandLine.Transport is not null) configuration.Mcp.Transport = commandLine.Transport;
+ ConfigurationLoader.Validate(configuration, Path.GetDirectoryName(configPath)!);
+
+ string language = commandLine.Language ?? configuration.Application.Language ?? "en";
+ string baseDirectory = Path.GetDirectoryName(configPath)!;
+ string localeDirectory = ConfigurationLoader.Resolve(baseDirectory, configuration.Localization.Directory);
+ LocaleCatalog locale = LocaleCatalog.Load(localeDirectory, language, configuration.Localization.Fallback);
+ _ = FixedPrompt.Load();
+
+ string logDirectory = ConfigurationLoader.Resolve(baseDirectory, configuration.Logging.Directory);
+ UserOnlyDirectory.Ensure(logDirectory);
+ using var log = new RollingFileLog(logDirectory, configuration.Logging.MaxFileBytes, configuration.Logging.RetainedFiles);
+ if (commandLine.Command == AppCommand.Check)
+ {
+ WindowsPlatformCheckResult platform = WindowsPlatformCheck.Run();
+ Console.Out.WriteLine($"administrator={platform.IsAdministrator.ToString().ToLowerInvariant()}");
+ Console.Out.WriteLine($"bfe_engine_open={platform.BfeEngineOpen.ToString().ToLowerInvariant()}");
+ Console.Out.WriteLine($"wfp_write_transaction={platform.WfpWriteTransaction.ToString().ToLowerInvariant()}");
+ Console.Out.WriteLine($"wfp_filter_add_delete={platform.WfpFilterAddDelete.ToString().ToLowerInvariant()}");
+ if (!platform.Success)
+ {
+ string detail = $"{locale.Get("check.wfp_admin_required")} error_code={platform.ErrorCode}; operation={platform.ErrorOperation}";
+ string failure = locale.Format("check.failure", new Dictionary<string, object?> { ["message"] = detail });
+ log.Error(failure);
+ Console.Error.WriteLine(failure);
+ return 2;
+ }
+ log.Information(locale.Get("check.success"));
+ Console.Out.WriteLine(locale.Get("check.success"));
+ return 0;
+ }
+
+ log.Information(locale.Get("app.starting"));
+ InternalToolDispatcher toolDispatcher = CreateToolDispatcher(configuration, baseDirectory, logDirectory);
+ var runtimeObserver = new JsonLinesRuntimeObserver(logDirectory);
+ if (configuration.Mcp.Transport == "stdio")
+ {
+ string userPrompt = File.ReadAllText(ConfigurationLoader.Resolve(baseDirectory, configuration.Agent.UserSystemPrompt));
+ string skillDirectory = ConfigurationLoader.Resolve(baseDirectory, configuration.Skills.Directory);
+ string[] skills = Directory.EnumerateFiles(skillDirectory, "*.md", SearchOption.TopDirectoryOnly).Select(File.ReadAllText).ToArray();
+ var runtimeSettings = new AgentRuntimeSettings(userPrompt, language, skills, "Windows AppContainer, ACL, Job Object, child-process restriction, and WFP sandbox capabilities.", configuration.Agent.MaxIterations, configuration.Agent.SameFailureLimit, locale.Get("ollama.vision_unsupported"));
+ var visionLimits = new VisionLimits(configuration.Mcp.Images.MaxCount, configuration.Mcp.Images.MaxBytesEach, configuration.Mcp.Images.MaxTotalBytes);
+ await McpHost.RunStdioAsync(locale, OllamaSettingsFactory.Create(configuration.Ollama), runtimeSettings, visionLimits, CancellationToken.None, toolDispatcher, runtimeObserver).ConfigureAwait(false);
+ return 0;
+ }
+ if (configuration.Mcp.Transport == "http")
+ {
+ string userPrompt = File.ReadAllText(ConfigurationLoader.Resolve(baseDirectory, configuration.Agent.UserSystemPrompt));
+ string skillDirectory = ConfigurationLoader.Resolve(baseDirectory, configuration.Skills.Directory);
+ string[] skills = Directory.EnumerateFiles(skillDirectory, "*.md", SearchOption.TopDirectoryOnly).Select(File.ReadAllText).ToArray();
+ var runtimeSettings = new AgentRuntimeSettings(userPrompt, language, skills, "Windows AppContainer, ACL, Job Object, child-process restriction, and WFP sandbox capabilities.", configuration.Agent.MaxIterations, configuration.Agent.SameFailureLimit, locale.Get("ollama.vision_unsupported"));
+ var visionLimits = new VisionLimits(configuration.Mcp.Images.MaxCount, configuration.Mcp.Images.MaxBytesEach, configuration.Mcp.Images.MaxTotalBytes);
+ await McpHost.RunHttpAsync(locale, OllamaSettingsFactory.Create(configuration.Ollama), runtimeSettings, visionLimits,
+ configuration.Mcp.Http.Host, configuration.Mcp.Http.Port, CancellationToken.None, toolDispatcher, runtimeObserver).ConfigureAwait(false);
+ return 0;
+ }
+ throw new ConfigurationException("Unsupported MCP transport.");
+ }
+ catch (Exception ex) when (ex is CommandLineException or ConfigurationException or LocaleException or IOException or UnauthorizedAccessException)
+ {
+ Console.Error.WriteLine($"agent-mcp: {ex.Message}");
+ return 2;
+ }
+ catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
+ {
+ Console.Error.WriteLine($"agent-mcp: [APP-UNHANDLED] {ex.Message}");
+ return 2;
+ }
+ }
+
+ private static InternalToolDispatcher CreateToolDispatcher(AgentConfiguration configuration, string baseDirectory, string logDirectory)
+ {
+ string stateDirectory = Path.Combine(baseDirectory, "state");
+ string journalDirectory = Path.Combine(stateDirectory, "journals");
+ string backupDirectory = Path.Combine(stateDirectory, "backups");
+ Directory.CreateDirectory(journalDirectory);
+ Directory.CreateDirectory(backupDirectory);
+ var audit = new JsonLinesSecurityAuditSink(Path.Combine(logDirectory, "audit.jsonl"));
+ RecoveryResult recovery = new RecoveryManager(journalDirectory, audit).RecoverAll();
+ if (!recovery.IsClean) throw new InvalidOperationException("Recovery did not reach a clean state: " + string.Join(" | ", recovery.Errors));
+ var fileEntries = new List<FilePolicyEntry>();
+ Add(configuration.Sandbox.Filesystem.Read, FileCapability.Read);
+ Add(configuration.Sandbox.Filesystem.Write, FileCapability.Write);
+ Add(configuration.Sandbox.Filesystem.Execute, FileCapability.Execute);
+ Add(configuration.Sandbox.Filesystem.Delete, FileCapability.Delete);
+ var settings = new WindowsSandboxSettings(
+ fileEntries,
+ configuration.Sandbox.Process.Approvals.Select(approval => new Agent.Windows.Security.ExecutableApproval(approval.Path, approval.Sha256)).ToArray(),
+ configuration.Sandbox.Process.AllowShell,
+ new SandboxJobLimits((uint)configuration.Sandbox.Process.MaxProcesses, checked((ulong)configuration.Sandbox.Resources.MemoryMb * 1024 * 1024)),
+ configuration.Sandbox.Network.Mode,
+ configuration.Sandbox.Network.Allow.Select(item => (item.Host, (IReadOnlyList<int>)item.Ports)).ToArray(),
+ journalDirectory, backupDirectory, checked((long)configuration.FileEdit.MemoryBackupLimitMb * 1024 * 1024),
+ configuration.Limits.WholeReadBytes, configuration.Limits.SearchMaxResults, configuration.Limits.TailMaxLines);
+ var backend = new WindowsToolBackend(settings, audit, new TaskGraphToolHandler(configuration.Planner.MaxTasks));
+ var allowed = InternalToolCatalog.All.Select(static item => item.Name).ToHashSet(StringComparer.Ordinal);
+ return new InternalToolDispatcher(new DenyByDefaultPolicy(allowed), backend);
+
+ void Add(IEnumerable<string> paths, FileCapability capability)
+ {
+ foreach (string path in paths) fileEntries.Add(new FilePolicyEntry(Path.IsPathFullyQualified(path) ? path : ConfigurationLoader.Resolve(baseDirectory, path), capability));
+ }
+ }
+}