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
|
||||
|
||||
Reference in New Issue
Block a user