-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
312 lines (283 loc) · 16.2 KB
/
Copy pathProgram.cs
File metadata and controls
312 lines (283 loc) · 16.2 KB
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
using Daggeragent.Agent;
using Daggeragent.Configuration;
using Daggeragent.Llm;
using Daggeragent.Mcp;
using Daggeragent.Modes;
using Daggeragent.Persistence;
using Daggeragent.Server;
using Daggeragent.Tools;
using Daggeragent.Triggers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using Serilog;
namespace Daggeragent;
public static class Program
{
public static async Task<int> Main(string[] args)
{
// Ensure Unicode (†, dim ANSI, etc.) renders correctly on Windows consoles.
try { Console.OutputEncoding = System.Text.Encoding.UTF8; } catch { /* not a TTY */ }
// When running as a Windows Service the working directory is C:\Windows\System32,
// so resolve config and logs relative to the exe. Pin cwd up-front so any relative
// path in config (Serilog file sink, jobs.db, etc.) resolves under the exe directory.
// Capture the user's launch cwd first — filesystem/shell tools default to that
// rather than the exe directory.
//
// IMPORTANT for single-file publish: AppContext.BaseDirectory points to the
// self-extracted bundle temp dir (e.g. %LOCALAPPDATA%\.net\dagger\<hash>\), NOT
// to where dagger.exe physically lives — so an appsettings.json sitting next to
// the exe would never be read. Resolve from Environment.ProcessPath instead when
// it looks like a real exe (i.e. not `dotnet` hosting a dll).
string contentRoot;
var processPath = Environment.ProcessPath;
if (!string.IsNullOrEmpty(processPath) &&
!Path.GetFileName(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) &&
!Path.GetFileName(processPath).Equals("dotnet.exe", StringComparison.OrdinalIgnoreCase))
{
contentRoot = Path.GetDirectoryName(processPath)!;
}
else
{
contentRoot = AppContext.BaseDirectory;
}
var launchInfo = new Configuration.HostLaunchInfo
{
OriginalWorkingDirectory = Environment.CurrentDirectory,
ContentRoot = contentRoot,
};
Directory.SetCurrentDirectory(contentRoot);
var isWindowsService = WindowsServiceHelpers.IsWindowsService();
var mode = ModeDetector.Detect(args, isWindowsService);
// Console sink routing per mode:
// CLI — route everything to stderr so `dagger.exe "..." > out.txt` only captures the reply.
// Interactive — fully silenced (restricted to Fatal, and even those go to stderr) so log
// lines never interleave with the streaming chat output. The file sink keeps everything.
// Service / WindowsService — normal: Information+ on stdout for terminal/container log capture.
var cliMode = mode == AppMode.Cli;
var interactiveMode = mode == AppMode.Interactive;
var consoleRestrictedLevel = interactiveMode
? Serilog.Events.LogEventLevel.Fatal
: Serilog.Events.LogEventLevel.Verbose;
var consoleStderrFromLevel = (interactiveMode || cliMode)
? Serilog.Events.LogEventLevel.Verbose
: Serilog.Events.LogEventLevel.Error;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(restrictedToMinimumLevel: consoleRestrictedLevel, standardErrorFromLevel: consoleStderrFromLevel)
.WriteTo.File(
Path.Combine(contentRoot, "logs", "dagger-bootstrap-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
shared: true)
.CreateBootstrapLogger();
try
{
Log.Information("DaggerAgent starting (mode={Mode}, contentRoot={ContentRoot})", mode, contentRoot);
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ContentRootPath = contentRoot,
});
builder.Configuration
.SetBasePath(contentRoot)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddEnvironmentVariables(prefix: "DAGGER_")
.AddCommandLine(args);
if (isWindowsService)
{
builder.Host.UseWindowsService(o => o.ServiceName = "DaggerAgent");
}
// Same Console-sink routing applied to the appsettings-loaded host logger.
if (cliMode || interactiveMode)
{
foreach (var sink in builder.Configuration.GetSection("Serilog:WriteTo").GetChildren())
{
if (sink.GetValue<string>("Name") == "Console")
{
sink["Args:standardErrorFromLevel"] = "Verbose";
if (interactiveMode)
{
sink["Args:restrictedToMinimumLevel"] = "Fatal";
}
}
}
}
builder.Host.UseSerilog((ctx, services, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext());
builder.Services.AddSingleton(launchInfo);
RegisterServices(builder);
// Kestrel binding — Service / WindowsService modes only. Default is loopback-only
// (Server:Host = "localhost") so the agent API (tool-calling jobs, config CRUD, the
// OpenAI/Ollama-compatible endpoints) isn't reachable off-box without an explicit
// opt-in. "0.0.0.0"/"*" binds all interfaces; a specific IP binds that IP; anything
// unrecognised falls back to loopback.
var server = builder.Configuration.GetSection(ServerOptions.SectionName).Get<ServerOptions>() ?? new ServerOptions();
var auth = builder.Configuration.GetSection(AuthOptions.SectionName).Get<AuthOptions>() ?? new AuthOptions();
var bindHost = (server.Host ?? "").Trim();
var loopbackOnly = bindHost.Length == 0
|| bindHost.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|| bindHost == "127.0.0.1" || bindHost == "::1";
builder.WebHost.ConfigureKestrel(k =>
{
if (loopbackOnly)
k.ListenLocalhost(server.Port);
else if (bindHost is "0.0.0.0" or "*" or "+" or "::")
k.ListenAnyIP(server.Port);
else if (System.Net.IPAddress.TryParse(bindHost, out var ip))
k.Listen(ip, server.Port);
else
{
Log.Warning("Server:Host '{Host}' is not a recognised IP or 'localhost' — binding loopback only.", bindHost);
k.ListenLocalhost(server.Port);
}
});
var app = builder.Build();
// Apply persisted runtime overrides (endpoints, MCP servers) before anything reads
// those options. MCP host starts later via IHostedService so picks up the new server
// list on first connect; ChatClientFactory reads endpoints per-call so no warm-up needed.
await app.Services.GetRequiredService<RuntimeConfigStore>().LoadAsync().ConfigureAwait(false);
if (mode is AppMode.Service or AppMode.WindowsService)
{
app.UseServiceTrafficLogging();
app.UseSerilogRequestLogging();
app.UseApiKeyAuth();
app.MapLanding();
app.MapJobsApi(server.Path);
app.MapJobsStream(server.Path);
app.MapAgentUi(server.Path);
app.MapOpenAiCompatApi();
app.MapOllamaCompatApi();
var store = app.Services.GetRequiredService<IJobStore>();
await store.InitializeAsync().ConfigureAwait(false);
await app.Services.GetRequiredService<MemoryStore>().InitializeAsync().ConfigureAwait(false);
// Reconcile rows orphaned by the previous run's ungraceful shutdown: anything
// marked Running has no in-process driver, so flip them to Paused + Interrupted
// so the UI shows accurate status and trigger auto-resume can pick them up.
try
{
var swept = await store.SweepOrphansAsync().ConfigureAwait(false);
if (swept.Count > 0)
Log.Information("Orphan sweep: marked {Count} job(s) as Paused+Interrupted", swept.Count);
}
catch (Exception ex) { Log.Warning(ex, "Orphan sweep failed"); }
if (!loopbackOnly && auth.ApiKeys.Count == 0)
Log.Warning(
"SECURITY: DaggerAgent is bound to {Host} (network-reachable) with NO Auth:ApiKeys configured — " +
"the agent API (tool-calling jobs, config, OpenAI-compatible endpoints) is UNAUTHENTICATED. " +
"Set Auth:ApiKeys (e.g. DAGGER_Auth__ApiKeys__0=<key>) before exposing it on a network.",
bindHost);
Log.Information("DaggerAgent service listening on http://{Host}:{Port}{Path}", server.Host, server.Port, server.Path);
await app.RunAsync().ConfigureAwait(false);
return 0;
}
// Interactive / CLI: start MCP host so tools are available, but skip app.RunAsync().
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
var mcpHost = app.Services.GetRequiredService<McpClientHost>();
// Lifetime tracing: in Interactive mode the console sink is silenced, so a
// shutdown-signal that aborts the chat loop would otherwise be invisible.
// Capture every transition to the file sink so we can diagnose "it just stopped".
lifetime.ApplicationStarted.Register(() => Log.Information("Lifetime: ApplicationStarted"));
lifetime.ApplicationStopping.Register(() => Log.Information("Lifetime: ApplicationStopping signalled (something requested shutdown)"));
lifetime.ApplicationStopped.Register(() => Log.Information("Lifetime: ApplicationStopped"));
Console.CancelKeyPress += (_, e) =>
{
Log.Information("Console.CancelKeyPress: Ctrl+{Key} pressed — requesting graceful shutdown", e.SpecialKey);
e.Cancel = true; // suppress immediate process kill — let the cancellation token unwind cleanly
lifetime.StopApplication();
};
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
Log.Fatal(e.ExceptionObject as Exception, "AppDomain.UnhandledException (isTerminating={Terminating})", e.IsTerminating);
TaskScheduler.UnobservedTaskException += (_, e) =>
{
Log.Error(e.Exception, "TaskScheduler.UnobservedTaskException");
e.SetObserved();
};
await mcpHost.StartAsync(lifetime.ApplicationStopping).ConfigureAwait(false);
try
{
var exitCode = mode switch
{
AppMode.Interactive => await app.Services.GetRequiredService<InteractiveRunner>().RunAsync(lifetime.ApplicationStopping).ConfigureAwait(false),
AppMode.Cli => await app.Services.GetRequiredService<CliRunner>().RunAsync(args, lifetime.ApplicationStopping).ConfigureAwait(false),
_ => 1,
};
Log.Information("Runner returned (mode={Mode}, exitCode={ExitCode})", mode, exitCode);
return exitCode;
}
finally
{
Log.Information("Shutting down: stopping MCP host");
await mcpHost.StopAsync(CancellationToken.None).ConfigureAwait(false);
await mcpHost.DisposeAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Fatal(ex, "DaggerAgent terminated unexpectedly");
return 1;
}
finally
{
await Log.CloseAndFlushAsync().ConfigureAwait(false);
}
}
private static void RegisterServices(WebApplicationBuilder builder)
{
builder.Services.Configure<AgentOptions>(builder.Configuration.GetSection(AgentOptions.SectionName));
builder.Services.Configure<OpenAIOptions>(builder.Configuration.GetSection(OpenAIOptions.SectionName));
builder.Services.Configure<EndpointsOptions>(builder.Configuration.GetSection(EndpointsOptions.SectionName));
builder.Services.Configure<McpOptions>(builder.Configuration.GetSection(McpOptions.SectionName));
builder.Services.Configure<ServerOptions>(builder.Configuration.GetSection(ServerOptions.SectionName));
builder.Services.Configure<JobsOptions>(builder.Configuration.GetSection(JobsOptions.SectionName));
builder.Services.Configure<ToolsOptions>(builder.Configuration.GetSection(ToolsOptions.SectionName));
builder.Services.Configure<WebOptions>(builder.Configuration.GetSection(WebOptions.SectionName));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection(AuthOptions.SectionName));
builder.Services.Configure<PricingOptions>(builder.Configuration.GetSection(PricingOptions.SectionName));
builder.Services.Configure<MemoryOptions>(builder.Configuration.GetSection(MemoryOptions.SectionName));
builder.Services.Configure<TriggerOptions>(builder.Configuration.GetSection(TriggerOptions.SectionName));
builder.Services.AddSingleton<ChatClientFactory>();
builder.Services.AddSingleton<EmbeddingClientFactory>();
builder.Services.AddSingleton<MemoryStore>();
builder.Services.AddSingleton<TokenEstimator>();
builder.Services.AddSingleton<PersonalityProvider>();
builder.Services.AddSingleton<ContextCompressor>();
builder.Services.AddSingleton<IJobStore, SqliteJobStore>();
builder.Services.AddSingleton<McpClientHost>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<McpClientHost>());
builder.Services.AddSingleton<McpToolProvider>();
builder.Services.AddSingleton<SubAgentManager>();
builder.Services.AddSingleton<SpawnSubagentTool>();
builder.Services.AddSingleton<PendingWriteStore>();
builder.Services.AddSingleton<FilesystemTools>();
builder.Services.AddSingleton<ShellToolset>();
builder.Services.AddSingleton<MemoryTools>();
builder.Services.AddSingleton<SystemTools>();
builder.Services.AddSingleton<WebTools>();
builder.Services.AddSingleton<PlanStore>();
builder.Services.AddSingleton<PlanningTools>();
builder.Services.AddSingleton<ToolResultStore>();
builder.Services.AddSingleton<ToolResultTools>();
builder.Services.AddSingleton<CliSessionStore>();
builder.Services.AddSingleton<CliDelegationTools>();
builder.Services.AddSingleton<RuntimeConfigStore>();
builder.Services.AddSingleton<BuiltInToolRegistry>();
builder.Services.AddTransient<LlmAgent>();
builder.Services.AddSingleton<InteractiveRunner>();
builder.Services.AddSingleton<CliRunner>();
builder.Services.AddSingleton<TriggerStateStore>();
// Register as a singleton AND a hosted service via the same instance — the singleton
// registration is what lets endpoint handlers inject TriggerService to fire a manual
// /triggers/sources/{id}/run, while the hosted-service shim keeps the background loop.
builder.Services.AddSingleton<TriggerService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TriggerService>());
}
}