-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAppConfig.cs
More file actions
143 lines (133 loc) · 8.11 KB
/
Copy pathAppConfig.cs
File metadata and controls
143 lines (133 loc) · 8.11 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
// ═══════════════════════════════════════════════════════════════════════
// AppConfig — single persistent-config directory rule
//
// Every file the user edits or the app persists that must SURVIVE updates lives
// under <app folder>\PersistentData\ — never next to the assemblies. Updates
// replace the app folder, so anything loose next to the exe is either
// regenerated by the SDK (agent.*.json) or would be overwritten/shipped by
// mistake. Per-user state that must not follow a portable copy stays in the OS
// app-data folder. See docs-dev/ARCHITECTURE.md "Where files live (storage
// tiers)" and docs-dev/RELEASE-CHECKLIST.md.
// ═══════════════════════════════════════════════════════════════════════
using System.Reflection;
/// <summary>Central definition of the persistent-config layout plus the one-time migration
/// from the legacy "next to the executable" locations, the default-appsettings seed and the
/// DEBUG tripwire that refuses to start when config json sits next to the assemblies.</summary>
public static class AppConfig
{
/// <summary>User-editable configuration folder: inside the app folder but never part of
/// any update (it is not in the release archive and the updater never touches it).</summary>
public static string PersistentDir => Path.Combine(AppContext.BaseDirectory, "PersistentData");
/// <summary>Server/user configuration file (persistent, user-editable).</summary>
public static string AppSettingsFile => Path.Combine(PersistentDir, "appsettings.json");
/// <summary>LLM provider definitions file (persistent, user-editable).</summary>
public static string ProvidersFile => Path.Combine(PersistentDir, "providers.json");
/// <summary>Telegram chat-medium config file (persistent, user-editable).</summary>
public static string TelegramFile => Path.Combine(PersistentDir, "telegram.json");
/// <summary>Per-tool policy file (persistent, user-editable).</summary>
public static string ToolsFile => Path.Combine(PersistentDir, "tools.json");
/// <summary>TUI agent tool-set file — the last /tools selection (custom combination or
/// agent-set preset), restored at startup so the enabled tools survive a restart.</summary>
public static string ToolsetFile => Path.Combine(PersistentDir, "toolset.json");
/// <summary>Telegram session file (auth keys, written on the first login).</summary>
public static string TelegramSessionFile => Path.Combine(PersistentDir, "telegram.session");
/// <summary>SDK-generated json that legitimately sits next to the executable — runtime
/// payload, not configuration (never flagged by <see cref="GuardNoStrayRootJson"/>).</summary>
private static string[] GeneratedRootJson()
{
var name = Assembly.GetExecutingAssembly().GetName().Name ?? "agent";
return [name + ".deps.json", name + ".runtimeconfig.json", name + ".staticwebassets.endpoints.json"];
}
/// <summary>Runs once at startup, before anything reads configuration:
/// anchors the process CWD to the executable folder, migrates legacy root config files
/// into <see cref="PersistentDir"/> and seeds the default appsettings.json there.</summary>
public static void Initialize()
{
// The process must behave identically no matter which directory the user launched it
// from: anchor the CWD to the executable folder so relative paths (ours or a spawned
// child's) resolve against the install dir, never against the caller's terminal.
try { Directory.SetCurrentDirectory(AppContext.BaseDirectory); } catch { }
try { Directory.CreateDirectory(PersistentDir); } catch { }
MigrateLegacy(AppSettingsFile, "appsettings.json");
MigrateLegacy(ProvidersFile, "providers.json");
MigrateLegacy(TelegramFile, "telegram.json");
MigrateLegacy(TelegramSessionFile, "telegram.session");
MigrateLegacy(ToolsFile, "tools.json");
// Seed the default appsettings.json (embedded copy of the repo file) so the ASP.NET
// config chain and the file-writing components (SipBridge, TUI) always find it. A
// user-edited file already in place is never overwritten.
if (!File.Exists(AppSettingsFile))
{
try
{
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("AgentBridge.appsettings.default.json");
if (stream != null)
{
using var file = File.Create(AppSettingsFile);
stream.CopyTo(file);
Console.WriteLine($"AppConfig: seeded default appsettings.json in {PersistentDir}");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"AppConfig: cannot seed default appsettings.json: {ex.Message}");
}
}
}
/// <summary>Moves one legacy file from the executable folder into PersistentData — only
/// when the new location has no file yet, so a newer user-edited copy is never clobbered.</summary>
private static void MigrateLegacy(string target, string fileName)
{
try
{
var source = Path.Combine(AppContext.BaseDirectory, fileName);
if (!File.Exists(source) || File.Exists(target)) return;
Directory.CreateDirectory(PersistentDir);
File.Move(source, target);
Console.WriteLine($"AppConfig: migrated {source} → {target} (persistent config now lives in PersistentData)");
}
catch (Exception ex)
{
Console.Error.WriteLine($"AppConfig: cannot migrate legacy '{fileName}': {ex.Message}");
}
}
/// <summary>DEBUG tripwire: refuses to start when a config/state *.json sits next to the
/// executable — the layout rule the release checklist enforces. Runs after
/// <see cref="Initialize"/> (known files were already migrated), so anything still there
/// is a mistake the programmer must see before it ships.</summary>
public static void GuardNoStrayRootJson()
{
#if DEBUG
string[] stray;
try
{
var allowed = GeneratedRootJson();
stray = Directory.EnumerateFiles(AppContext.BaseDirectory, "*.json", SearchOption.TopDirectoryOnly)
.Select(Path.GetFileName)
.Where(name => name != null && !allowed.Contains(name, StringComparer.OrdinalIgnoreCase))
.Select(name => name!)
.ToArray();
}
catch (Exception ex)
{
Console.Error.WriteLine($"AppConfig: cannot scan {AppContext.BaseDirectory} for stray json: {ex.Message}");
return;
}
if (stray.Length == 0) return;
Console.Error.WriteLine();
Console.Error.WriteLine("AgentBridge (DEBUG) refuses to start: json file(s) found next to the executable.");
Console.Error.WriteLine($" Directory: {AppContext.BaseDirectory}");
foreach (var name in stray) Console.Error.WriteLine($" - {name}");
Console.Error.WriteLine();
Console.Error.WriteLine("Persistent/user configuration must live under PersistentData\\ (or the OS app-data folder),");
Console.Error.WriteLine("never next to the assemblies: the update replaces the app folder, so such files would be");
Console.Error.WriteLine("overwritten or shipped by mistake — the single-config-directory rule exists to avoid that.");
Console.Error.WriteLine();
Console.Error.WriteLine("How to fix: move these files under PersistentData\\ (config there is read automatically after");
Console.Error.WriteLine("the startup migration) or delete them if they are build/test leftovers. Then start again.");
Console.Error.WriteLine("Before any push/release, run the developer checklist: docs-dev/RELEASE-CHECKLIST.md");
Environment.Exit(1);
#endif
}
}