fix(daily-briefing): add calendar/tasks scope re-auth guidance

This commit is contained in:
William Valentin
2026-02-23 16:07:12 -08:00
parent 056b8ce515
commit 49fd2b5327
6 changed files with 96 additions and 7 deletions
+12
View File
@@ -65,6 +65,18 @@ automation:
3. Confirm backup cron and daily briefing cron schedules match operator expectations. 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`). 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 ## Notes
- Heartbeat notification noise is controlled by `automation.heartbeat.notify_cooldown` (default `30m`). - Heartbeat notification noise is controlled by `automation.heartbeat.notify_cooldown` (default `30m`).
+18 -2
View File
@@ -1,8 +1,23 @@
{ {
"version": "1.0", "version": "1.0",
"updated_at": "2026-02-23", "updated_at": "2026-02-24",
"description": "Tracks the status of all Flynn plans and implementation phases", "description": "Tracks the status of all Flynn plans and implementation phases",
"plans": { "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": { "council-tool-timeout-override": {
"status": "completed", "status": "completed",
"date": "2026-02-23", "date": "2026-02-23",
@@ -6355,7 +6370,7 @@
} }
}, },
"overall_progress": { "overall_progress": {
"total_test_count": 1967, "total_test_count": 1971,
"all_tests_passing": true, "all_tests_passing": true,
"p0_completion": "3/3 (100%)", "p0_completion": "3/3 (100%)",
"p1_completion": "4/4 (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", "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_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", "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", "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", "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", "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",
+14
View File
@@ -185,6 +185,19 @@ describe('calendar.today', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded'); 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 () => { it('respects calendarId parameter', async () => {
@@ -326,6 +339,7 @@ describe('calendar.search', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded'); expect(result.error).toContain('API quota exceeded');
expect(result.error).not.toContain('flynn gcal-auth');
}); });
it('formats events with all fields', async () => { it('formats events with all fields', async () => {
+20 -3
View File
@@ -109,6 +109,20 @@ function formatEvents(events: EventSummary[]): string {
.join('\n\n'); .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. * Creates Google Calendar query tools bound to the given GcalConfig.
* Tools create their own OAuth2 client per invocation. * Tools create their own OAuth2 client per invocation.
@@ -150,10 +164,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events), output: formatEvents(events),
}; };
} catch (error) { } catch (error) {
const message = parseErrorMessage(error);
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error), error: formatToolError(message),
}; };
} }
}, },
@@ -211,10 +226,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events), output: formatEvents(events),
}; };
} catch (error) { } catch (error) {
const message = parseErrorMessage(error);
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error), error: formatToolError(message),
}; };
} }
}, },
@@ -268,10 +284,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events), output: formatEvents(events),
}; };
} catch (error) { } catch (error) {
const message = parseErrorMessage(error);
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error), error: formatToolError(message),
}; };
} }
}, },
+14
View File
@@ -171,6 +171,19 @@ describe('tasks.lists', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded'); 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.success).toBe(false);
expect(result.error).toContain('Not Found'); expect(result.error).toContain('Not Found');
expect(result.error).not.toContain('flynn gtasks-auth');
}); });
it('respects maxResults parameter', async () => { it('respects maxResults parameter', async () => {
+18 -2
View File
@@ -94,6 +94,20 @@ function formatTasks(tasks: TaskSummary[]): string {
.join('\n\n'); .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. * Creates Google Tasks read-only tools bound to the given GtasksConfig.
* Tools create their own OAuth2 client per invocation. * Tools create their own OAuth2 client per invocation.
@@ -137,10 +151,11 @@ export function createGtasksTools(config: NonNullable<GtasksConfig>): Tool[] {
output: formatTaskLists(lists), output: formatTaskLists(lists),
}; };
} catch (error) { } catch (error) {
const message = parseErrorMessage(error);
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error), error: formatToolError(message),
}; };
} }
}, },
@@ -204,10 +219,11 @@ export function createGtasksTools(config: NonNullable<GtasksConfig>): Tool[] {
output: formatTasks(taskList), output: formatTasks(taskList),
}; };
} catch (error) { } catch (error) {
const message = parseErrorMessage(error);
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error), error: formatToolError(message),
}; };
} }
}, },