Skip to content

Commit 8f66517

Browse files
feat(config): Add TOML configuration system with env overrides (Task 1.4, TDD)
1 parent bdd18c2 commit 8f66517

12 files changed

Lines changed: 534 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Channel configurations for messaging platforms.</summary>
4+
public class ChannelsConfig
5+
{
6+
public TelegramChannelConfig? Telegram { get; set; }
7+
public DiscordChannelConfig? Discord { get; set; }
8+
public SlackChannelConfig? Slack { get; set; }
9+
}
10+
11+
public class TelegramChannelConfig
12+
{
13+
public string? BotToken { get; set; }
14+
public List<string> AllowedUsers { get; set; } = [];
15+
public bool UseWebhook { get; set; }
16+
}
17+
18+
public class DiscordChannelConfig
19+
{
20+
public string? BotToken { get; set; }
21+
public List<string> AllowedGuilds { get; set; } = [];
22+
}
23+
24+
public class SlackChannelConfig
25+
{
26+
public string? BotToken { get; set; }
27+
public string? AppToken { get; set; }
28+
public string? SigningSecret { get; set; }
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Root configuration for ClawSharp.</summary>
4+
public class ClawSharpConfig
5+
{
6+
public string WorkspaceDir { get; set; } = Path.Combine(
7+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".clawsharp", "workspace");
8+
9+
public string DataDir { get; set; } = Path.Combine(
10+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".clawsharp");
11+
12+
public string? DefaultProvider { get; set; }
13+
public string? DefaultModel { get; set; }
14+
public double DefaultTemperature { get; set; } = 0.7;
15+
public int MaxContextTokens { get; set; } = 128_000;
16+
17+
public ProvidersConfig Providers { get; set; } = new();
18+
public MemoryConfig Memory { get; set; } = new();
19+
public GatewayConfig Gateway { get; set; } = new();
20+
public ChannelsConfig Channels { get; set; } = new();
21+
public SecurityConfig Security { get; set; } = new();
22+
public HeartbeatConfig Heartbeat { get; set; } = new();
23+
public TunnelConfig Tunnel { get; set; } = new();
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>HTTP gateway settings.</summary>
4+
public class GatewayConfig
5+
{
6+
public string Host { get; set; } = "127.0.0.1";
7+
public int Port { get; set; } = 8080;
8+
public bool EnableUi { get; set; } = true;
9+
public string? ApiKey { get; set; }
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Heartbeat polling settings.</summary>
4+
public class HeartbeatConfig
5+
{
6+
public bool Enabled { get; set; } = true;
7+
public int IntervalSeconds { get; set; } = 1800;
8+
public string Prompt { get; set; } = "Read HEARTBEAT.md if it exists. If nothing needs attention, reply HEARTBEAT_OK.";
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Memory store settings.</summary>
4+
public class MemoryConfig
5+
{
6+
public string DbPath { get; set; } = "memory.db";
7+
public bool EnableVectorSearch { get; set; } = true;
8+
public string? EmbeddingProvider { get; set; }
9+
public string? EmbeddingModel { get; set; }
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>LLM provider configurations.</summary>
4+
public class ProvidersConfig
5+
{
6+
public ProviderEntry? Openai { get; set; }
7+
public ProviderEntry? Anthropic { get; set; }
8+
public ProviderEntry? OpenRouter { get; set; }
9+
public ProviderEntry? Ollama { get; set; }
10+
public List<ProviderEntry> Compatible { get; set; } = [];
11+
}
12+
13+
/// <summary>A single provider's connection settings.</summary>
14+
public class ProviderEntry
15+
{
16+
public string? ApiKey { get; set; }
17+
public string? BaseUrl { get; set; }
18+
public string? DefaultModel { get; set; }
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Security and sandboxing settings.</summary>
4+
public class SecurityConfig
5+
{
6+
public bool SandboxEnabled { get; set; } = true;
7+
public List<string> AllowedCommands { get; set; } = ["ls", "cat", "grep", "find", "echo", "date"];
8+
public List<string> AllowedPaths { get; set; } = [];
9+
public string? PairingSecret { get; set; }
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ClawSharp.Core.Config;
2+
3+
/// <summary>Tunnel settings for external access.</summary>
4+
public class TunnelConfig
5+
{
6+
public string? Provider { get; set; }
7+
public string? Token { get; set; }
8+
public string? Domain { get; set; }
9+
}

src/ClawSharp.Infrastructure/ClawSharp.Infrastructure.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<ProjectReference Include="..\ClawSharp.Core\ClawSharp.Core.csproj" />
55
</ItemGroup>
66

7+
<ItemGroup>
8+
<PackageReference Include="Tomlyn" Version="0.20.0" />
9+
</ItemGroup>
10+
711
<PropertyGroup>
812
<ImplicitUsings>enable</ImplicitUsings>
913
<Nullable>enable</Nullable>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace ClawSharp.Infrastructure.Config;
2+
3+
using ClawSharp.Core.Config;
4+
using Tomlyn;
5+
using Tomlyn.Model;
6+
7+
/// <summary>Loads ClawSharp configuration from TOML files with environment variable overrides.</summary>
8+
public static class TomlConfigLoader
9+
{
10+
/// <summary>
11+
/// Loads configuration from the specified path, CLAWSHARP_CONFIG_PATH env var, or the default location.
12+
/// Missing files return default config. Environment variables always take precedence.
13+
/// </summary>
14+
public static ClawSharpConfig Load(string? configPath = null)
15+
{
16+
configPath ??= Environment.GetEnvironmentVariable("CLAWSHARP_CONFIG_PATH")
17+
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".clawsharp", "config.toml");
18+
19+
ClawSharpConfig config;
20+
21+
if (!File.Exists(configPath))
22+
{
23+
config = new ClawSharpConfig();
24+
}
25+
else
26+
{
27+
var toml = File.ReadAllText(configPath);
28+
try
29+
{
30+
config = Toml.ToModel<ClawSharpConfig>(toml,
31+
options: new TomlModelOptions
32+
{
33+
ConvertPropertyName = name => ToSnakeCase(name),
34+
});
35+
}
36+
catch (TomlException ex)
37+
{
38+
throw new InvalidOperationException(
39+
$"Failed to parse config file '{configPath}': {ex.Message}", ex);
40+
}
41+
}
42+
43+
return ApplyEnvironmentOverrides(config);
44+
}
45+
46+
private static ClawSharpConfig ApplyEnvironmentOverrides(ClawSharpConfig config)
47+
{
48+
if (Environment.GetEnvironmentVariable("CLAWSHARP_DEFAULT_PROVIDER") is { } provider)
49+
config.DefaultProvider = provider;
50+
if (Environment.GetEnvironmentVariable("CLAWSHARP_DEFAULT_MODEL") is { } model)
51+
config.DefaultModel = model;
52+
if (Environment.GetEnvironmentVariable("OPENAI_API_KEY") is { } oaiKey)
53+
(config.Providers.Openai ??= new ProviderEntry()).ApiKey = oaiKey;
54+
if (Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") is { } antKey)
55+
(config.Providers.Anthropic ??= new ProviderEntry()).ApiKey = antKey;
56+
57+
return config;
58+
}
59+
60+
private static string ToSnakeCase(string name)
61+
{
62+
var result = new System.Text.StringBuilder();
63+
for (var i = 0; i < name.Length; i++)
64+
{
65+
var c = name[i];
66+
if (char.IsUpper(c))
67+
{
68+
if (i > 0) result.Append('_');
69+
result.Append(char.ToLowerInvariant(c));
70+
}
71+
else
72+
{
73+
result.Append(c);
74+
}
75+
}
76+
return result.ToString();
77+
}
78+
}

0 commit comments

Comments
 (0)