ulysses/routes
Kenny Van de Maele 8ce945d338
feat: Add plan mode to the chat agent (#638)
* feat: Add plan mode to the chat agent

Adds a plan mode: the agent investigates read-only, proposes a checklist, and
waits for approval before changing anything. On approval it runs with full
tools and checks items off as it goes. Enforcement reuses the existing
disabled_tools gate.

Includes a slash command: `/plan [on|off]` (and `/toggle plan`) to flip the
plan toggle from the chat input.

- src/tool_security.py, src/mcp_manager.py: read-only allowlist (tools + MCP).
- src/agent_loop.py, routes/chat_routes.py: union the disabled set, prepend the
  plan directive, force agent mode.
- static/: plan toggle pill, Approve & Run, dockable plan window, task-list
  checkboxes, and the /plan slash command.
- tests/test_plan_mode.py.

* Plan mode: persistent re-referenceable plan + agent write-back

Three improvements so a long plan survives a weak model and stays in reach:

1. Re-reference the plan (out-of-context fix). On the execution turn the frontend
   sends the approved checklist back (`approved_plan`); the backend pins it as a
   top-of-context `## ACTIVE PLAN` system note (kept by the context trimmer), so
   the agent can always re-read the plan instead of losing the thread on a long
   run. New `build_active_plan_note()` (unit-tested).

2. Re-open / dock the plan anytime. The plan checklist is stored per-session
   (localStorage). When a plan exists, the plan-mode button opens a small menu
   ("Show plan" / "Plan mode: On/Off") that re-opens the side-dockable plan
   window — so it can stay docked while the agent works. The window live-refreshes
   as the plan changes.

3. Agent write-back: new `update_plan` tool. The agent calls it to tick steps
   `- [x]` after finishing them, or to revise steps when the user asks. Marker
   tool (no I/O) → `plan_update` SSE event → the stored plan + docked window
   update live. The ACTIVE PLAN note instructs the agent to use it.

Backend: src/agent_loop.py (param + pin + note builder + emit + prompt blurb),
src/tool_execution.py (update_plan handler), routes/chat_routes.py (parse
`approved_plan`, relay `plan_update`), registration in tool_schemas / agent_tools
/ tool_index (always-available, not admin-gated).
Frontend: static/js/chat.js (plan store, send `approved_plan`, handle
`plan_update`, capture restated checklists), static/app.js (plan-button menu),
static/js/planWindow.js (`isPlanWindowOpen`), static/js/storage.js (PLAN key).
Tests: tests/test_plan_mode.py (plan-note), tests/test_update_plan_tool.py.

* Plan mode: drop bash/python, rely on read-only discovery tools

Shell can mutate (write files, hit the network) and can't be constrained to
read-only at the tool layer, so plan mode no longer relies on a prompt to keep
it well-behaved — bash/python are removed from the read-only allowlist and added
to the fail-closed block set. Discovery is covered by the dedicated read-only
tools (read_file, grep, glob, ls) instead.

Rewrites the plan-mode directive to state shell is disabled and lists the
available read-only tools positively. Addresses review feedback on #638.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Comment: note _MCP_READONLY_VERBS are prefixes not whole words

Clarifies that entries like "summar" are intentional stems matched via
startswith (covers summarise/summarize/summary), not typos. Addresses review
feedback on #638.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Plan mode: clarify why gating inverts the allowlist into a denylist

Rename _PLAN_MODE_FALLBACK_BLOCK -> _PLAN_MODE_KNOWN_MUTATORS and rewrite the
comments. The tool gate is a denylist (disabled_tools); plan mode's policy is an
allowlist, so it returns the inverse (all known tool names minus the allowlist).
The static mutator set is a backstop for the schema-derived name list, which
misses XML-only tools and can fail to import. Addresses review feedback on #638.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Plan mode: stop hardcoding the read-only tool list in the directive

The model is already shown its available (read-only) tools by _assemble_prompt,
which removes every disabled tool. Enumerating them again in the directive only
duplicated that list and would drift as tools change. Point at the tools listed
below instead. Addresses review feedback on #638.
2026-06-05 16:32:25 +02:00
..
__init__.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
admin_wipe_routes.py Admin: wipe gallery albums with images 2026-06-02 20:35:57 +09:00
api_token_routes.py Codex Agent integration: HTTP surface + plugin bundle + Settings UI 2026-06-03 22:49:09 +09:00
assistant_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
auth_routes.py feat: round-limit handling — Continue affordance at the cap + configurable cap (#1999) 2026-06-04 22:36:05 +02:00
backup_routes.py fix: backup import drops a user's memory when its text matches another user's (#1743) 2026-06-03 13:29:14 +09:00
calendar_routes.py fix: Settings now correctly displays CalDAV integrations when more than one isconfigured (#2901) 2026-06-05 16:11:08 +02:00
chat_helpers.py fix: normalize Gemma 4 thought-channel output (#2224) 2026-06-04 19:26:58 +02:00
chat_routes.py feat: Add plan mode to the chat agent (#638) 2026-06-05 16:32:25 +02:00
cleanup_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
codex_routes.py cookbook agent debug loop: persistent log files, auto-adopt orphan tmux, Codex/Claude skill parity 2026-06-04 23:27:18 +09:00
compare_routes.py fix(compare): stop blind mode leaking model identities via session names (#1318) 2026-06-04 04:39:01 +01:00
contacts_routes.py Harden DAV outbound URL validation (#2819) 2026-06-05 13:22:21 +02:00
cookbook_helpers.py Fix/windows llama cpp serve and test upstream (#2669) 2026-06-05 14:53:33 +02:00
cookbook_routes.py Fix/windows llama cpp serve and test upstream (#2669) 2026-06-05 14:53:33 +02:00
copilot_routes.py feat(provider): add GitHub Copilot provider with device-flow auth (#1480) 2026-06-04 21:13:14 +02:00
diagnostics_routes.py Fix email-thread HTML injection, attachment path traversal, and missing authz (#475) 2026-06-01 22:20:17 +09:00
document_helpers.py fix: _resolve_user_upload_path crashes on a non-dict resolve_upload result (#1715) 2026-06-03 13:34:33 +09:00
document_routes.py fix: quote IMAP mailbox arguments (#2170) 2026-06-05 16:00:20 +02:00
editor_draft_routes.py Ignore invalid editor draft payloads (#1533) 2026-06-03 14:07:03 +09:00
email_helpers.py fix(email): scope AI caches by owner (#2695) 2026-06-05 02:21:50 +02:00
email_pollers.py fix: quote IMAP mailbox arguments (#2170) 2026-06-05 16:00:20 +02:00
email_routes.py fix(email): scope AI caches by owner (#2695) 2026-06-05 02:21:50 +02:00
embedding_routes.py Add support for EMBEDDING_API_KEY (#2691) 2026-06-05 14:47:24 +02:00
emoji_routes.py Harden emoji SVG proxy responses (#2842) 2026-06-05 10:31:58 +02:00
font_routes.py Keep compact font family names together (#1263) 2026-06-03 14:24:30 +09:00
gallery_helpers.py fix: gallery records raw instead of display dimensions for EXIF-rotated photos (#1667) 2026-06-03 14:23:04 +09:00
gallery_routes.py fix(gallery): validate target album owner on image PATCH + owner-scope album count/cover (#2755) 2026-06-05 15:01:01 +02:00
history_routes.py fix(history): block compact during active runs (#2635) 2026-06-04 21:50:16 +02:00
hwfit_routes.py Merge remote-tracking branch 'origin/main' into visual-pr-playground 2026-06-03 16:49:10 +09:00
mcp_routes.py feat(mcp): add Streamable HTTP transport with OAuth 2.0 (#1033) 2026-06-05 02:40:52 +02:00
memory_routes.py fix(uploads): bound direct upload reads 2026-06-04 00:32:50 +01:00
model_routes.py fix: bool('false') is True coerces endpoint toggles incorrectly (#2361) 2026-06-04 19:43:38 +02:00
note_routes.py fix: fire-reminder endpoint crashes with NameError on _gcu (#1250) 2026-06-03 01:02:25 +09:00
personal_routes.py fix: personal-docs path confinement used abspath, allowing symlink escape (#1728) 2026-06-03 13:29:57 +09:00
prefs_routes.py Persist user prefs atomically (#1840) 2026-06-04 03:55:22 +01:00
preset_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
research_routes.py fix(research): owner-scope endpoint resolution 2026-06-03 23:19:28 +01:00
search_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
session_routes.py fix(history): block compact during active runs (#2635) 2026-06-04 21:50:16 +02:00
shell_routes.py Fix/windows llama cpp serve and test upstream (#2669) 2026-06-05 14:53:33 +02:00
signature_routes.py Constrain signature uploads to PNG data (#2844) 2026-06-05 13:17:43 +02:00
skills_routes.py fix: skill test-task / precision helpers crash on a non-dict skill (#1638) 2026-06-03 08:59:24 +09:00
stt_routes.py fix(uploads): bound direct upload reads 2026-06-04 00:32:50 +01:00
task_routes.py Cookbook scheduler + serve: schedule via Tasks, Stop verifies kill, Ollama auto port-pick 2026-06-05 14:41:43 +09:00
tts_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
upload_routes.py Constrain upload paths to upload root (#2825) 2026-06-05 13:15:23 +02:00
vault_routes.py Keep Bitwarden unlock password off argv (#1311) 2026-06-03 02:13:51 +09:00
webhook_routes.py Reapply "Merge branch 'main' of github.com:pewdiepie-archdaemon/odysseus" 2026-06-03 22:47:00 +09:00
workspace_routes.py feat: Add workspace: confine agent tools to a folder (#1103) 2026-06-05 00:06:37 +02:00