Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ Notable changes to `@wirecat/gcal-cli`. One section per version, newest first.

### Fixed

- Sharing a calendar through `api calendar-acl-insert | patch | update` or `api call calendar.acl.*` no longer mails
the person it is shared with unless the new key `sharing.notify` allows it. Google mails them unless
`sendNotifications` is `false`, and over MCP the sharing methods run without a question. `sharing.notify` is `deny`
by default: such a call is refused before anything reaches Google, and the refusal says to pass
`--send-notifications false` or allow the key. gcal never changes the request itself. Removing access
(`calendar-acl-delete`) mails nobody and needs no new key. `--dry-run` lists `sharing.notify` with its level.
- `gcal config set | unset` lists only the settings gcal uses: the messenger settings (`sends_per_hour`,
`transcribe_with`, `phone_countries`, `task_*` and the others) are gone, and `gcal skill show` takes no skill name.
Over MCP, `sync` and `calendars list` run through `gcal_read`, since they only save what they read from Google.
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Without a setting of yours:
- Sharing a calendar (`calendar.acl.insert`, `patch`, `update`, `delete`) and giving it to another account
(`calendar.calendars.transferOwnership`) are `ask`, like any other change. Over MCP that means an agent can share
your calendar with anyone, or give it away, without a question. Set `permissions.api.calendar.acl deny` to stop it.
- `sharing.notify`, letting Google mail the person a calendar is shared with, is `deny`. Google sends that mail
unless `sendNotifications` is `false`, so while the key is `deny` a share must say `--send-notifications false`.
- Emptying a calendar (`calendar.calendars.clear`, every event of the main calendar) is `deny`, and never offered over
MCP.
- The event writes (`events create`, `update`, `delete`, `respond`) are `ask`, each under its own key, such as
Expand All @@ -65,10 +67,14 @@ An event write also needs the key of the Google method it calls: `api.calendar.e
The `api` commands answer to the same keys. A method that has Google mail the attendees (`--send-updates` other than
`none`, or `--send-notifications true`) also needs `events.notify`. Deleting an event, or patching it to `cancelled`,
also needs `events.delete.series` when the event repeats: `gcal` reads the event from Google first to find out.
Sharing a calendar (`calendar.acl.insert`, `patch`, `update`) also needs `sharing.notify` unless the call passes
`--send-notifications false` (`"sendNotifications": false` in `api call --params`); `gcal` never adds it for you.
Removing access (`calendar.acl.delete`) mails nobody, so it needs no extra key.
`--dry-run` lists every key a call checks, with its level, and calls nothing.

```sh
gcal personal config set permissions.events.notify allow # let --notify mail attendees
gcal personal config set permissions.sharing.notify allow # let a share mail the person it is shared with
gcal personal config set permissions.events.create allow # add events without the question
```

Expand Down
28 changes: 14 additions & 14 deletions docs/dev/test-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ran it · ⛔ not tested offline, with the reason and where it is checked instea
Measured from the test run (`coverage/argv.jsonl`), not searched for — see
[TESTING.md](TESTING.md) for how, and for the states and failures that cut across commands.

**192 ✅ · 331 ⛔ · 0 ❌** — 71 commands, 452 options.
**205 ✅ · 318 ⛔ · 0 ❌** — 71 commands, 452 options.

| Command | Option | | Note |
|---|---|---|---|
Expand Down Expand Up @@ -121,14 +121,14 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
| `api call` | `--params` | ✅ | |
| `api call` | `--body` | ✅ | |
| `api call` | `--dry-run` | ✅ | |
| `api calendar-acl-delete` | | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | | ✅ | |
| `api calendar-acl-delete` | `--alt` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--fields` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--user-ip` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--pretty-print` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--quota-user` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--rule-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--calendar-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-delete` | `--rule-id` | ✅ | |
| `api calendar-acl-delete` | `--calendar-id` | ✅ | |
| `api calendar-acl-delete` | `--dry-run` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-get` | | ✅ | |
| `api calendar-acl-get` | `--alt` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
Expand All @@ -144,11 +144,11 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
| `api calendar-acl-insert` | `--user-ip` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--pretty-print` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--quota-user` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--send-notifications` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--send-notifications` | ✅ | |
| `api calendar-acl-insert` | `--calendar-id` | ✅ | |
| `api calendar-acl-insert` | `--body` | ✅ | |
| `api calendar-acl-insert` | `--body-file` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--dry-run` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-insert` | `--dry-run` | ✅ | |
| `api calendar-acl-list` | | ✅ | |
| `api calendar-acl-list` | `--alt` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-list` | `--fields` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
Expand All @@ -160,28 +160,28 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
| `api calendar-acl-list` | `--calendar-id` | ✅ | |
| `api calendar-acl-list` | `--sync-token` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-list` | `--page-token` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | | ✅ | |
| `api calendar-acl-patch` | `--alt` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--fields` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--user-ip` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--pretty-print` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--quota-user` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--calendar-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--rule-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--calendar-id` | ✅ | |
| `api calendar-acl-patch` | `--rule-id` | ✅ | |
| `api calendar-acl-patch` | `--send-notifications` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--body` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--body` | ✅ | |
| `api calendar-acl-patch` | `--body-file` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-patch` | `--dry-run` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | | ✅ | |
| `api calendar-acl-update` | `--alt` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--fields` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--user-ip` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--pretty-print` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--quota-user` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--rule-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--rule-id` | ✅ | |
| `api calendar-acl-update` | `--send-notifications` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--calendar-id` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--body` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--calendar-id` | ✅ | |
| `api calendar-acl-update` | `--body` | ✅ | |
| `api calendar-acl-update` | `--body-file` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-acl-update` | `--dry-run` | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
| `api calendar-calendar-list-delete` | | ⛔ | generated from Google's Discovery document; src/alignment.test.ts checks every method's options against the snapshot, and src/api.test.ts runs generated commands and `api call` through the one executor they share |
Expand Down
3 changes: 2 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ An agent connected through `gcal <profile> mcp` can change who sees your calenda
`--grant sharing` it can share a calendar with any address, make it public, or remove someone. It can also call the
method that gives a calendar to another account; that one works only for a Google Workspace administrator, and only
when the sign-in has a permission `gcal` does not ask for. Over MCP no one is there to answer a question, so `ask`
lets these run. To keep sharing with you, run
lets these run. An agent cannot have Google mail the person it shares with: that needs `sharing.notify`, which is
`deny`. It can still share in silence, with `--send-notifications false`. To keep sharing with you, run
`gcal personal config set permissions.api.calendar.acl deny`, or sign in without `--grant sharing`. Emptying a
calendar is never offered to an agent.

