summaryrefslogtreecommitdiff
path: root/src/Agent.App/Program.cs
blob: 6f3485f9bc1a27ba83e0215f97a56bf751900639 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using Agent.App.Cli;
using Agent.App.Configuration;
using Agent.App.Logging;
using Agent.Core.Localization;
using Agent.Core.Prompts;
using Agent.Core.Sessions;
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, configuration.Logging.Level);
            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"));
            WindowsPlatformCheckResult servePlatform = WindowsPlatformCheck.Run();
            if (!servePlatform.Success)
            {
                string failure = locale.Format("app.preflight_failed", new Dictionary<string, object?>
                {
                    ["operation"] = servePlatform.ErrorOperation ?? "privilege/BFE/WFP probe",
                    ["error_code"] = servePlatform.ErrorCode,
                    ["administrator"] = servePlatform.IsAdministrator.ToString().ToLowerInvariant(),
                    ["bfe"] = servePlatform.BfeEngineOpen.ToString().ToLowerInvariant(),
                    ["transaction"] = servePlatform.WfpWriteTransaction.ToString().ToLowerInvariant(),
                    ["filters"] = servePlatform.WfpFilterAddDelete.ToString().ToLowerInvariant()
                });
                log.Fatal(failure);
                return 2;
            }

            var sessionStore = new SessionStore();
            InternalToolDispatcher toolDispatcher = CreateToolDispatcher(configuration, baseDirectory, logDirectory, sessionStore);
            using var runtimeObserver = new JsonLinesRuntimeObserver(logDirectory, configuration.Logging.MaxFileBytes, configuration.Logging.RetainedFiles);
            string taskStateDirectory = Path.Combine(baseDirectory, "state", "mcp-tasks");
            UserOnlyDirectory.Ensure(taskStateDirectory);
            var taskStoreSettings = new McpTaskStoreSettings(taskStateDirectory,
                TimeSpan.FromHours(configuration.Mcp.Tasks.TimeToLiveHours), configuration.Mcp.Tasks.PollIntervalMs);
            using var hostObserver = new McpHostLogObserver(log, locale, logDirectory, configuration.Logging.HttpRequests,
                configuration.Logging.MaxFileBytes, configuration.Logging.RetainedFiles);
            if (configuration.Mcp.Transport == "stdio")
            {
                string userPrompt = File.ReadAllText(ConfigurationLoader.Resolve(baseDirectory, configuration.Agent.UserSystemPrompt));
                string skillDirectory = ConfigurationLoader.Resolve(baseDirectory, configuration.Skills.Directory);
                var skills = SkillLoader.Load(skillDirectory);
                AgentRuntimeSettings runtimeSettings = CreateRuntimeSettings(configuration, userPrompt, language, skills, locale);
                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, taskStoreSettings, hostObserver, sessionStore: sessionStore).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);
                var skills = SkillLoader.Load(skillDirectory);
                AgentRuntimeSettings runtimeSettings = CreateRuntimeSettings(configuration, userPrompt, language, skills, locale);
                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,
                    taskStoreSettings, hostObserver, sessionStore: sessionStore).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, SessionStore? sessionStore = null)
    {
        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,
            configuration.Sandbox.Resources.ProcessTimeoutSeconds);
        var backend = new WindowsToolBackend(settings, audit, new TaskGraphToolHandler(configuration.Planner.MaxTasks, sessionStore));
        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));
        }
    }

    private static AgentRuntimeSettings CreateRuntimeSettings(AgentConfiguration configuration, string userPrompt, string language,
        IReadOnlyList<Agent.Core.Skills.AgentSkill> skills, LocaleCatalog locale) => new(
        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"),
        configuration.Ollama.Options.NumCtx ?? 32768,
        configuration.Limits.ContextSummaryRatio,
        MaxTasks: configuration.Planner.MaxTasks,
        Model: configuration.Ollama.Model);
}