diff --git a/SECURITY.md b/SECURITY.md index 1fa5b0b3b9..048d6cf977 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,7 @@ Security fixes are handled on the default branch until formal releases are cut. - Give admin accounts strong passwords and enable 2FA where possible. - Leave high-risk agent tools restricted to admins: shell, Python, file read/write, email send/read, MCP, app API, task/skill/memory management, settings, tokens, and model serving. - Rotate API keys, webhook secrets, and Odysseus API tokens if they appear in logs, screenshots, demos, or shared chats. -- Treat shell, model-serving, MCP, email, calendar, and vault features as privileged admin functionality. +- Treat shell, model-serving, MCP, email, calendar, and vault features as privileged admin functionality. Both `read_calendar` and `manage_calendar` are admin-only; `read_calendar` is also disabled in plan mode because its owner-scoped CalDAV pull can refresh the local cache. - Common internal-only ports are Odysseus `7000`, SearXNG `8080`, ntfy `8091`, ChromaDB `8100`, Ollama `11434`, and local model/provider APIs such as `8000-8020`. ## Publishing A Fork diff --git a/docs/calendar-read-tool.md b/docs/calendar-read-tool.md new file mode 100644 index 0000000000..e77b5f25e6 --- /dev/null +++ b/docs/calendar-read-tool.md @@ -0,0 +1,23 @@ +# Read-only Calendar tool + +`read_calendar` is the bounded, admin-only Calendar facade for agent and voice integrations. It uses the authenticated owner from the tool execution context and supports only: + +- `list_calendars` +- `list_events` with explicit ISO `start` and `end` values + +The tool attempts an owner-scoped CalDAV pull before reading the local owner-scoped cache. That pull may refresh cached CalDAV rows, but the tool never creates, updates, or deletes a user event. If the pull fails or CalDAV is not configured, the cached read still succeeds with `calendar_freshness="sync_failed"` and a response beginning `Calendar freshness could not be confirmed; cached owner-scoped data follows.` Sync error details are not exposed. Because the refresh can update the local cache, `read_calendar` is disabled in plan mode even though its user-facing Calendar operations are read-only. + +`max_results` defaults to 50 and cannot exceed 100. Event ranges must be ordered and cannot exceed 366 days. Returned text fields are treated as untrusted tool data and capped at 500 characters, the formatted response is capped at 20,000 characters, and truncation is reported through `data_truncated` and `response_truncated`. + +Example: + +```json +{ + "action": "list_events", + "start": "2026-07-15T00:00:00", + "end": "2026-07-16T00:00:00", + "max_results": 25 +} +``` + +Use `manage_calendar` for event creation, updates, and deletion. `read_calendar` has no dependency on a specific UI or voice implementation. diff --git a/routes/chat_routes.py b/routes/chat_routes.py index b8d9934b4c..99029dd4a5 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -892,7 +892,7 @@ async def chat_stream(request: Request) -> StreamingResponse: "read_file", "write_file", "edit_file", "create_document", "edit_document", "update_document", "send_email", "reply_to_email", - "manage_notes", "manage_calendar", "manage_tasks", + "manage_notes", "manage_calendar", "read_calendar", "manage_tasks", "api_call", "builtin_browser", }) if _search_enabled: diff --git a/src/agent_loop.py b/src/agent_loop.py index 6f7dde6054..d4e747a12d 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -121,7 +121,7 @@ def _load_mcp_disabled_map() -> Dict[str, set]: - AFTER A TOOL SUCCEEDS, do not second-guess. The success message ("Document edited: v2, 1 edit") means it worked. Reply in ONE short sentence confirming what was done. No re-checking, no replaying the diff in your head, no validation theater. - AFTER A TOOL FAILS (timeout, error, "Unknown action", "not found"), DO NOT GO SILENT. The user expects a follow-up: either retry with a fix (e.g. correct args, longer-running form, run `tail -f /tmp/foo.log` to see progress, split into smaller steps), OR explicitly tell them "this didn't work, want me to try X instead?". A failed tool is not a stopping condition — only a successful one is. - YOU DECLARE WHEN THE JOB IS DONE — not a timer. Keep taking concrete steps while the task still needs them; you have plenty of rounds, so don't rush to quit just because you've made a few calls. There are exactly three ways to end a turn: (1) DONE — before you declare it, sanity-check that every concrete thing the user asked for actually exists or succeeded (file written, edit applied, command exited clean); then stop calling tools and write the final answer (that IS your "done" signal); (2) BLOCKED — you genuinely can't proceed (a capability is missing, permission denied, or data you can't obtain), so say plainly what's blocking you, in a sentence or two, and stop; (3) keep going with the single most useful next step. The only wrong moves are trailing off mid-task without one of these, and repeating a call you already ran. -- Calendar: call `manage_calendar` with `action=list_calendars` FIRST before create/update/delete operations. +- Calendar reads use `read_calendar`; create/update/delete operations call `manage_calendar` with `action=list_calendars` first. - BULK email actions ("delete all those", "mark all as read", "archive these", "delete all spam", "mark these 19 read") → use the `bulk_email` tool ONCE with either the exact `uids` list from the latest `list_emails` result or `all_unread: true`. NEVER just say you deleted/archived/marked messages unless a delete/archive/mark/bulk email tool call succeeded. NEVER loop mark_email_read / archive_email / delete_email one message at a time — that floods the context and can blow the token budget. One bulk_email call handles the whole set. - Email UIDs are the values after `UID:` in tool output, not list row numbers. For example, row `1.` with `UID: 90186` must use `"90186"`, never `"1"`. - "Last/latest/newest email" means call `list_emails` with `max_results: 1`, `unread_only: false`, and the right `account`, then read the UID returned by that tool if full content is needed. NEVER use a table row number like "#18" as an email UID. @@ -171,7 +171,7 @@ def _load_mcp_disabled_map() -> Dict[str, set]: - AFTER A TOOL SUCCEEDS, do not second-guess. A success response means it worked. Reply in ONE short sentence confirming what was done. No verification thinking, no re-analyzing — move on. - AFTER A TOOL FAILS, DO NOT GO SILENT. The user expects a follow-up: retry with a fix, run a diagnostic (`tail`, `ls`, `which`), or explicitly tell them what didn't work and what you'll try next. Failure is not a stopping condition. - YOU DECLARE WHEN THE JOB IS DONE — not a timer. Keep taking concrete steps while the task still needs them; don't quit early just because you've made a few calls. Three ways to end a turn: (1) DONE — before declaring it, verify every concrete deliverable the user asked for actually exists or succeeded; then stop calling tools and write the final answer (that IS your "done" signal); (2) BLOCKED — you can't proceed (missing capability, permission denied, unobtainable data), so state plainly what's blocking you and stop; (3) keep going with the single most useful next step. Never trail off mid-task without (1) or (2), and never repeat a call you already ran. -- Calendar: call `manage_calendar` with `action=list_calendars` FIRST before create/update/delete operations. +- Calendar reads use `read_calendar`; create/update/delete operations call `manage_calendar` with `action=list_calendars` first. - "Create/add/write a note" / "notes" / "todos" / "remind me to X at