forked from admin/VolcanicAgenticCore
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:
@@ -110,9 +110,15 @@ Each chat's refresh token is stored in its workspace, at `<workspace>/.volcanic/
|
||||
Telegram bot, `<WorkspaceRoot>/<chatId>/.volcanic/google-calendar/token.json`). It stays there across `/new`,
|
||||
resume and bot restarts.
|
||||
|
||||
- The file is encrypted: `{ "version": 2, "sealed": "..." }`, AES-256-GCM with a key derived from the OAuth client
|
||||
secret and bound to the chat. The agent can read the file with `read` or `bash` (it runs as the bot's OS user, and
|
||||
on some disks, such as NTFS, file modes mean nothing), but it never sees the client secret, so it can't decrypt
|
||||
the file. A file copied into another chat's folder doesn't open there either.
|
||||
- **A new client secret disconnects every chat**: their files no longer decrypt, and each chat runs
|
||||
`/calendar_connect` again.
|
||||
- A plain file, written before encryption or copied from VolcanicSecretary (`{ "refresh_token": "..." }`), is read
|
||||
once and rewritten encrypted.
|
||||
- On Unix the file has mode 0600 and its directory 0700.
|
||||
- The file is inside the workspace, so the agent can read it with `read` or `bash` and could send it with `send`.
|
||||
The refresh token gives full access to that chat's calendar; treat the workspace accordingly.
|
||||
- Deleting a chat's workspace disconnects its calendar locally. Run `/calendar_disconnect` first to also revoke the
|
||||
token at Google.
|
||||
|
||||
@@ -123,6 +129,22 @@ A host can keep tokens out of the workspaces with `GoogleCalendarOptions.TokenDi
|
||||
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.
|
||||
|
||||
## Watcher scripts
|
||||
|
||||
In the Telegram bot, a chat's [watcher scripts](telegram-bot.md#scheduling) get `GOOGLE_ACCESS_TOKEN` when the chat
|
||||
has a connected calendar. The bot refreshes it before each run so that it is valid for the job's timeout plus a minute,
|
||||
up to Google's one-hour limit. A script calls the Calendar API with it directly:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
|
||||
"https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=$(date -u +%FT%TZ)&maxResults=5"
|
||||
```
|
||||
|
||||
The script never gets the refresh token or the client secret: if it leaks the access token, that token only works on
|
||||
this chat's calendar and expires within the hour. Its value is redacted from the script's output and failure alerts.
|
||||
Without a connected calendar, or when Google rejects the stored token, the variable is absent. Keep calendar watchers'
|
||||
timeouts short: a run that outlasts the token gets `401` from Google.
|
||||
|
||||
## Time zones
|
||||
|
||||
- `create` and `update` take an optional `timeZone` (an IANA id such as `Europe/Berlin`) for the event. Times without
|
||||
|
||||
@@ -27,7 +27,7 @@ target formats were checked against this repo's code.
|
||||
| `workspace/scripts/*` | `workspaces/<id>/scripts/` | Copy. The relative paths in job commands still resolve |
|
||||
| stray root files (reports, PDFs, …) | `workspaces/<id>/` | Copy |
|
||||
| `workspace/fortress/` profiles | — | **Drop** (we have no Fortress; Playwright profiles aren't compatible). Logins have to be redone |
|
||||
| `workspace/.secretary/google-token.json` | `workspaces/<id>/.volcanic/google-calendar/token.json` | **Convert** (§3.2) |
|
||||
| `workspace/.secretary/google-token.json` | `workspaces/<id>/.volcanic/google-calendar/token.json` | Copy; the bot encrypts it on first read (§3.2) |
|
||||
| `workspace/.secretary/jobs.json` | `workspaces/<id>/.volcanic/scheduler/jobs.json` | Copy; the scheduler converts it on start (§3.3) |
|
||||
| `pi-agent/sessions/--workspace--/*.jsonl` | `agent/sessions/telegram-<id>/<sessionId>.jsonl` | **Convert**; a verbatim copy won't load (§3.1) |
|
||||
| env `SECRETARY_TIMEZONE` (probably Europe/Warsaw) | `agent/telegram/chat-settings.json` | Generate `{"version":1,"chats":{"<id>":{"timeZone":"Europe/Warsaw"}}}` |
|
||||
@@ -79,10 +79,9 @@ with a fresh session that has a compaction summary built from the old one, inste
|
||||
|
||||
### 3.2 Google Calendar token
|
||||
|
||||
The source is `{"refresh_token":"1//0e…"}`. Our `GoogleTokenStore` reads `refreshToken`: key case is ignored but
|
||||
the underscore still matters, so a straight copy reads as "not connected". Write
|
||||
`{"refreshToken":"…","scope":"https://www.googleapis.com/auth/calendar","account":"<owner gmail>","timeZone":"Europe/Warsaw","connectedAt":"<now>"}`
|
||||
with mode 0600 and directories 0700.
|
||||
**Implemented:** `GoogleTokenStore` reads a plain `{"refresh_token":"1//0e…"}` and rewrites it encrypted
|
||||
(`{"version":2,"sealed":…}`, a key derived from the client secret, bound to the chat), so a straight copy works. The
|
||||
account and calendar time zone are unknown until a reconnect; the chat's zone is used meanwhile.
|
||||
|
||||
Both apps use the device flow with a "TVs and Limited Input devices" client. The refresh token is bound to the
|
||||
client, so **set our `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` to the Secretary's values**. With a different client,
|
||||
|
||||
@@ -164,7 +164,9 @@ script itself, **without the agent**:
|
||||
Later identical failures stay quiet until the script succeeds again.
|
||||
|
||||
Scripts run in the chat's workspace with the same filtered environment as `bash` and `TZ` set to the chat's zone.
|
||||
Known secrets are redacted from their output.
|
||||
When the chat has a connected Google Calendar they also get `GOOGLE_ACCESS_TOKEN`, a short-lived token for that
|
||||
calendar ([google-calendar.md](google-calendar.md#watcher-scripts)). Known secrets and that token are redacted from
|
||||
their output.
|
||||
|
||||
When a job fires while the agent is working, a scheduled prompt waits until the current work is done; it never
|
||||
interrupts. Occurrences that come due while the same job is still running are skipped.
|
||||
|
||||
Reference in New Issue
Block a user