docs(google-calendar): setup guide, Telegram configuration and architecture notes
Spec: the unverified-app screen is described in text; open question 1 resolved (full calendar scope); status implemented.
This commit is contained in:
@@ -78,8 +78,11 @@ Read `docs/architecture.md` first. Key points:
|
||||
|
||||
### Extensions (`extensions/`)
|
||||
|
||||
Provider extensions built on the public API only: `Anthropic` (Messages API), `OpenAI` (chat-completions compatible),
|
||||
and `ClaudeCode` + `ClaudeCode.Bridge`. The Claude Code provider is the most intricate piece (`docs/claude-code-provider.md`):
|
||||
Extensions built on the public API only: providers `Anthropic` (Messages API), `OpenAI` (chat-completions compatible),
|
||||
`ClaudeCode` + `ClaudeCode.Bridge`, and the `GoogleCalendar` feature extension (`calendar` tool,
|
||||
`/calendar_connect|status|disconnect`, one Google account per chat keyed by session scope, tokens under
|
||||
`<agentDir>/google-calendar/`; `docs/google-calendar.md`). The Claude Code provider is the most intricate piece
|
||||
(`docs/claude-code-provider.md`):
|
||||
|
||||
- Every model call spawns a fresh `claude -p` with built-in tools off and replays the whole transcript as stream-json.
|
||||
Core tools are exposed as MCP tools `mcp__volcanic__<alias>` through the Bridge, a tiny MCP server that **never
|
||||
|
||||
@@ -17,6 +17,7 @@ Claude Code as a model provider without an API key.
|
||||
| Anthropic Messages API provider | `extensions/Volcanic.AgenticCore.Extensions.Anthropic` | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI-compatible provider | `extensions/Volcanic.AgenticCore.Extensions.OpenAI` | OpenAI, OpenRouter, Groq, Ollama, llama.cpp, vLLM |
|
||||
| Claude Code (subscription) provider | `extensions/Volcanic.AgenticCore.Extensions.ClaudeCode` (+ `.Bridge`) | Spawns `claude -p`, MCP "proposal" bridge |
|
||||
| Google Calendar (tool + connect commands) | `extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar` | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`; one account per chat |
|
||||
| Telegram bot | `hosts/Volcanic.AgenticCore.Telegram` | Chat with the agent from Telegram; long polling, Claude Code, allow-list |
|
||||
| Reference headless host | `samples/Volcanic.AgenticCore.Sample.Headless` | Prompt in, JSON events out |
|
||||
| Tests | `tests/` | xunit; no test calls a paid API |
|
||||
@@ -67,6 +68,7 @@ await session.PromptAsync("Explain this repository");
|
||||
- [Writing extensions](docs/extensions.md): tools, commands, lifecycle hooks, providers, message injection, discovery.
|
||||
- [Providers and models](docs/providers.md): model keys, authentication, the built-in provider extensions.
|
||||
- [Telegram bot](docs/telegram-bot.md): setup, commands, security, connection recovery.
|
||||
- [Google Calendar](docs/google-calendar.md): Google Cloud setup, connecting a chat, token storage, time zones.
|
||||
- [Building a UI (Telegram guide)](docs/ui-integration.md): how to put a chat front end on the core.
|
||||
- [Claude Code provider](docs/claude-code-provider.md): requirements, how the bridge works, limits, the doctor
|
||||
command, automatic re-login when the subscription session expires.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
```
|
||||
src/Volcanic.AgenticCore.Abstractions contracts (no runtime code except helpers)
|
||||
src/Volcanic.AgenticCore runtime, organised as vertical slices
|
||||
extensions/* provider extensions built on the public API only
|
||||
extensions/* provider and feature extensions built on the public API only
|
||||
samples/* reference host
|
||||
tests/* xunit
|
||||
```
|
||||
@@ -17,6 +17,9 @@ the streaming event protocol, `ITool`, the extension API and its events, session
|
||||
`ISecretRedactor`). Extensions reference only this assembly, which is what allows them to be loaded from separate
|
||||
directories.
|
||||
|
||||
`GoogleCalendar` is a feature extension: a tool and three commands, per-chat accounts through process-wide
|
||||
`GoogleCalendarConnections` (see [google-calendar.md](google-calendar.md)).
|
||||
|
||||
## Vertical slices
|
||||
|
||||
The runtime is organised by feature, not by layer. Each folder under `src/Volcanic.AgenticCore/Features`
|
||||
|
||||
171
docs/google-calendar.md
Normal file
171
docs/google-calendar.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Google Calendar
|
||||
|
||||
`extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar` gives the agent a Google Calendar. Each chat connects its
|
||||
own Google account from the chat itself, with no browser on the bot's machine. The Telegram bot loads it in every
|
||||
chat once the OAuth client is configured.
|
||||
|
||||
## What it does
|
||||
|
||||
- A `calendar` tool with the actions `list`, `search`, `get`, `list_instances`, `create`, `update`, `delete` and
|
||||
`calendars`. It's registered whenever the client is configured, connected or not. Before a chat connects, the tool
|
||||
tells the model to ask the user to run `/calendar_connect`.
|
||||
- Three commands: `/calendar_connect`, `/calendar_status` and `/calendar_disconnect`.
|
||||
- One Google account per chat. The account is keyed by the session scope, which the Telegram bot sets to
|
||||
`telegram-<chatId>`. A headless host uses the default scope derived from the working directory, so it gets one
|
||||
account per workspace. The tool only uses the current chat's token, so nobody sees another chat's calendar through
|
||||
the agent.
|
||||
|
||||
This controls what the agent shows to whom. It is not a security boundary between allow-listed users: every one of
|
||||
them can run shell commands as the bot's OS user, and so could read another chat's token file.
|
||||
|
||||
## Google Cloud setup
|
||||
|
||||
You need one Google Cloud OAuth client for the bot. All chats share it; each chat still approves its own account.
|
||||
|
||||
1. Create a project at [console.cloud.google.com](https://console.cloud.google.com). Then go to
|
||||
*APIs & Services → Library → Google Calendar API* and click **Enable**.
|
||||
2. Set up the *OAuth consent screen* (in the newer console: *Google Auth Platform → Branding* and *Audience*). Pick
|
||||
user type **External** and fill in the app name, a support email and the developer contact address. You don't need
|
||||
to add scopes there: the bot requests `https://www.googleapis.com/auth/calendar` itself.
|
||||
3. **Publish the app**: *Audience → Publishing status → Publish app*, so the status reads "In production". A handful
|
||||
of users doesn't need Google's verification, and refresh tokens of a published app don't expire after 7 days.
|
||||
4. **Expect the "unverified app" warning.** When a user approves access, Google first shows a page titled
|
||||
"Google hasn't verified this app". It says the app requests access to sensitive info in the Google account and
|
||||
that the user shouldn't use it until the developer verifies it with Google. The big button on it is
|
||||
**Back to safety**. To go on, click the small **Advanced** link at the bottom left, then
|
||||
**Go to <app name> (unsafe)**. On the consent screen that follows, allow "See, edit, share, and permanently delete
|
||||
all the calendars you can access using Google Calendar" and press **Continue**. Each Google account sees this
|
||||
once.
|
||||
5. Go to *Credentials → Create credentials → OAuth client ID* and pick the application type
|
||||
**TVs and Limited Input devices**. The device flow the bot uses only works with that type. Copy the client id and
|
||||
the client secret.
|
||||
|
||||
## Configure the bot
|
||||
|
||||
Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`, or `GoogleCalendar:ClientId` and `GoogleCalendar:ClientSecret` in
|
||||
`appsettings.json` (`GoogleCalendar__ClientId` and `GoogleCalendar__ClientSecret` in the environment). A value in the
|
||||
section wins over the environment variable.
|
||||
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com \
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-... \
|
||||
TELEGRAM_BOT_TOKEN=123456:ABC... \
|
||||
TELEGRAM_ALLOWED_USER_IDS=111111111 \
|
||||
dotnet run --project hosts/Volcanic.AgenticCore.Telegram
|
||||
```
|
||||
|
||||
- Empty values count as unset.
|
||||
- With neither value set, the extension stays off and logs one information line:
|
||||
`google-calendar disabled: set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET`.
|
||||
- With only one of them set, it stays off and logs a warning that names the missing one.
|
||||
- With both set, every chat gets the tool and the commands, and the three commands appear in the bot's menu and in
|
||||
`/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).
|
||||
|
||||
## Connecting a chat
|
||||
|
||||
`/calendar_connect` sends one message with the link and the code:
|
||||
|
||||
```
|
||||
Connect Google Calendar:
|
||||
1. Open https://www.google.com/device
|
||||
2. Enter the code ABCD-EFGH
|
||||
3. Approve access. I'll confirm here when it's done (code valid for 30 min).
|
||||
```
|
||||
|
||||
Open the link on any device, sign in with the Google account for this chat, enter the code and approve. The bot polls
|
||||
Google on its own, so there's nothing more to type. When you approve, the chat gets:
|
||||
|
||||
```
|
||||
✅ Google Calendar connected as you@gmail.com (time zone Europe/Berlin).
|
||||
```
|
||||
|
||||
The account shown is the primary calendar's id, which is the account's email address.
|
||||
|
||||
- Sending `/calendar_connect` again while the bot waits re-sends the same link and code with the minutes left. It
|
||||
doesn't start a second flow, because a new code would invalidate the one you may be typing.
|
||||
- A denial ("Google Calendar access was denied."), an expired code ("The Google code expired. Run /calendar_connect
|
||||
again.") and a failure ("Google Calendar connection failed: ...") each end the wait with one message.
|
||||
- `/stop` doesn't cancel the wait; it only stops agent runs. `/calendar_disconnect` does cancel it.
|
||||
- A restart of the bot loses a pending flow. Run `/calendar_connect` again.
|
||||
- Connecting again while connected switches the chat to the newly approved account. The old token is replaced once the
|
||||
new one arrives, but it is not revoked.
|
||||
|
||||
`/calendar_status` shows one of three things:
|
||||
|
||||
- `Connected as <account> since <date>, time zone <tz>.`
|
||||
- `Not connected. Run /calendar_connect.`
|
||||
- `Waiting for approval: open <url> and enter <code> (<N> min left).`
|
||||
|
||||
`/calendar_disconnect` cancels a pending flow, revokes the token at Google (best effort) and deletes the local token.
|
||||
If Google can't be reached or refuses, the token is still deleted locally and the reply says so; remove the app by hand
|
||||
at [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
|
||||
|
||||
## Where the token lives
|
||||
|
||||
Each chat's refresh token is stored in `<agentDir>/google-calendar/<chat key>/token.json`. The agent dir is
|
||||
`$VOLCANIC_AGENT_DIR`, or `~/.volcanic/agent` when that's unset. Hosts can move the root with
|
||||
`GoogleCalendarOptions.TokenDirectory`.
|
||||
|
||||
- On Unix the file has mode 0600 and its directory 0700.
|
||||
- The directory is outside every chat workspace, so the agent doesn't come across it while it reads and searches the
|
||||
workspace.
|
||||
- The chat key is the session scope (`telegram-<chatId>`) when it matches `[A-Za-z0-9._-]{1,100}`. Any other scope
|
||||
becomes `scope-<16 hex digits>`, the start of the scope's SHA-256 hash.
|
||||
- Removing a chat from the allow-list doesn't delete its directory. Run `/calendar_disconnect` first, or delete the
|
||||
directory yourself.
|
||||
|
||||
The file holds the refresh token, the granted scope, the account, the calendar's time zone and the connection time.
|
||||
The refresh token never appears in logs, tool results or error messages.
|
||||
|
||||
## Time zones
|
||||
|
||||
- Times without an offset (`2026-09-30T10:00`) are in the calendar's own time zone, which the bot reads when the chat
|
||||
connects. If that's unknown, they are in the chat's zone.
|
||||
- A time with an offset (`2026-09-30T10:00:00+02:00`) keeps its offset.
|
||||
- Every tool result starts with the zone it uses, such as `Times in Europe/Berlin.` When the calendar's zone differs
|
||||
from the chat's, the header names both:
|
||||
`Times in America/New_York (the calendar's zone; this chat is in Europe/Berlin).`
|
||||
- A date alone (`2026-09-30`) is an all-day event. All-day ends are exclusive, as in Google: an event on the 30th ends
|
||||
on the 1st.
|
||||
- For recurring events, `list_instances` returns the ids of single occurrences. `update` and `delete` detect whether an
|
||||
id is one occurrence or a whole series, so `scope` (`instance` or `series`) only needs to be passed to be explicit.
|
||||
"This and following" edits aren't supported.
|
||||
|
||||
## Testing status instead of publishing
|
||||
|
||||
You can leave the app in *Testing* status instead of publishing it:
|
||||
|
||||
- Add every user's Google account under *Audience → Test users*. Anyone else gets `access_denied`, which the bot
|
||||
reports as "Google Calendar access was denied."
|
||||
- Google expires refresh tokens of apps in testing status after 7 days. After that the tool reports that Google
|
||||
rejected the stored calendar token, and the chat has to run `/calendar_connect` again, about once a week.
|
||||
|
||||
## Other hosts
|
||||
|
||||
The extension only depends on the public API, so any host can load it:
|
||||
|
||||
```csharp
|
||||
services.AddSingleton(new GoogleCalendarOptions { ClientId = "...", ClientSecret = "..." });
|
||||
services.AddAgenticCore(o => o.AddExtension<GoogleCalendarExtension>());
|
||||
```
|
||||
|
||||
`AddExtension<T>()` creates one instance per session, which the extension needs because it keeps per-session fields.
|
||||
Don't use the overload that takes a shared instance. Without the singleton, the extension reads `GOOGLE_CLIENT_ID`
|
||||
and `GOOGLE_CLIENT_SECRET` from the environment. To add it for particular sessions only, pass an
|
||||
`ExtensionRegistration` in `AgentSessionOptions.ExtraExtensions`, as the Telegram host does
|
||||
(`GoogleCalendarSetup.Registration`). `/calendar_connect` needs a UI (`ctx.HasUi`) to show the link and code; without
|
||||
one it replies that it needs an interactive UI.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Message | Cause and fix |
|
||||
|---|---|
|
||||
| "Google Calendar is not connected. Ask the user to run /calendar_connect in this chat." | This chat never connected, or it disconnected. Run `/calendar_connect` |
|
||||
| "Google rejected the stored calendar token (revoked or expired)." | The access was revoked, the account's password changed, or the app is in testing status and 7 days have passed. Run `/calendar_connect` again |
|
||||
| "Google Calendar connection failed: Google refused the device code request: invalid_client ..." | Wrong client id or secret, or the OAuth client isn't of type *TVs and Limited Input devices* |
|
||||
| "Google returned no refresh token." | Google already had a grant for this app and didn't issue a new token. Remove the app at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) and run `/calendar_connect` again |
|
||||
| No calendar tool or commands at all | The client id or secret is missing. Check the startup log for the `google-calendar disabled` line |
|
||||
@@ -1,7 +1,7 @@
|
||||
# Google Calendar tool and connect flow — design
|
||||
|
||||
Date: 2026-09-26
|
||||
Status: draft
|
||||
Status: implemented
|
||||
Depends on: `2026-09-26-scheduler-design.md` §7–§8 (the chat's time zone, `IExtensionContext.TimeZone`).
|
||||
|
||||
## Problem
|
||||
@@ -57,7 +57,7 @@ reshaped for this harness: an extension on the public API, one Google account pe
|
||||
- **The OAuth app is published unverified ("In production").** This is the setup the owner already uses. Publishing
|
||||
needs no Google verification for a handful of users, and it avoids testing status's 7-day refresh-token expiry, so
|
||||
a chat connects once instead of weekly. Each user sees Google's "unverified app" warning once, on approval
|
||||
("Advanced → Go to <app> (unsafe)"); the setup docs show it. Testing status still works (every user added as a test
|
||||
("Advanced → Go to <app> (unsafe)"); the setup docs describe it in text. Testing status still works (every user added as a test
|
||||
user, otherwise `access_denied`, which the flow reports as a denial), but then each chat has to reconnect about
|
||||
weekly; the `invalid_grant` message covers it.
|
||||
- **Command names use underscores** (`/calendar_connect`, `/calendar_status`, `/calendar_disconnect`). Telegram only
|
||||
@@ -354,7 +354,7 @@ add to this test project) so polling and expiry run instantly.
|
||||
## Docs
|
||||
|
||||
New `docs/google-calendar.md` (Google Cloud setup: enable the Calendar API, configure the OAuth consent screen and
|
||||
**publish the app** without verification, with a screenshot of the "unverified app" warning users click through,
|
||||
**publish the app** without verification, with a text description of the "unverified app" warning users click through,
|
||||
create an OAuth client of type *TVs and Limited Input devices*, set the env vars, the three commands, where the token
|
||||
lives; testing status as an alternative, with its test-user list and 7-day refresh-token expiry). Update `docs/telegram-bot.md`
|
||||
(configuration, one account per chat), the extensions list in `CLAUDE.md` and `docs/architecture.md`.
|
||||
@@ -370,5 +370,6 @@ lives; testing status as an alternative, with its test-user list and 7-day refre
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Should `calendar.events` + `calendar.calendarlist.readonly` replace the full `calendar` scope (least privilege), if
|
||||
the device flow allows them?
|
||||
1. ~~Should `calendar.events` + `calendar.calendarlist.readonly` replace the full `calendar` scope (least privilege), if
|
||||
the device flow allows them?~~ Resolved: kept the full `calendar` scope; revisit if Google lists narrower scopes for
|
||||
the device flow.
|
||||
|
||||
@@ -22,7 +22,8 @@ Open the bot in Telegram and say hello.
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables, or the `Telegram` section of `appsettings.json` (`Telegram__Name` in the environment):
|
||||
Environment variables, or the `Telegram` section of `appsettings.json` (`Telegram__Name` in the environment; the Google
|
||||
settings use the `GoogleCalendar` section):
|
||||
|
||||
| Setting | Default | Notes |
|
||||
|---|---|---|
|
||||
@@ -32,6 +33,8 @@ Environment variables, or the `Telegram` section of `appsettings.json` (`Telegra
|
||||
| `Telegram:Model` | `claude-code/claude-opus-5-5` | Any `provider/model` the Claude Code provider serves |
|
||||
| `Telegram:EditInterval` | `00:00:01.5` | Minimum time between edits of the "Working..." status message |
|
||||
| `Telegram:ReconnectInitialDelay` / `ReconnectMaxDelay` | `1 s` / `60 s` | Backoff after lost connection |
|
||||
| `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` |
|
||||
| `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 |
|
||||
@@ -64,11 +67,16 @@ A message sent while the agent works steers the current run. Photos (with or wit
|
||||
| `/compact` | Summarise the conversation to free context |
|
||||
| `/thinking [level]` | Show or set the thinking level (Claude Code `--effort`) |
|
||||
| `/help` | Short help |
|
||||
| `/calendar_connect` | Connect this chat's Google Calendar (link and code; the bot confirms when you approve) |
|
||||
| `/calendar_status` | The connected account and time zone, or the pending code |
|
||||
| `/calendar_disconnect` | Revoke and delete this chat's calendar token, or cancel a pending connect |
|
||||
|
||||
Any other `/command` is passed to the session, so extension commands such as `/claude-code-doctor` and
|
||||
`/claude-code-login` work.
|
||||
When an extension asks something, the question appears with buttons (or as a reply prompt for free text).
|
||||
|
||||
The `/calendar_*` commands exist only when `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are set.
|
||||
|
||||
If the Claude subscription session behind the bot expires, the **first id in `AllowedUserIds`** (the bot owner)
|
||||
gets the re-login link and code prompt in their own private chat, regardless of which chat's request triggered
|
||||
it. That user must already have started a chat with the bot — Telegram bots cannot message a user first — or
|
||||
@@ -79,6 +87,13 @@ something else in the owner chat until the code is in: a second free-text questi
|
||||
an extension) replaces the pending prompt and cancels the login. See
|
||||
[claude-code-provider.md](claude-code-provider.md#session-expiry-and-re-login).
|
||||
|
||||
### Google Calendar
|
||||
|
||||
Each chat connects its own Google account with `/calendar_connect`, and the agent's `calendar` tool only uses that
|
||||
chat's account, so nobody sees another chat's calendar through the agent. The bot polls Google by itself after
|
||||
sending the link and code and confirms in the chat once access is approved. Setup of the Google Cloud app, token
|
||||
storage and time zones are in [google-calendar.md](google-calendar.md).
|
||||
|
||||
## Security
|
||||
|
||||
The agent has the built-in tools, including `bash`, and runs them as the bot's OS user. Only allow-listed user ids
|
||||
|
||||
Reference in New Issue
Block a user