From 49fd2b5327d54356be56f35c2c7bbf27f9e50bfe Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 23 Feb 2026 16:07:12 -0800 Subject: [PATCH] fix(daily-briefing): add calendar/tasks scope re-auth guidance --- docs/operations/OPERATOR_PACK.md | 12 ++++++++++++ docs/plans/state.json | 20 ++++++++++++++++++-- src/tools/builtin/gcal.test.ts | 14 ++++++++++++++ src/tools/builtin/gcal.ts | 23 ++++++++++++++++++++--- src/tools/builtin/gtasks.test.ts | 14 ++++++++++++++ src/tools/builtin/gtasks.ts | 20 ++++++++++++++++++-- 6 files changed, 96 insertions(+), 7 deletions(-) diff --git a/docs/operations/OPERATOR_PACK.md b/docs/operations/OPERATOR_PACK.md index ff50477..be5f20c 100644 --- a/docs/operations/OPERATOR_PACK.md +++ b/docs/operations/OPERATOR_PACK.md @@ -65,6 +65,18 @@ automation: 3. Confirm backup cron and daily briefing cron schedules match operator expectations. 4. If using MinIO ingestion, confirm extractor dependencies via doctor output (`MinIO ingest extractors`). +## Troubleshooting: Daily Briefing Calendar/Tasks Blocked + +If the daily briefing reports Calendar or Tasks as blocked with insufficient permissions, re-run OAuth for both services to refresh scopes/tokens: + +1. `flynn gcal-auth` +2. `flynn gtasks-auth` + +Expected scopes after re-auth: + +- Calendar: `https://www.googleapis.com/auth/calendar.readonly` +- Tasks: `https://www.googleapis.com/auth/tasks.readonly` + ## Notes - Heartbeat notification noise is controlled by `automation.heartbeat.notify_cooldown` (default `30m`). diff --git a/docs/plans/state.json b/docs/plans/state.json index fda3cb6..1e85ade 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1,8 +1,23 @@ { "version": "1.0", - "updated_at": "2026-02-23", + "updated_at": "2026-02-24", "description": "Tracks the status of all Flynn plans and implementation phases", "plans": { + "daily-briefing-google-scope-remediation": { + "status": "completed", + "date": "2026-02-24", + "updated": "2026-02-24", + "summary": "Improved Google Calendar and Google Tasks tool error handling so insufficient-scope failures now include explicit re-auth commands (`flynn gcal-auth` / `flynn gtasks-auth`) instead of vague permission errors. Added regression tests and operator runbook troubleshooting steps for daily briefing blockers.", + "files_modified": [ + "src/tools/builtin/gcal.ts", + "src/tools/builtin/gtasks.ts", + "src/tools/builtin/gcal.test.ts", + "src/tools/builtin/gtasks.test.ts", + "docs/operations/OPERATOR_PACK.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/tools/builtin/gcal.test.ts src/tools/builtin/gtasks.test.ts src/tools/builtin/gmail.test.ts + pnpm typecheck passing" + }, "council-tool-timeout-override": { "status": "completed", "date": "2026-02-23", @@ -6355,7 +6370,7 @@ } }, "overall_progress": { - "total_test_count": 1967, + "total_test_count": 1971, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -6377,6 +6392,7 @@ "gmail_filter_creation": "completed — gmail.filter.create tool added with criteria/action validation; gmail-auth requests explicit gmail.settings.basic + gmail.readonly scopes for filter creation and inbox reads", "toolloop_action_intent_recovery": "completed — when a model claims it will execute a tool but emits no tool call, NativeAgent now issues one internal nudge and continues the same turn to execute tools or produce a concrete blocker", "toolloop_execution_claim_recovery": "completed — when a model claims a known tool already succeeded/failed without emitting a tool call, NativeAgent now nudges once and retries the same turn before returning text", + "daily_briefing_google_scope_remediation": "completed — calendar.* and tasks.* now append explicit re-auth guidance (`flynn gcal-auth` / `flynn gtasks-auth`) for insufficient-scope errors, and operator runbook includes remediation steps", "council_tool_timeout_override": "completed — ToolExecutor supports per-tool timeout overrides and council.run now uses a 180s timeout to avoid false 30s council timeouts in the tool loop", "minimal_tui_multiline_paste_mode": "completed — minimal TUI now supports `/paste`/`/multiline` multiline compose mode ending with single '.' line, preventing newline truncation for pasted prompts", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback, plus 2026-02-23 arg hydration hardening, tool.args_rewritten audit metric, transient fetch retry/timeout hardening, localhost->127.0.0.1 fallback for transcription endpoint connectivity, and whisper docker-compose entrypoint arg fix for port 18801", diff --git a/src/tools/builtin/gcal.test.ts b/src/tools/builtin/gcal.test.ts index 691b36f..00c1d75 100644 --- a/src/tools/builtin/gcal.test.ts +++ b/src/tools/builtin/gcal.test.ts @@ -185,6 +185,19 @@ describe('calendar.today', () => { expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); + expect(result.error).not.toContain('flynn gcal-auth'); + }); + + it('surfaces re-auth hint for insufficient scopes', async () => { + setupValidAuth(); + mockEventsList.mockRejectedValue(new Error('Request had insufficient authentication scopes.')); + + const [todayTool] = createGcalTools(testConfig); + const result = await todayTool.execute({}); + + expect(result.success).toBe(false); + expect(result.error).toContain('insufficient authentication scopes'); + expect(result.error).toContain('flynn gcal-auth'); }); it('respects calendarId parameter', async () => { @@ -326,6 +339,7 @@ describe('calendar.search', () => { expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); + expect(result.error).not.toContain('flynn gcal-auth'); }); it('formats events with all fields', async () => { diff --git a/src/tools/builtin/gcal.ts b/src/tools/builtin/gcal.ts index d85f3aa..136446c 100644 --- a/src/tools/builtin/gcal.ts +++ b/src/tools/builtin/gcal.ts @@ -109,6 +109,20 @@ function formatEvents(events: EventSummary[]): string { .join('\n\n'); } +function parseErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function formatToolError(message: string): string { + const needsScopeHint = /insufficient.*scope|insufficientPermissions|Request had insufficient authentication scopes/i.test(message); + return needsScopeHint + ? `${message}. Re-run "flynn gcal-auth" to grant Google Calendar read permissions.` + : message; +} + /** * Creates Google Calendar query tools bound to the given GcalConfig. * Tools create their own OAuth2 client per invocation. @@ -150,10 +164,11 @@ export function createGcalTools(config: NonNullable): Tool[] { output: formatEvents(events), }; } catch (error) { + const message = parseErrorMessage(error); return { success: false, output: '', - error: error instanceof Error ? error.message : String(error), + error: formatToolError(message), }; } }, @@ -211,10 +226,11 @@ export function createGcalTools(config: NonNullable): Tool[] { output: formatEvents(events), }; } catch (error) { + const message = parseErrorMessage(error); return { success: false, output: '', - error: error instanceof Error ? error.message : String(error), + error: formatToolError(message), }; } }, @@ -268,10 +284,11 @@ export function createGcalTools(config: NonNullable): Tool[] { output: formatEvents(events), }; } catch (error) { + const message = parseErrorMessage(error); return { success: false, output: '', - error: error instanceof Error ? error.message : String(error), + error: formatToolError(message), }; } }, diff --git a/src/tools/builtin/gtasks.test.ts b/src/tools/builtin/gtasks.test.ts index 416e458..49f19e2 100644 --- a/src/tools/builtin/gtasks.test.ts +++ b/src/tools/builtin/gtasks.test.ts @@ -171,6 +171,19 @@ describe('tasks.lists', () => { expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); + expect(result.error).not.toContain('flynn gtasks-auth'); + }); + + it('surfaces re-auth hint for insufficient scopes', async () => { + setupValidAuth(); + mockTasklistsList.mockRejectedValue(new Error('Request had insufficient authentication scopes.')); + + const [listsTool] = createGtasksTools(testConfig); + const result = await listsTool.execute({}); + + expect(result.success).toBe(false); + expect(result.error).toContain('insufficient authentication scopes'); + expect(result.error).toContain('flynn gtasks-auth'); }); }); @@ -258,6 +271,7 @@ describe('tasks.list', () => { expect(result.success).toBe(false); expect(result.error).toContain('Not Found'); + expect(result.error).not.toContain('flynn gtasks-auth'); }); it('respects maxResults parameter', async () => { diff --git a/src/tools/builtin/gtasks.ts b/src/tools/builtin/gtasks.ts index 738fec0..1543a55 100644 --- a/src/tools/builtin/gtasks.ts +++ b/src/tools/builtin/gtasks.ts @@ -94,6 +94,20 @@ function formatTasks(tasks: TaskSummary[]): string { .join('\n\n'); } +function parseErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function formatToolError(message: string): string { + const needsScopeHint = /insufficient.*scope|insufficientPermissions|Request had insufficient authentication scopes/i.test(message); + return needsScopeHint + ? `${message}. Re-run "flynn gtasks-auth" to grant Google Tasks read permissions.` + : message; +} + /** * Creates Google Tasks read-only tools bound to the given GtasksConfig. * Tools create their own OAuth2 client per invocation. @@ -137,10 +151,11 @@ export function createGtasksTools(config: NonNullable): Tool[] { output: formatTaskLists(lists), }; } catch (error) { + const message = parseErrorMessage(error); return { success: false, output: '', - error: error instanceof Error ? error.message : String(error), + error: formatToolError(message), }; } }, @@ -204,10 +219,11 @@ export function createGtasksTools(config: NonNullable): Tool[] { output: formatTasks(taskList), }; } catch (error) { + const message = parseErrorMessage(error); return { success: false, output: '', - error: error instanceof Error ? error.message : String(error), + error: formatToolError(message), }; } },