Skip to content

feat(cal,meet): scope-aware commands, Calendar event fields, Google Meet settings - #83

Open
robpc wants to merge 3 commits into
mainfrom
feat/scope-aware-commands
Open

robpc wants to merge 3 commits into
mainfrom
feat/scope-aware-commands

Conversation

@robpc

@robpc robpc commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Closes #82, #80. Closes the buildable half of #81.

Three commits' worth of work in dependency order: the bug had to be fixed before the scope layer could work, and the scope layer had to exist before the Meet scope could be added without a coordinated re-auth.


1. The bug (#82) — ADR-034 §1

desk auth status had reported missing_scopes: [] for every user since ADR-030 shipped. Two compounding causes:

  1. Load overwrote granted with requested. SCOPES was passed into Credentials.from_authorized_user_info(), and google-auth only reads the token's stored scopes field when that argument is None. So creds.scopes became the requested set and _missing_scopes() computed set(SCOPES) - set(SCOPES).
  2. Save persisted the requested set as if it were granted. to_json() serializes scopes (requested), never granted_scopes, and from_authorized_user_info() doesn't restore it — so the consented set was discarded at first save and lived only in memory right after a refresh.

Fixing the load path alone would have done nothing; there was no persisted truth to read. Now the granted set is stored under its own key, re-attached on load, and read by _missing_scopes(). None means unknown, not empty.

Verified on a real token during this work: it self-healed on an ordinary refresh, with no re-auth, and auth status then reported the genuine presentations-era drift for the first time.

2. Scope-aware commands — ADR-034

  • enforce_scopes() fails fast with INSUFFICIENT_SCOPES naming the scope, affected commands, and desk auth login, before any API call.
  • SCOPE_COMMANDS maps a scope to a service or a specific command.
  • --capabilities reports per-command scope and tri-state enabled, derived from the map so it can't drift from the gate.

Two properties taken from cafe's ADR-006/024: scopes resolve at invocation, never at import (desk imports every command module at startup, so an import-time keyring read crashes headless hosts), and unknown grants fail open.

Caught while implementing: the capabilities scope read turned desk --capabilities into a NoKeyringError traceback on hosts with no keyring backend — the exact regression cafe's ADR-024 documents. Keyring reads now treat a missing backend as "nothing stored"; writes still fail loudly.

3. Calendar event fields (#80) — ADR-035

  • --meet on create and update. Idempotent on update; requestId derived from the event because Calendar treats it as an idempotency key.
  • --send-updates all|external-only|none on create/update/delete/respond, default all. This was hardcoded in four places — worse than the issue reported, since you could never opt out: deleting an event always mailed a cancellation to every attendee.
  • --hide-guest-list, --no-guest-invites, --guests-can-modify, --location, --visibility, --free. Only sent when passed.
  • Reads surface meetLink / conferenceId / conferenceStatus. A gap cal: create/update can't set Meet link, guest permissions, or sendUpdates #80 didn't mention: _parse_event() dropped hangoutLink entirely, so desk couldn't display a Meet link on any event.

4. Google Meet (#81) — ADR-036

New desk meet group — read and update for auto-recording, auto-transcription, auto smart notes. Each takes on/off/default; only requested fields enter the updateMask.

Not desk cal create --auto-record as #81 suggested: that makes a Calendar command call the Meet API, which ADR-003 forbids, bundles two failure modes, and would gate cal create on a scope most callers don't need. The two compose through conferenceId instead:

desk cal create "Training" --start ... --end ... --meet --json   # → conferenceId
desk meet update <conferenceId> --auto-record on

Co-hosts are documented, not implemented. spaces.members.create is restricted to Google's Developer Preview Program, so shipping it would fail for anyone unenrolled with an error that looks like a desk bug. desk meet --help states the limitation and the ordering trap (add guests before picking co-hosts). Parked in idea 081.

Adds meetings.space.settings — non-sensitive, and documented for spaces created by other apps, so meetings.space.created isn't needed.

Also removes the @requires_scope decorator from ADR-034: it existed for a scope covering part of a service, which Meet was expected to be. Making Meet its own group left it with no user and no reader, so it's deleted rather than shipped unused. ADR-036 §5 records the amendment.

Verification

  • 948 tests (855 baseline, 93 new), passing normally and under PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring.
  • Reverting only the _missing_scopes fix fails 3 of the new tests, so they're a real regression guard.
  • Live against a real token, three signals agreeing: auth status --verify reports meet: false with missing_scopes: [meetings.space.settings]; --capabilities reports meet enabled=false, slides enabled=true; desk meet read fails fast with INSUFFICIENT_SCOPES and affected_commands: ["meet (all commands)"].

Docs

ADR-034 (amends ADR-030 §3), ADR-035, ADR-036 (amends ADR-034 §2); ideas 079–081; a correction appended to idea 060, whose "rollout complete" note was wrong about the proactive half; CLAUDE.md architecture, README, and both README indexes (which were also missing ADRs 032–033 and ideas 077–078).

Left undone, deliberately

  • Meet co-hosts (preview-gated) — idea 081.
  • --send-updates still defaults to all; flipping it to none would be safer but silently changes every existing script, so it's an open question in idea 080 rather than a decision made here.
  • Only slides and meet are gated, since they're the only services with post-release scopes.

🤖 Generated with Claude Code

`desk auth status` reported `missing_scopes: []` for every user because the
granted scope set was never persisted, so the proactive half of ADR-030 §3
never worked (#82).

Two compounding causes: `SCOPES` was passed into
`Credentials.from_authorized_user_info()`, which makes `creds.scopes` the
*requested* set (google-auth only reads the stored field when the argument is
None); and `to_json()` doesn't serialize `granted_scopes`, so the consented set
was discarded at first save.

Fix: persist the granted set under its own key, re-attach it on load, and read
it in `_missing_scopes()`. `None` means unknown, not empty — pre-fix tokens
recover on their next refresh with no re-auth.

Builds the scope-aware layer this unblocks (ADR-034):

- `enforce_scopes()` fails fast with `INSUFFICIENT_SCOPES` naming the scope,
  affected commands, and the fix, before any API call. `@requires_scope`
  wraps it for partial-coverage scopes.
- `SCOPE_COMMANDS` maps scope to a service or a specific command.
- `--capabilities` reports per-command `scope` and tri-state `enabled`,
  derived from the map so it can't drift from the gate.
- `slides` gates on `presentations` at its `_get_client()` choke point.

Scopes resolve at invocation, never at import, and unknown grant sets fail
open. Keyring reads now treat a missing backend as "nothing stored" — the
capabilities scope read otherwise turned `desk --capabilities` into a
NoKeyringError traceback on headless hosts.

Refs #80, #81

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Meet settings

Closes #80. Closes the buildable half of #81.

## Calendar (ADR-035, #80)

- `--meet` on create and update attaches a Google Meet conference. Idempotent on
  update; requestId is derived from the event since Calendar treats it as an
  idempotency key.
- `--send-updates all|external-only|none` on create/update/delete/respond,
  defaulting to `all`. This was hardcoded in four places, so you could never opt
  out — deleting an event always mailed a cancellation to every attendee.
- `--hide-guest-list`, `--no-guest-invites`, `--guests-can-modify`, `--location`,
  `--visibility`, `--free`. Only sent when passed, so Google's defaults stand.
- Reads now surface `meetLink`, `conferenceId`, and `conferenceStatus`. Desk
  previously dropped `hangoutLink` entirely and couldn't show a Meet link on any
  event, whoever created it. `conferenceId` is what addresses a Meet space.

## Meet (ADR-036, #81)

New `desk meet` service group with `read` and `update`, over the Meet REST API:
auto-recording, auto-transcription, and auto smart notes. Each setting takes
on/off/default, mapping to AutoGenerationType; only requested fields go in the
updateMask.

Not folded into `desk cal create --auto-record` as #81 suggested: that would make
a Calendar command call the Meet API, which ADR-003 forbids. The two compose via
`conferenceId` instead.

Co-hosts are documented as UI-only rather than implemented — `spaces.members` is
restricted to Google's Developer Preview Program. Parked in idea 081.

Adds the `meetings.space.settings` scope. It's non-sensitive and documented for
spaces created by other apps, so a Calendar conference is reachable without
`meetings.space.created`. Existing tokens keep working; `desk meet` reports
itself disabled and fails fast per ADR-034.

Also removes the `@requires_scope` decorator added in ADR-034. It existed for a
scope covering part of a service, which Meet was expected to be; making Meet its
own group left it with no user and no reader.

948 tests pass, normally and under a keyring-less backend.
@robpc robpc changed the title fix(auth): persist granted scopes; add scope-aware commands feat(cal,meet): scope-aware commands, Calendar event fields, Google Meet settings Jul 31, 2026
Revisited idea 080's open question with the user. The premise for switching to
`none` was weaker than it looked: Calendar's own UI default (add a guest, hit
the primary button) only notifies that guest, not the full attendee list — but
that scoping happens as backend dedup under sendUpdates=all, not a distinct API
value. all already tracks the UI's default lean for adds, and is correctly
unscoped for deletes/reschedules where every attendee is genuinely affected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auth: granted scopes are never persisted, so auth status missing_scopes is always empty

1 participant