mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 16:16:48 +00:00
Hold resumed turns and public-link visitors to the right wiki rules
A paused turn is found by the agent owner's id, so anyone holding one of the owner's agent keys could resume the owner's own chat with its saved wiki edit rights. A resume now counts as an API or widget caller when either the saved state or the resuming request is one, cuts the wiki tool to wiki_view unless the wiki allows outside edits, and gives the tool executor the same flags. A request that names an agent, by key or id, may only resume that agent's turn; otherwise the claim is released and the request refused. Public-link visitors run as themselves and reach only wikis they may edit, so the wiki switch no longer applies to them. Instead every wiki write in a public-link run waits for the visitor's approval, so the agent owner's prompt or sources can't steer an edit to the visitor's wiki unasked.
This commit is contained in:
1 parent
91e70d2f0c
commit
051452c438
10 files changed
+405
-64
No files matched your search
@@ -23,7 +23,7 @@ MAX_WIKI_PAGE_BYTES = 1_000_000
|
||||
_WRITE_ACTIONS = frozenset({"create", "str_replace", "insert", "delete", "rename"})
|
||||
|
||||
OUTSIDE_EDITS_DENIED = (
|
||||
"Error: This wiki's owner doesn't let API, widget or public-link users edit it, "
|
||||
"Error: This wiki's owner doesn't let API or widget users edit it, "
|
||||
"so it can't be changed from here. You can still read it."
|
||||
)
|
||||
|
||||
@@ -45,8 +45,8 @@ class WikiTool(Tool):
|
||||
self.config = config
|
||||
self.source_id: Optional[str] = config.get("source_id")
|
||||
self.source_owner_id: Optional[str] = config.get("source_owner_id")
|
||||
# An API-key, widget or public-link run: it writes only while the
|
||||
# wiki's owner allows such edits.
|
||||
# An API-key or widget run (it acts as the agent's owner): it writes
|
||||
# only while the wiki's owner allows such edits.
|
||||
self.outside_caller: bool = bool(config.get("outside_caller"))
|
||||
decoded_token = config.get("decoded_token") or {}
|
||||
self.updated_by: Optional[str] = (
|
||||
@@ -249,7 +249,7 @@ class WikiTool(Tool):
|
||||
Fails closed on a missing row or a failed lookup.
|
||||
|
||||
Returns:
|
||||
bool: Whether an API, widget or public-link run may edit the wiki.
|
||||
bool: Whether an API-key or widget run may edit the wiki.
|
||||
"""
|
||||
try:
|
||||
with db_readonly() as conn:
|
||||
@@ -531,11 +531,12 @@ class WikiTool(Tool):
|
||||
return f"Renamed: {validated_old} -> {validated_new}"
|
||||
|
||||
|
||||
def build_wiki_tool_entry(writes_allowed: bool = True) -> Dict[str, Any]:
|
||||
def build_wiki_tool_entry(writes_allowed: bool = True, approval_required: bool = False) -> Dict[str, Any]:
|
||||
"""Build the synthetic tools_dict entry for the WikiTool.
|
||||
|
||||
Args:
|
||||
writes_allowed: False offers the model only ``wiki_view``.
|
||||
approval_required: Every write action waits for the caller's approval.
|
||||
"""
|
||||
entry = {"name": "wiki"}
|
||||
entry["actions"] = [
|
||||
@@ -543,6 +544,8 @@ def build_wiki_tool_entry(writes_allowed: bool = True) -> Dict[str, Any]:
|
||||
for action in _wiki_actions_metadata()
|
||||
if writes_allowed or action["name"] == "wiki_view"
|
||||
]
|
||||
if approval_required:
|
||||
_require_write_approval(entry)
|
||||
return entry
|
||||
|
||||
|
||||
@@ -550,8 +553,14 @@ def _wiki_actions_metadata() -> List[Dict[str, Any]]:
|
||||
return WikiTool().get_actions_metadata()
|
||||
|
||||
|
||||
def _require_write_approval(entry: Dict[str, Any]) -> None:
|
||||
for action in entry.get("actions") or []:
|
||||
if action.get("name") != "wiki_view":
|
||||
action["require_approval"] = True
|
||||
|
||||
|
||||
def outside_edits_allowed(source_row: Optional[Dict[str, Any]]) -> bool:
|
||||
"""Whether a wiki's owner lets API, widget and public-link runs edit it."""
|
||||
"""Whether a wiki's owner lets API-key and widget runs edit it."""
|
||||
return bool(source_row and source_row.get("wiki_outside_edits"))
|
||||
|
||||
|
||||
@@ -578,13 +587,17 @@ def add_wiki_tool(tools_dict: Dict, config: Dict) -> None:
|
||||
Mirrors ``add_internal_search_tool``: the entry carries ``id=WIKI_TOOL_ID``
|
||||
so the executor can resolve the synthetic (DB-rowless) tool, and a ``config``
|
||||
the executor copies into the loaded tool. Mutates ``tools_dict`` in place.
|
||||
``writes_allowed=False`` (an API, widget or public-link run on a wiki
|
||||
whose owner hasn't allowed their edits) offers only ``wiki_view``; the
|
||||
tool still refuses writes itself from ``outside_caller``.
|
||||
``writes_allowed=False`` (an API-key or widget run on a wiki whose owner
|
||||
hasn't allowed their edits) offers only ``wiki_view``; the tool still
|
||||
refuses writes itself from ``outside_caller``. ``approval_required`` (a
|
||||
public-link run) puts every write behind the caller's approval.
|
||||
"""
|
||||
if not config or not config.get("source_id") or not config.get("source_owner_id"):
|
||||
return
|
||||
entry = build_wiki_tool_entry(writes_allowed=config.get("writes_allowed", True) is not False)
|
||||
entry = build_wiki_tool_entry(
|
||||
writes_allowed=config.get("writes_allowed", True) is not False,
|
||||
approval_required=bool(config.get("approval_required")),
|
||||
)
|
||||
entry["id"] = WIKI_TOOL_ID
|
||||
entry["config"] = build_wiki_tool_config(
|
||||
source_id=config["source_id"],
|
||||
@@ -594,3 +607,46 @@ def add_wiki_tool(tools_dict: Dict, config: Dict) -> None:
|
||||
outside_caller=bool(config.get("outside_caller")),
|
||||
)
|
||||
tools_dict[WIKI_TOOL_ID] = entry
|
||||
|
||||
|
||||
def apply_resume_caller_rules(
|
||||
tools_dict: Dict, *, outside_caller: bool, public_link_caller: bool
|
||||
) -> None:
|
||||
"""Hold a resumed run's saved WikiTool entry to its caller's rules.
|
||||
|
||||
A paused turn is resumed by whoever sends its tool actions, which may not
|
||||
be who started it: a widget key can resume the owner's own chat. So the
|
||||
saved entry is tightened here rather than trusted. An API-key or widget
|
||||
run gets ``outside_caller`` and, unless the wiki's owner allows their
|
||||
edits (read live), only ``wiki_view``; a public-link run approves every
|
||||
write. Never loosens an entry. Mutates ``tools_dict`` in place.
|
||||
|
||||
Args:
|
||||
tools_dict: The resumed run's tools.
|
||||
outside_caller: The saved state or the resuming request is an
|
||||
API-key or widget caller.
|
||||
public_link_caller: The saved state or the resuming request reached
|
||||
the agent through its public link.
|
||||
"""
|
||||
entry = tools_dict.get(WIKI_TOOL_ID) if isinstance(tools_dict, dict) else None
|
||||
if not isinstance(entry, dict):
|
||||
return
|
||||
if public_link_caller:
|
||||
_require_write_approval(entry)
|
||||
if not outside_caller:
|
||||
return
|
||||
config = entry.get("config")
|
||||
if not isinstance(config, dict):
|
||||
config = {}
|
||||
entry["config"] = config
|
||||
config["outside_caller"] = True
|
||||
allowed = False
|
||||
source_id = config.get("source_id")
|
||||
if source_id:
|
||||
try:
|
||||
with db_readonly() as conn:
|
||||
allowed = outside_edits_allowed(SourcesRepository(conn).get_by_id(str(source_id)))
|
||||
except Exception:
|
||||
logger.exception("Wiki outside-edits check failed for source %s", source_id)
|
||||
if not allowed:
|
||||
entry["actions"] = [a for a in entry.get("actions") or [] if a.get("name") == "wiki_view"]
|
||||
Reference in new issue
Block a user