feat(google-calendar): encrypt chat tokens and give watchers a short-lived access token
token.json is sealed with AES-256-GCM (key derived from the OAuth client secret, bound to the chat), so the agent's processes can read the file but not the refresh token. Plain files, including VolcanicSecretary's refresh_token format, are rewritten sealed on first read. The scheduler gains IScriptEnvironment: extra per-run variables set after the process environment filter and redacted from the run's output. The Telegram host uses it to pass GOOGLE_ACCESS_TOKEN, refreshed to cover the job's timeout; the refresh token and client secret never reach the script.
This commit is contained in:
@@ -95,6 +95,8 @@ builder.Services.AddSingleton<IUpdateRouter, UpdateRouter>();
|
||||
builder.Services.AddSingleton<PollingService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<PollingService>());
|
||||
if (scheduling.Enabled) SchedulerSetup.Register(builder.Services); // the hosted service starts after PollingService
|
||||
// Watchers get a short-lived GOOGLE_ACCESS_TOKEN for their chat's calendar, never the client secret or refresh token.
|
||||
if (scheduling.Enabled && calendar is not null) builder.Services.AddSingleton<IScriptEnvironment, CalendarScriptEnvironment>();
|
||||
builder.Services.AddHostedService<BotSetupService>();
|
||||
|
||||
using var host = builder.Build();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Globalization;
|
||||
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
|
||||
using Volcanic.AgenticCore.Extensions.Scheduler;
|
||||
using Volcanic.AgenticCore.Telegram.Chats;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram.Scheduling;
|
||||
|
||||
/// <summary>
|
||||
/// Gives a chat's watcher scripts <c>GOOGLE_ACCESS_TOKEN</c>: an access token for that chat's calendar, valid for the
|
||||
/// run's timeout plus a minute (as far as Google's one-hour lifetime allows). The bot refreshes it; the refresh token
|
||||
/// and the client secret never reach the script. A chat without a connected calendar gets nothing.
|
||||
/// </summary>
|
||||
public sealed class CalendarScriptEnvironment(ChatRegistry chats, GoogleCalendarOptions calendar) : IScriptEnvironment
|
||||
{
|
||||
public const string AccessTokenVariable = "GOOGLE_ACCESS_TOKEN";
|
||||
private static readonly TimeSpan Margin = TimeSpan.FromMinutes(1);
|
||||
private static readonly IReadOnlyDictionary<string, string> None = new Dictionary<string, string>();
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, string>> ForRunAsync(string target, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!long.TryParse(target, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var chatId)) return None;
|
||||
var token = await (calendar.Connections ?? GoogleCalendarConnections.Shared)
|
||||
.TryGetAccessTokenAsync(calendar, ChatRegistry.ScopeFor(chatId), chats.WorkspaceFor(chatId), timeout + Margin, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return token is null ? None : new Dictionary<string, string> { [AccessTokenVariable] = token };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user