mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 16:02:20 +02:00
Squash Odysseus development history
This commit is contained in:
+147
-17
@@ -8,6 +8,8 @@ run-local integrity gates before dispatch.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -99,7 +101,7 @@ _register(
|
||||
result_integrity=ResultIntegrity.WORKSPACE_UNTRUSTED,
|
||||
)
|
||||
_register(
|
||||
{"web_search"},
|
||||
{"private_browser", "web_search", "youtube_tool"},
|
||||
ToolEffect.BROKERED_NETWORK_READ,
|
||||
result_integrity=ResultIntegrity.EXTERNAL_UNTRUSTED,
|
||||
)
|
||||
@@ -109,11 +111,22 @@ _register(
|
||||
ToolEffect.NETWORK_EGRESS,
|
||||
result_integrity=ResultIntegrity.EXTERNAL_UNTRUSTED,
|
||||
)
|
||||
_register(
|
||||
{"pdf_extract"},
|
||||
ToolEffect.BROKERED_NETWORK_READ,
|
||||
result_integrity=ResultIntegrity.EXTERNAL_UNTRUSTED,
|
||||
)
|
||||
_register(
|
||||
{"inspect_media", "extract_text", "transcribe_media"},
|
||||
ToolEffect.READ_WORKSPACE,
|
||||
result_integrity=ResultIntegrity.WORKSPACE_UNTRUSTED,
|
||||
)
|
||||
_register(
|
||||
{
|
||||
"list_email_accounts",
|
||||
"list_emails",
|
||||
"read_email",
|
||||
"scan_spam",
|
||||
"resolve_contact",
|
||||
"scan_email_unsubscribes",
|
||||
"search_chats",
|
||||
@@ -127,7 +140,7 @@ _register(
|
||||
result_integrity=ResultIntegrity.EXTERNAL_UNTRUSTED,
|
||||
)
|
||||
_register(
|
||||
{"bash", "manage_bg_jobs", "python"},
|
||||
{"bash", "host_shell", "manage_bg_jobs", "python"},
|
||||
ToolEffect.EXECUTE_CODE,
|
||||
result_integrity=ResultIntegrity.WORKSPACE_UNTRUSTED,
|
||||
)
|
||||
@@ -206,7 +219,9 @@ _register(
|
||||
_register(
|
||||
{
|
||||
"archive_email",
|
||||
"block_sender",
|
||||
"bulk_email",
|
||||
"manage_email_state",
|
||||
"mark_email_read",
|
||||
"reply_to_email",
|
||||
"send_email",
|
||||
@@ -287,6 +302,8 @@ _BROWSER_MCP_READ_CAPABILITIES = _capabilities(
|
||||
)
|
||||
_BROWSER_MCP_READ_TOOLS = frozenset(
|
||||
{
|
||||
"private_browser",
|
||||
"youtube_tool",
|
||||
"mcp__builtin_browser__browser_console_messages",
|
||||
"mcp__builtin_browser__browser_network_requests",
|
||||
"mcp__builtin_browser__browser_snapshot",
|
||||
@@ -315,7 +332,7 @@ def capabilities_for_tool(tool_name: Any) -> ToolCapabilities:
|
||||
_PRIVATE_ACTION_READS: Mapping[str, frozenset[str]] = MappingProxyType(
|
||||
{
|
||||
"manage_calendar": frozenset({"list_calendars", "list_events"}),
|
||||
"manage_contact": frozenset({"list"}),
|
||||
"manage_contact": frozenset({"list", "search", "find"}),
|
||||
"manage_documents": frozenset({"list", "read", "view", "open", "get"}),
|
||||
"manage_memory": frozenset({"list", "search"}),
|
||||
"manage_notes": frozenset({"list", "search", "find", "view"}),
|
||||
@@ -323,6 +340,7 @@ _PRIVATE_ACTION_READS: Mapping[str, frozenset[str]] = MappingProxyType(
|
||||
"manage_session": frozenset({"list", "switch", "open", "select", "view"}),
|
||||
"manage_skills": frozenset({"list", "index", "view", "view_ref", "search"}),
|
||||
"manage_tasks": frozenset({"list"}),
|
||||
"manage_email_state": frozenset({"list_blocked"}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -350,6 +368,18 @@ _PRIVATE_ACTION_WRITES: Mapping[str, frozenset[str]] = MappingProxyType(
|
||||
),
|
||||
"manage_skills": frozenset({"add", "edit", "patch", "publish", "delete"}),
|
||||
"manage_tasks": frozenset({"create", "edit", "delete", "pause", "resume", "run"}),
|
||||
"manage_email_state": frozenset(
|
||||
{
|
||||
"favorite",
|
||||
"unfavorite",
|
||||
"mark_read",
|
||||
"mark_unread",
|
||||
"mark_done",
|
||||
"mark_undone",
|
||||
"unarchive",
|
||||
"unblock_sender",
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -459,6 +489,40 @@ def capabilities_for_action(tool_name: Any, content: Any) -> ToolCapabilities:
|
||||
if not isinstance(tool_name, str):
|
||||
return base
|
||||
|
||||
if tool_name == "extract_text":
|
||||
payload = content
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
payload = json.loads(payload)
|
||||
except (TypeError, ValueError):
|
||||
payload = None
|
||||
if isinstance(payload, Mapping) and re.fullmatch(
|
||||
r'odysseus://attachment/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9]+)?', str(payload.get('path') or '')
|
||||
):
|
||||
return _capabilities(ToolEffect.READ_PRIVATE,
|
||||
result_integrity=ResultIntegrity.EXTERNAL_UNTRUSTED)
|
||||
|
||||
# Media inspection is normally read-only, but its export forms create
|
||||
# workspace artifacts. Classify the concrete call instead of treating
|
||||
# every inspect_media invocation as a read; completion and security gates
|
||||
# both rely on these effects being truthful.
|
||||
if tool_name == "inspect_media":
|
||||
payload: Any = content
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
payload = json.loads(payload) if payload.strip() else {}
|
||||
except (TypeError, ValueError):
|
||||
payload = {}
|
||||
if isinstance(payload, Mapping) and any(
|
||||
payload.get(key) not in (None, "", [], {})
|
||||
for key in ("output_path", "export_path", "exports", "export")
|
||||
):
|
||||
return ToolCapabilities(
|
||||
frozenset(set(base.effects) | {ToolEffect.WRITE_WORKSPACE}),
|
||||
base.result_integrity,
|
||||
known=base.known,
|
||||
)
|
||||
|
||||
action = _action_from_content(tool_name, content)
|
||||
destructive = action in _ACTION_DESTRUCTIVE.get(tool_name, ())
|
||||
if tool_name not in _PRIVATE_ACTION_READS:
|
||||
@@ -565,6 +629,12 @@ POST_EXTERNAL_BLOCKED_EFFECTS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
TOOL_APPROVAL_GATE_ENABLED = (
|
||||
str(os.getenv("ODYSSEUS_TOOL_APPROVAL_GATE", "0")).strip().lower()
|
||||
in {"1", "true", "yes", "on"}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolGateDecision:
|
||||
allowed: bool
|
||||
@@ -581,6 +651,22 @@ _EXTERNAL_MESSAGE_SOURCES = frozenset(
|
||||
}
|
||||
)
|
||||
_EXTERNAL_MESSAGE_SOURCE_PREFIXES = ("web page:",)
|
||||
_CONTROL_PLANE_CONTEXT_SOURCES = frozenset(
|
||||
{
|
||||
"skills",
|
||||
"client runtime context",
|
||||
"backend runtime context",
|
||||
"integrations",
|
||||
"mcp tools",
|
||||
"agents.md",
|
||||
"active editor document",
|
||||
"active email reader",
|
||||
"email writing style",
|
||||
"current chat uploaded files",
|
||||
"saved memory: minimal context",
|
||||
"recent tool context",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def messages_contain_external_untrusted_context(messages: Iterable[dict]) -> bool:
|
||||
@@ -619,6 +705,10 @@ class ToolRunSecurityContext:
|
||||
external_untrusted_context_seen: bool = False
|
||||
external_sources: list[str] = field(default_factory=list)
|
||||
run_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
||||
# Request-scoped local tools explicitly authorized by a trusted execution
|
||||
# surface (for example, TUI --yolo plus its authenticated host bridge).
|
||||
# This never authorizes personal, network, or deployment-local tools.
|
||||
unattended_tools: frozenset[str] = field(default_factory=frozenset)
|
||||
# Task-scope approval sets this for the resumed in-memory run. Chat-scope
|
||||
# approval is projected from the server-owned session history marker below.
|
||||
# The bypass affects only this automatic gate; current tool policy, ownership,
|
||||
@@ -637,29 +727,69 @@ class ToolRunSecurityContext:
|
||||
for message in message_list
|
||||
):
|
||||
self.approval_gate_bypassed = True
|
||||
for message in message_list:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
metadata = message.get("metadata")
|
||||
if not isinstance(metadata, dict) or metadata.get("trusted") is not False:
|
||||
continue
|
||||
if metadata.get("tool_gate_untrusted") is not True:
|
||||
continue
|
||||
source = str(metadata.get("source") or "").strip().casefold()
|
||||
if source and source not in self.external_sources:
|
||||
self.external_sources.append(source)
|
||||
self.external_untrusted_context_seen = True
|
||||
if messages_contain_external_untrusted_context(message_list):
|
||||
self.external_untrusted_context_seen = True
|
||||
|
||||
def decision_for(self, tool_name: Any, content: Any = None) -> ToolGateDecision:
|
||||
if not TOOL_APPROVAL_GATE_ENABLED:
|
||||
return ToolGateDecision(True)
|
||||
if self.approval_gate_bypassed:
|
||||
return ToolGateDecision(True)
|
||||
if isinstance(tool_name, str) and tool_name in self.unattended_tools:
|
||||
return ToolGateDecision(True)
|
||||
if not self.external_untrusted_context_seen:
|
||||
return ToolGateDecision(True)
|
||||
capabilities = capabilities_for_action(tool_name, content)
|
||||
blocked_effects = capabilities.effects & POST_EXTERNAL_BLOCKED_EFFECTS
|
||||
if capabilities.known and not blocked_effects:
|
||||
return ToolGateDecision(True)
|
||||
effects = ", ".join(sorted(effect.value for effect in blocked_effects))
|
||||
if not capabilities.known:
|
||||
effects = "unknown/high-impact"
|
||||
return ToolGateDecision(
|
||||
False,
|
||||
(
|
||||
"External untrusted context has already influenced this run. "
|
||||
f"Tool '{tool_name}' requires a separate user-authorized action "
|
||||
f"because it can cause {effects}."
|
||||
),
|
||||
|
||||
# Skills and other server-owned descriptors are control-plane metadata
|
||||
# rather than external result content; web/document/tool-result taint
|
||||
# still gates actions that can mutate state or execute code.
|
||||
control_plane_only = bool(self.external_sources) and set(self.external_sources).issubset(
|
||||
_CONTROL_PLANE_CONTEXT_SOURCES
|
||||
)
|
||||
capabilities = capabilities_for_action(tool_name, content)
|
||||
read_only_effects = frozenset(
|
||||
{
|
||||
ToolEffect.READ_PUBLIC,
|
||||
ToolEffect.READ_WORKSPACE,
|
||||
ToolEffect.READ_PRIVATE,
|
||||
ToolEffect.BROKERED_NETWORK_READ,
|
||||
ToolEffect.USER_INTERACTION,
|
||||
}
|
||||
)
|
||||
if control_plane_only and capabilities.known and not (capabilities.effects - read_only_effects):
|
||||
return ToolGateDecision(True)
|
||||
if control_plane_only and tool_name == "manage_skills":
|
||||
try:
|
||||
action = str(json.loads(content or "{}").get("action") or "").strip().lower()
|
||||
except (TypeError, ValueError, json.JSONDecodeError, AttributeError):
|
||||
action = str(content or "").strip().splitlines()[0].lower()
|
||||
if action in {"list", "index", "search", "view", "view_ref"}:
|
||||
return ToolGateDecision(True)
|
||||
if not capabilities.known:
|
||||
return ToolGateDecision(
|
||||
False,
|
||||
"external untrusted context blocks unknown/high-impact tool",
|
||||
)
|
||||
blocked_effects = capabilities.effects & POST_EXTERNAL_BLOCKED_EFFECTS
|
||||
if blocked_effects:
|
||||
effects = ", ".join(sorted(effect.value for effect in blocked_effects))
|
||||
return ToolGateDecision(
|
||||
False,
|
||||
f"external untrusted context blocks {effects}",
|
||||
)
|
||||
return ToolGateDecision(True)
|
||||
|
||||
def observe_tool_result(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user