Expand Down
2 changes: 2 additions & 0 deletions skills/gcal-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Find your way with the tool itself, not this file:
- A method that changes Calendar needs `--grant write`, sharing (`calendar-acl-*`) needs `--grant sharing`, and the
calendar-wide ones a permission gcal does not ask for: on `permission_error`, tell the owner; do not retry.
- Share a calendar, change a sharing rule or give a calendar away only when the owner asked for that exact change.
Pass `send_notifications: "false"` when you share: Google otherwise mails the person, and `sharing.notify` is denied
by default; do not ask to loosen it on your own.

## Rules

Expand Down
13 changes: 13 additions & 0 deletions src/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { Asker, GuardRequest, Level, SendGuard } from "@wirecat/cli-core/se

export { EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY }

/** Google mailing the person a calendar is shared with. Its own key, as `events.notify` is for attendees (WC-PERM-22). */
export const SHARING_NOTIFY_KEY = "sharing.notify"

/** The events methods that take `sendUpdates` and `sendNotifications` (Discovery snapshot, spec/google/calendar.json). */
const MAILING = new Set([
"calendar.events.insert",
Expand All @@ -25,6 +28,16 @@ export const notifies = (method: string, params: Record<string, unknown>): boole
return params.sendNotifications === true || params.sendNotifications === "true"
}

const SHARING = new Set(["calendar.acl.insert", "calendar.acl.patch", "calendar.acl.update"])

/**
* Whether Google mails the person a calendar is shared with. Unlike events, left out means it does: "The default is
* True" (https://developers.google.com/workspace/calendar/api/v3/reference/acl/insert#parameters). `acl.delete` mails
* nobody ("there are no notifications on access removal").
*/
export const notifiesSharing = (method: string, params: Record<string, unknown>): boolean =>
SHARING.has(method) && params.sendNotifications !== false && params.sendNotifications !== "false"

/** A write that removes the event it names: a delete, or a patch or update to `status: "cancelled"`. */
export const cancels = (method: string, body: Record<string, unknown> | undefined): boolean =>
method === "calendar.events.delete" ||
Expand Down
Loading
Loading