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.
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`).
+18 -2
View File
@@ -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",
+14
View File
@@ -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 () => {
+20 -3
View File
@@ -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<GcalConfig>): 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<GcalConfig>): 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<GcalConfig>): 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),
};
}
},
+14
View File
@@ -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 () => {
+18 -2
View File
@@ -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<GtasksConfig>): 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<GtasksConfig>): 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),
};
}
},