Files
VolcanicAgenticCore/tests/Volcanic.AgenticCore.Telegram.Tests/ProcessEnvironmentTests.cs
EugeneTes 0cae6f600a feat(telegram): pass GOOGLE_CLIENT_SECRET to agent processes, still redacted
GOOGLE_CLIENT_SECRET is no longer denied by name in the core defaults (the
*_SECRET glob still removes it); the Telegram host allows it by default so
workspace skills (gmail, gdrive) can use the Google OAuth client. Its value
stays in SecretValues: redacted from tool output, and aliases with the same
value are still removed.
2026-10-12 15:10:49 +00:00

173 lines
8.4 KiB
C#

using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volcanic.AgenticCore.Abstractions.Json;
using Volcanic.AgenticCore.Abstractions.Messages;
using Volcanic.AgenticCore.Abstractions.Processes;
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
using Volcanic.AgenticCore.Features.Processes;
using Volcanic.AgenticCore.Hosting;
using Volcanic.AgenticCore.Telegram.Tests.Support;
using Volcanic.AgenticCore.Testing;
namespace Volcanic.AgenticCore.Telegram.Tests;
public sealed class ProcessEnvironmentTests
{
private const string Token = "123456789:AAH-test-bot-token-value";
private const string GoogleSecret = "GOCSPX-test-google-client-secret";
[Fact]
public void Configure_adds_the_bot_token_section_lists_and_PROCESS_ENV_ALLOW()
{
var target = new ProcessEnvironmentOptions();
var section = new ProcessEnvironmentSection { Deny = ["EXTRA_*"], Allow = ["NPM_TOKEN"], HardenHostProcess = false };
TelegramProcessEnvironment.Configure(target, new TelegramOptions { BotToken = Token }, section,
name => name == "PROCESS_ENV_ALLOW" ? "GH_TOKEN, HF_TOKEN" : null);
Assert.Contains(Token, target.SecretValues);
Assert.Equal(ProcessEnvironmentOptions.DefaultDeny.Count + 1, target.Deny.Count);
Assert.Contains("EXTRA_*", target.Deny);
Assert.Equal(["NPM_TOKEN", "GH_TOKEN", "HF_TOKEN", "GOOGLE_CLIENT_SECRET"], target.Allow);
Assert.False(target.HardenHostProcess);
Assert.Empty(target.Validate());
}
[Fact]
public void GOOGLE_CLIENT_SECRET_in_PROCESS_ENV_ALLOW_is_valid_and_not_duplicated()
{
var target = new ProcessEnvironmentOptions();
TelegramProcessEnvironment.Configure(target, new TelegramOptions { BotToken = Token }, new ProcessEnvironmentSection(),
name => name == "PROCESS_ENV_ALLOW" ? "google_client_secret" : null);
Assert.Equal(["google_client_secret"], target.Allow);
Assert.Empty(target.Validate());
_ = new ProcessSpawnPolicy(target, null, () => new Dictionary<string, string?>());
}
[Fact]
public void Configure_adds_the_google_client_secret_to_the_secret_values()
{
var target = new ProcessEnvironmentOptions();
TelegramProcessEnvironment.Configure(target, new TelegramOptions { BotToken = Token }, new ProcessEnvironmentSection(), _ => null,
new GoogleCalendarOptions { ClientSecret = GoogleSecret });
Assert.Contains(GoogleSecret, target.SecretValues);
}
[Fact]
public void Configure_adds_the_speech_api_key_to_the_secret_values()
{
var target = new ProcessEnvironmentOptions();
var telegram = new TelegramOptions { BotToken = Token };
telegram.Speech.ApiKey = "sk-speech-secret";
TelegramProcessEnvironment.Configure(target, telegram, new ProcessEnvironmentSection(), _ => null);
Assert.Contains("sk-speech-secret", target.SecretValues);
}
[Fact]
public void The_section_binds_from_configuration()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["ProcessEnvironment:Allow:0"] = "GH_TOKEN",
["ProcessEnvironment:HardenHostProcess"] = "false",
}).Build();
var section = config.GetSection(ProcessEnvironmentSection.SectionName).Get<ProcessEnvironmentSection>()!;
Assert.Equal(["GH_TOKEN"], section.Allow);
Assert.False(section.HardenHostProcess);
}
[Fact]
public void Describe_lists_the_rule_count_and_allowed_names_only()
{
var options = new ProcessEnvironmentOptions();
options.Allow.Add("GH_TOKEN");
options.SecretValues.Add(Token);
var line = TelegramProcessEnvironment.Describe(options);
Assert.Equal($"Process environment: {ProcessEnvironmentOptions.DefaultDeny.Count} deny rules, allow: GH_TOKEN", line);
Assert.Equal($"Process environment: {ProcessEnvironmentOptions.DefaultDeny.Count} deny rules, allow: none", TelegramProcessEnvironment.Describe(new ProcessEnvironmentOptions()));
}
private static ChatHost Host(IReadOnlyDictionary<string, string?>? environment = null) => new((h, services) =>
{
h.Options.BotToken = Token;
services.Configure<AgenticCoreOptions>(o =>
TelegramProcessEnvironment.Configure(o.ProcessEnvironment, h.Options, new ProcessEnvironmentSection { HardenHostProcess = false }, _ => null,
new GoogleCalendarOptions { ClientSecret = GoogleSecret }));
if (environment is not null)
services.AddSingleton<IProcessSpawnPolicy>(sp =>
new ProcessSpawnPolicy(sp.GetRequiredService<IOptions<AgenticCoreOptions>>().Value.ProcessEnvironment, null, () => environment));
});
private static string LastToolResult(ChatHost host) =>
JsonSerializer.Serialize(host.Faux.Requests[^1].Messages.OfType<ToolResultMessage>().Last(), VolcanicJson.Options);
[Fact]
public async Task Bash_printing_the_bot_token_from_a_file_shows_REDACTED_to_the_model()
{
await using var host = Host();
var chat = await host.ChatAsync();
await File.WriteAllTextAsync(Path.Combine(chat.Session.Cwd, "token.txt"), Token);
host.Faux.Enqueue(FauxTurn.Tool("bash", new { command = OperatingSystem.IsWindows() ? "type token.txt" : "cat token.txt" }), FauxTurn.Say("done"));
await host.SendAsync("print the file");
await host.SettleAsync();
var result = LastToolResult(host);
Assert.Contains("[REDACTED]", result);
Assert.DoesNotContain(Token, result);
}
[Fact]
public async Task Env_in_a_chat_does_not_show_the_bot_token_under_any_name()
{
var environment = new Dictionary<string, string?>(OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
foreach (var name in new[] { "PATH", "HOME", "SystemRoot", "ComSpec", "TEMP", "TMP" })
if (Environment.GetEnvironmentVariable(name) is { } value) environment[name] = value;
environment["TELEGRAM_BOT_TOKEN"] = Token;
environment["COOLIFY_BOT_ALIAS"] = Token;
await using var host = Host(environment);
host.Faux.Enqueue(FauxTurn.Tool("bash", new { command = OperatingSystem.IsWindows() ? "set" : "env" }), FauxTurn.Say("done"));
await host.SendAsync("show the environment");
await host.SettleAsync();
var result = LastToolResult(host);
Assert.Contains("PATH=", result, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("TELEGRAM_BOT_TOKEN", result);
Assert.DoesNotContain("COOLIFY_BOT_ALIAS", result);
Assert.DoesNotContain(Token, result);
}
[Fact]
public async Task Bash_sees_GOOGLE_CLIENT_SECRET_but_the_model_sees_REDACTED_and_other_secrets_stay_out()
{
var environment = new Dictionary<string, string?>(OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
foreach (var name in new[] { "PATH", "HOME", "SystemRoot", "ComSpec", "TEMP", "TMP" })
if (Environment.GetEnvironmentVariable(name) is { } value) environment[name] = value;
environment["GOOGLE_CLIENT_SECRET"] = GoogleSecret;
environment["GOOGLE_ALIAS_COPY"] = GoogleSecret;
environment["TELEGRAM_BOT_TOKEN"] = Token;
environment["OPENAI_API_KEY"] = "sk-openai-test-key-123";
environment["OTHER_SERVICE_SECRET"] = "other-service-secret-456";
await using var host = Host(environment);
var command = OperatingSystem.IsWindows()
? "if defined GOOGLE_CLIENT_SECRET (echo HAS_GOOGLE_SECRET) & set"
: "[ \"$GOOGLE_CLIENT_SECRET\" = '" + GoogleSecret + "' ] && echo HAS_GOOGLE_SECRET; env";
host.Faux.Enqueue(FauxTurn.Tool("bash", new { command }), FauxTurn.Say("done"));
await host.SendAsync("show the environment");
await host.SettleAsync();
var result = LastToolResult(host);
Assert.Contains("HAS_GOOGLE_SECRET", result);
Assert.Contains("GOOGLE_CLIENT_SECRET=[REDACTED]", result);
Assert.DoesNotContain(GoogleSecret, result);
Assert.DoesNotContain("GOOGLE_ALIAS_COPY", result);
Assert.DoesNotContain("TELEGRAM_BOT_TOKEN", result);
Assert.DoesNotContain("OPENAI_API_KEY", result);
Assert.DoesNotContain("OTHER_SERVICE_SECRET", result);
}
}