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.
This commit is contained in:
@@ -81,7 +81,7 @@ The compose file maps them into the container.
|
||||
| `OPENROUTER_API_KEY` | — | Turns on group watching with topic triggers, classified by Jev through OpenRouter ([telegram-bot.md](telegram-bot.md#groups-and-triggers)) | No, and redacted |
|
||||
| `TRIGGER_MODEL` | `typesafe/jev-1.13` | The Jev model triggers use | Yes |
|
||||
| `GOOGLE_CLIENT_ID` | — | With the secret, turns on Google Calendar ([google-calendar.md](google-calendar.md)) | Yes |
|
||||
| `GOOGLE_CLIENT_SECRET` | — | See above | No, and redacted |
|
||||
| `GOOGLE_CLIENT_SECRET` | — | See above. Passed to the agent's processes (gmail/gdrive skills), its value redacted from tool output | Yes, and redacted |
|
||||
| `MCP_ENCRYPTION_SECRET` | — | Seals each chat's remote MCP keys; without it the bot token does ([mcp.md](mcp.md#remote-servers-per-chat)) | No, and redacted |
|
||||
| `A2A_ENABLED` | `false` | Serves other agents over A2A on port 8080 ([a2a.md](a2a.md), [telegram-bot.md#a2a](telegram-bot.md#a2a)); passed as `A2A__Enabled`. Needs a domain in Coolify (see below) | No |
|
||||
| `A2A_PUBLIC_URL` | — | External base URL for the agent card and push webhooks, e.g. `https://bot.example.com`; passed as `A2A__PublicUrl`. Required for push notifications to work | No |
|
||||
@@ -110,15 +110,17 @@ MCP servers:
|
||||
|
||||
- anything matching the default deny rules: `*_API_KEY`, `*_APIKEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` and their
|
||||
`Section__Key` forms (`*__APIKEY`, `*__*TOKEN`, `*__*SECRET`, `*__*PASSWORD`), plus `TELEGRAM_BOT_TOKEN`,
|
||||
`OPENAI_API_KEY`, `GOOGLE_CLIENT_SECRET`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN`;
|
||||
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN`;
|
||||
- any variable whose value is the bot token, the speech or OpenRouter API key or the Google client secret, whatever its name.
|
||||
|
||||
Those values are also replaced by `[REDACTED]` in tool output. The startup log names the rules, never values:
|
||||
`Process environment: 15 deny rules, allow: none`.
|
||||
The one exception is `GOOGLE_CLIENT_SECRET` itself: the bot always allows it, so skills such as gmail and gdrive can
|
||||
use the OAuth client. Its value is still replaced by `[REDACTED]` in tool output, like all the values above. The
|
||||
startup log names the rules, never values: `Process environment: 14 deny rules, allow: GOOGLE_CLIENT_SECRET`.
|
||||
|
||||
To give the agent a credential on purpose, add the variable in Coolify, add it to `environment:` in the compose file,
|
||||
and list its name in `PROCESS_ENV_ALLOW` (`PROCESS_ENV_ALLOW=GH_TOKEN` lets `gh` work). A name that is on the deny
|
||||
list by name, such as `TELEGRAM_BOT_TOKEN`, can't be allowed: trying stops startup.
|
||||
list by name, such as `TELEGRAM_BOT_TOKEN`, can't be allowed: trying stops startup. Listing `GOOGLE_CLIENT_SECRET`
|
||||
there is harmless.
|
||||
|
||||
### Other settings
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@ dotnet run --project hosts/Volcanic.AgenticCore.Telegram
|
||||
`/help`.
|
||||
|
||||
The client secret is added to the process policy's `SecretValues`, so it shows up as `[REDACTED]` in tool output.
|
||||
`GOOGLE_CLIENT_SECRET` is also on the default deny-list (`*_SECRET`), so `bash`, scheduled scripts and MCP servers
|
||||
never see it in their environment. See [telegram-bot.md](telegram-bot.md#security).
|
||||
The core's default deny-list (`*_SECRET`) would keep `GOOGLE_CLIENT_SECRET` out of `bash`, scheduled scripts and MCP
|
||||
servers, but the Telegram host allows it by default, so skills (gmail, gdrive) can read it from the environment. Its
|
||||
value is still redacted from tool output. See [telegram-bot.md](telegram-bot.md#security).
|
||||
|
||||
## Connecting a chat
|
||||
|
||||
|
||||
@@ -149,12 +149,17 @@ public sealed class ProcessEnvironmentOptions
|
||||
| `*_TOKEN`, `*__*TOKEN` | `TELEGRAM_BOT_TOKEN`, `GITHUB_TOKEN`, `Telegram__BotToken` |
|
||||
| `*_SECRET`, `*__*SECRET` | `GOOGLE_CLIENT_SECRET`, `GoogleCalendar__ClientSecret` |
|
||||
| `*_PASSWORD`, `*__*PASSWORD` | database and registry passwords |
|
||||
| `TELEGRAM_BOT_TOKEN`, `OPENAI_API_KEY`, `GOOGLE_CLIENT_SECRET` | named explicitly so that an `Allow` glob never re-admits them by accident (an explicit name in `Deny` beats a glob in `Allow`) |
|
||||
| `TELEGRAM_BOT_TOKEN`, `OPENAI_API_KEY` | named explicitly so that an `Allow` glob never re-admits them by accident (an explicit name in `Deny` beats a glob in `Allow`) |
|
||||
| `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` | the provider credentials (§7) |
|
||||
|
||||
The `__` forms exist because .NET configuration reads `Section__Key` variables, and the Docker spec tells operators to
|
||||
use them (`Telegram__Speech__ApiKey`, `GoogleCalendar__ClientSecret`).
|
||||
|
||||
Amendment (2026-10-12): `GOOGLE_CLIENT_SECRET` is no longer denied by name. The core still removes it through
|
||||
`*_SECRET`; the Telegram host adds it to `Allow` by default so workspace skills (gmail, gdrive) can use the Google OAuth
|
||||
client. Its value stays in `SecretValues`, so it is redacted from tool output, and an alias holding the same value is
|
||||
still removed.
|
||||
|
||||
Precedence: an explicit name in `Deny` removes; else a name or glob in `Allow` keeps; else a glob in `Deny` removes;
|
||||
else a `SecretValues` match removes; else kept. `Allow` of an explicitly denied name is a startup validation error, so
|
||||
the conflict is visible instead of silently lost.
|
||||
|
||||
@@ -61,7 +61,7 @@ settings use the `GoogleCalendar` section, the scheduler the `Scheduler` section
|
||||
| `Telegram:Triggers:MaxQueuedPerTrigger` | `5000` | Messages kept per snoozed trigger; past it the oldest are dropped |
|
||||
| `Telegram:InlineImageMaxBytes` | `3750000` | Image files up to this size are also shown to the model; must be positive |
|
||||
| `GOOGLE_CLIENT_ID` / `GoogleCalendar:ClientId` | — | Optional. With the secret, every chat gets the `calendar` tool and the `/calendar_*` commands; see [google-calendar.md](google-calendar.md) |
|
||||
| `GOOGLE_CLIENT_SECRET` / `GoogleCalendar:ClientSecret` | — | Optional; redacted from tool output and hidden from `bash` |
|
||||
| `GOOGLE_CLIENT_SECRET` / `GoogleCalendar:ClientSecret` | — | Optional; `GOOGLE_CLIENT_SECRET` is passed to `bash`, scheduled scripts and MCP servers (for skills), its value redacted from tool output |
|
||||
| `PROCESS_ENV_ALLOW` | — | Comma-separated names or globs let through to `bash`, scheduled scripts and MCP servers despite the deny-list (e.g. `GH_TOKEN` for `gh`). Added to `ProcessEnvironment:Allow` |
|
||||
| `ProcessEnvironment:Deny` / `ProcessEnvironment:Allow` | built-in deny-list | Added to the defaults (`*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, their `Section__Key` forms and the named provider credentials), never replacing them. Allowing a name that is denied by name stops startup |
|
||||
| `ProcessEnvironment:HardenHostProcess` | `true` | Linux: the bot process is made non-dumpable. `ProcessEnvironment__HardenHostProcess=false` for a debugging session only |
|
||||
@@ -401,8 +401,9 @@ token.
|
||||
Secrets are removed from the environment of `bash`, scheduled scripts and MCP servers: every variable on the deny-list
|
||||
(see `ProcessEnvironment:Deny` above) is dropped, and so is any variable whose value is the bot token, whatever its
|
||||
name (a Coolify alias, for example). `PROCESS_ENV_ALLOW` lets a named variable through, for example `GH_TOKEN` so the
|
||||
agent can use `gh`. At startup the bot logs which rules apply, names only:
|
||||
`Process environment: 15 deny rules, allow: GH_TOKEN`.
|
||||
agent can use `gh`. `GOOGLE_CLIENT_SECRET` is always allowed, so skills such as gmail and gdrive can use the Google
|
||||
OAuth client; its value stays among the redacted secrets. At startup the bot logs which rules apply, names only:
|
||||
`Process environment: 14 deny rules, allow: GH_TOKEN, GOOGLE_CLIENT_SECRET`.
|
||||
|
||||
On Linux the bot's own environment can't be read through `/proc/<pid>/environ` or ptrace: at startup the process
|
||||
makes itself non-dumpable (`prctl(PR_SET_DUMPABLE, 0)`). Set `DOTNET_EnableDiagnostics=0` in the bot's environment
|
||||
|
||||
@@ -8,7 +8,10 @@ public static class TelegramProcessEnvironment
|
||||
{
|
||||
public const string AllowVariable = "PROCESS_ENV_ALLOW";
|
||||
|
||||
/// <summary>Adds the section's lists, <c>PROCESS_ENV_ALLOW</c>, the bot token, the speech and OpenRouter API keys and the Google client secret to <paramref name="target"/>.</summary>
|
||||
/// <summary>Let through by default: skills (gmail, gdrive) need it. Its value stays in the secret values, so tool output shows <c>[REDACTED]</c>.</summary>
|
||||
public const string GoogleClientSecretVariable = "GOOGLE_CLIENT_SECRET";
|
||||
|
||||
/// <summary>Adds the section's lists, <c>PROCESS_ENV_ALLOW</c>, <c>GOOGLE_CLIENT_SECRET</c> to the allow-list, and the bot token, the speech and OpenRouter API keys and the Google client secret to the secret values of <paramref name="target"/>.</summary>
|
||||
public static void Configure(ProcessEnvironmentOptions target, TelegramOptions telegram, ProcessEnvironmentSection section, Func<string, string?> getEnv,
|
||||
GoogleCalendarOptions? calendar = null)
|
||||
{
|
||||
@@ -20,6 +23,7 @@ public static class TelegramProcessEnvironment
|
||||
target.Allow.AddRange(section.Allow);
|
||||
if (getEnv(AllowVariable) is { Length: > 0 } allow)
|
||||
target.Allow.AddRange(allow.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||
if (!target.Allow.Contains(GoogleClientSecretVariable, StringComparer.OrdinalIgnoreCase)) target.Allow.Add(GoogleClientSecretVariable);
|
||||
target.HardenHostProcess = section.HardenHostProcess;
|
||||
if (!string.IsNullOrWhiteSpace(telegram.BotToken)) target.SecretValues.Add(telegram.BotToken);
|
||||
if (!string.IsNullOrWhiteSpace(telegram.Speech?.ApiKey)) target.SecretValues.Add(telegram.Speech.ApiKey);
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class ProcessEnvironmentOptions
|
||||
"*_TOKEN", "*__*TOKEN",
|
||||
"*_SECRET", "*__*SECRET",
|
||||
"*_PASSWORD", "*__*PASSWORD",
|
||||
"TELEGRAM_BOT_TOKEN", "OPENAI_API_KEY", "GOOGLE_CLIENT_SECRET",
|
||||
"TELEGRAM_BOT_TOKEN", "OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN",
|
||||
];
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -15,6 +16,7 @@ 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()
|
||||
@@ -27,11 +29,32 @@ public sealed class ProcessEnvironmentTests
|
||||
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"], target.Allow);
|
||||
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()
|
||||
{
|
||||
@@ -70,7 +93,8 @@ public sealed class ProcessEnvironmentTests
|
||||
{
|
||||
h.Options.BotToken = Token;
|
||||
services.Configure<AgenticCoreOptions>(o =>
|
||||
TelegramProcessEnvironment.Configure(o.ProcessEnvironment, h.Options, new ProcessEnvironmentSection { HardenHostProcess = false }, _ => null));
|
||||
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));
|
||||
@@ -115,4 +139,34 @@ public sealed class ProcessEnvironmentTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,17 @@ public class ProcessSpawnPolicyTests
|
||||
Assert.Throws<InvalidOperationException>(() => new ProcessSpawnPolicy(options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Allowed_GOOGLE_CLIENT_SECRET_is_valid_and_kept_even_when_its_value_is_a_secret_value()
|
||||
{
|
||||
var options = TestProcesses.Options(o => { o.Allow.Add("GOOGLE_CLIENT_SECRET"); o.SecretValues.Add("GOCSPX-google-secret"); });
|
||||
Assert.Empty(options.Validate());
|
||||
var env = Prepare(new() { ["GOOGLE_CLIENT_SECRET"] = "GOCSPX-google-secret", ["ALIAS"] = "GOCSPX-google-secret" },
|
||||
o => { o.Allow.Add("GOOGLE_CLIENT_SECRET"); o.SecretValues.Add("GOCSPX-google-secret"); });
|
||||
Assert.Equal("GOCSPX-google-secret", env["GOOGLE_CLIENT_SECRET"]);
|
||||
Assert.False(env.ContainsKey("ALIAS"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Secret_value_under_an_innocent_name_is_removed()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user