mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
620fdd0859
* feat(agent): workspace confinement via context-local binding + get_workspace tool Bind the per-turn workspace once in execute_tool_block; the shared path resolvers (_resolve_tool_path / _resolve_search_root) and the subprocess cwd helper (agent_cwd) read it, so file tools + bash/python are confined centrally and a new tool that uses the shared helpers cannot accidentally bypass it. Adds the admin-gated /api/workspace/browse picker, a workspace pill + directory modal (reusing existing modal/button CSS), the /workspace slash command, and a get_workspace tool (replaces a system-prompt block). Confinement is OS-agnostic (realpath/normcase/commonpath) and docker-safe (container paths, no host assumptions). Reopens #2023. * ux(workspace): clarify workspace is not a sandbox Picker modal note + pill tooltip + get_workspace tool/output wording now state plainly: read_file/write_file/edit_file/grep/glob/ls are confined to the folder, but bash/python only start there (cwd) and are not sandboxed. Modal note reuses the existing .muted class. * fix(agent): treat an active workspace as file-work intent A vague low-signal message (e.g. "look at the local project") matches no domain keywords, so tool retrieval is skipped and only always-available tools are offered — leaving the agent with no file access even though a workspace is set. When a workspace is active, include the file/code tools (incl. get_workspace) on low-signal turns so the agent can act on the folder. Also requires the tool index (ChromaDB) to be reachable for normal retrieval; that is an environment dependency, not part of this change. * ux(workspace): hide pill + overflow entry in chat mode Workspace only scopes the agent's file/shell tools, so the pill and the overflow 'Workspace' entry are agent-only now — hidden in chat mode like the bash toggle. Mode read from the DOM in syncWorkspaceIndicator; applyMode() is called from the agent/chat setMode handler. * prompt(tools): steer bash/python to defer to the dedicated file tools bash/python schema descriptions (what native-tool-calling models read) were bare and gave no steer, so models would do file ops via the shell (e.g. writing SVG/HTML, which then dumps raw markup into the tool preview). Tell bash/python in the schema + tool-index + prompt section to prefer read_file/write_file/ edit_file/grep/glob/ls and only be used for what those do not cover. * prompt(tools): keep bash/python deferral generic (no hardcoded tool names) Reference 'a dedicated tool' rather than listing read_file/write_file/grep/etc. by name, so the guidance does not go stale if those tools are renamed. * style(workspace): drop em-dashes from added code comments/strings * ux(workspace): terser non-sandbox note in picker (no tool-name list) * ux(workspace): mirror terse non-sandbox wording in pill tooltip * chore: untrack local venv symlink (run-only, not part of the feature) * prompt(workspace): keep get_workspace text generic (no hardcoded tool names) * fix(agent): low-signal + workspace surfaces only read-only file tools Intersect the files tool group with PLAN_MODE_READONLY_TOOLS so a vague message in a workspace exposes read_file/grep/glob/ls/get_workspace for exploration, but not write_file/edit_file/bash/python -- those wait for a request that actually calls for them (RAG retrieval still adds them on a real ask). * feat(workspace): cap browse listing at 500 dirs with a truncated hint Mirror the filesystem_tools._CODENAV_MAX_HITS pattern with a module-local _MAX_BROWSE_DIRS so a directory with thousands of children does not dump every row into the picker; the response carries a truncated flag and the modal tells the user to type a path to jump in. * chore: untrack local venv symlink (run-only artifact) * fix(workspace): vet the workspace root against the sensitive-path deny list at bind time The in-workspace resolver deny-lists sensitive paths inside the workspace, but the empty-path search root is the workspace itself, so a workspace of ~/.ssh could be listed via ls with no path. vet_workspace() (public, in tool_execution next to the resolvers) rejects non-directories and sensitive roots before the path is ever bound; chat_routes uses it instead of its inline isdir check. * fix(workspace): reject filesystem roots and stop showing rejected workspaces as active Review findings from #3665: P2: vet_workspace accepted / (and would accept drive/UNC roots), which makes every absolute path 'inside' the workspace and collapses confinement into host-wide file access. A root is its own dirname, so reject when dirname(resolved) == resolved; the browse response now carries a selectable flag and the picker disables 'Use this folder' on unselectable dirs. P3: /workspace set stored any string client-side and the chat route silently dropped rejected values, so the pill could claim a confinement that was not in effect. New admin-gated /api/workspace/vet validates manual paths before they persist (canonical path returned), and when a posted workspace is rejected at send time the stream emits workspace_rejected so the client clears the stored value and toasts instead of continuing silently. * fix(workspace): check caller privilege before vetting the posted workspace Review finding: /api/chat_stream called vet_workspace() on the posted value for every caller and emitted workspace_rejected on failure, so a non-admin who can chat but cannot use file/shell tools could distinguish existing directories from missing/file/sensitive/root paths by whether the event appeared. The resolution now lives in _resolve_request_workspace, which drops the submitted value uniformly for non-admin callers, with no vetting and no event, before the path ever touches the filesystem. Admin and single-user behavior is unchanged. Test pins that valid and invalid paths are indistinguishable for a non-admin and that vet_workspace is never invoked for them.
137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
"""
|
|
agent_tools.py — Facade module.
|
|
|
|
Re-exports tool parsing, schemas, execution, and implementations
|
|
for backward compatibility. All importers continue to work unchanged.
|
|
|
|
Sub-modules:
|
|
- tool_parsing.py: regex patterns, parse/strip functions
|
|
- tool_schemas.py: FUNCTION_TOOL_SCHEMAS, function_call_to_tool_block
|
|
- tool_execution.py: execute_tool_block, format_tool_result, MCP helpers
|
|
- tool_implementations.py: all do_* tool functions
|
|
"""
|
|
|
|
import logging
|
|
from collections import namedtuple
|
|
|
|
from src.tool_utils import _truncate, get_mcp_manager, set_mcp_manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from .subprocess_tools import BashTool, PythonTool
|
|
from .web_tools import WebSearchTool, WebFetchTool
|
|
from .filesystem_tools import ReadFileTool, WriteFileTool, EditFileTool, LsTool, GlobTool, GrepTool, GetWorkspaceTool
|
|
from .document_tools import CreateDocumentTool, UpdateDocumentTool, EditDocumentTool, SuggestDocumentTool, ManageDocumentTool
|
|
|
|
TOOL_HANDLERS = {
|
|
"bash": BashTool().execute,
|
|
"python": PythonTool().execute,
|
|
"web_search": WebSearchTool().execute,
|
|
"web_fetch": WebFetchTool().execute,
|
|
"read_file": ReadFileTool().execute,
|
|
"write_file": WriteFileTool().execute,
|
|
"edit_file": EditFileTool().execute,
|
|
"ls": LsTool().execute,
|
|
"glob": GlobTool().execute,
|
|
"grep": GrepTool().execute,
|
|
"create_document": CreateDocumentTool().execute,
|
|
"update_document": UpdateDocumentTool().execute,
|
|
"edit_document": EditDocumentTool().execute,
|
|
"suggest_document": SuggestDocumentTool().execute,
|
|
"manage_documents": ManageDocumentTool().execute,
|
|
"get_workspace": GetWorkspaceTool().execute,
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants (re-exported for backward compatibility — single source of truth
|
|
# is src.constants; always prefer importing from there for new code)
|
|
# ---------------------------------------------------------------------------
|
|
MAX_AGENT_ROUNDS = 50
|
|
SHELL_TIMEOUT = 60
|
|
PYTHON_TIMEOUT = 30
|
|
|
|
# Tool types that trigger execution
|
|
TOOL_TAGS = {"bash", "python", "web_search", "web_fetch", "read_file", "write_file", "edit_file",
|
|
"grep", "glob", "ls", "get_workspace",
|
|
"create_document", "update_document", "edit_document",
|
|
"search_chats",
|
|
"chat_with_model", "create_session", "list_sessions",
|
|
"send_to_session",
|
|
"pipeline",
|
|
"manage_session", "manage_memory", "list_models",
|
|
"ui_control", "generate_image", "ask_user", "update_plan",
|
|
"manage_tasks", "api_call", "ask_teacher", "manage_skills",
|
|
"suggest_document",
|
|
"manage_endpoints", "manage_mcp", "manage_webhooks",
|
|
"manage_tokens", "manage_documents", "manage_settings",
|
|
"manage_notes", "manage_calendar",
|
|
"resolve_contact", "manage_contact", "list_email_accounts", "send_email", "list_emails",
|
|
"read_email", "reply_to_email", "bulk_email", "archive_email",
|
|
"delete_email", "mark_email_read",
|
|
# Cookbook tools (LLM serving + downloads). Without these
|
|
# entries, native function calls to e.g. list_served_models
|
|
# are rejected as "Unknown function call" before reaching
|
|
# the dispatcher — silent failure for the whole cookbook
|
|
# surface.
|
|
"download_model", "serve_model",
|
|
"list_served_models", "stop_served_model",
|
|
"list_downloads", "cancel_download",
|
|
"search_hf_models", "list_cached_models",
|
|
"list_serve_presets", "serve_preset", "adopt_served_model",
|
|
"list_cookbook_servers",
|
|
# Other tools the agent reaches for that were also missing.
|
|
"edit_image", "trigger_research", "manage_research",
|
|
# Generic loopback to any UI-button endpoint (cookbook,
|
|
# gallery, email folders, etc.) — agent uses this when
|
|
# there's no named tool wrapper for the action.
|
|
"app_api"}
|
|
|
|
ToolBlock = namedtuple("ToolBlock", ["tool_type", "content"])
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Re-exports from sub-modules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Parsing
|
|
from src.tool_parsing import ( # noqa: E402, F401
|
|
parse_tool_blocks,
|
|
strip_tool_blocks,
|
|
_TOOL_NAME_MAP,
|
|
_TOOL_BLOCK_RE,
|
|
_TOOL_CALL_RE,
|
|
_XML_TOOL_CALL_RE,
|
|
_XML_INVOKE_RE,
|
|
_XML_PARAM_RE,
|
|
)
|
|
|
|
# Schemas
|
|
from src.tool_schemas import ( # noqa: E402, F401
|
|
FUNCTION_TOOL_SCHEMAS,
|
|
function_call_to_tool_block,
|
|
)
|
|
|
|
# Execution
|
|
from src.tool_execution import ( # noqa: E402, F401
|
|
execute_tool_block,
|
|
format_tool_result,
|
|
)
|
|
|
|
# Document functions
|
|
from .document_tools import (
|
|
set_active_document,
|
|
set_active_model
|
|
)
|
|
|
|
# Implementations
|
|
from src.tool_implementations import ( # noqa: E402, F401
|
|
do_search_chats,
|
|
do_manage_skills,
|
|
do_manage_tasks,
|
|
do_manage_endpoints,
|
|
do_manage_mcp,
|
|
do_manage_webhooks,
|
|
do_manage_tokens,
|
|
do_manage_settings,
|
|
do_api_call,
|
|
)
|