fix(preview): hung-frame watchdog + require approval for guessed action calls
A runaway preview script (sync infinite loop, or a re-render loop outpacing the bootstrap's own coalescing) had nothing detecting it - the frame just spun. The bootstrap now heartbeats every second, and the parent tears the iframe down if it goes _WATCHDOG_MS silent, whatever the cause. _coerce_tool_calls recovers a tool call guessed from `content` for models with no native tool_calls field. That guess is weaker evidence than the API's own structured field - a model can land on JSON shaped like a call while only meaning to describe one - so an action tool recovered this way now always requires approval, even under the "allow" policy that lets a native tool_calls field run unattended.
This commit is contained in:
@@ -93,6 +93,52 @@ def test_ask_policy_skips_on_deny(monkeypatch):
|
||||
assert any(m["role"] == "tool" and "declined" in m["content"] for m in messages)
|
||||
|
||||
|
||||
class _ContentJsonActionManager:
|
||||
"""Small-model shape: dumps the action call into `content`, no native
|
||||
`tool_calls` field — the lower-confidence path the "allow" bypass must
|
||||
not trust."""
|
||||
def __init__(self):
|
||||
self.n = 0
|
||||
|
||||
async def chat(self, **_):
|
||||
self.n += 1
|
||||
if self.n == 1:
|
||||
return {"role": "assistant",
|
||||
"content": json.dumps({"name": "remember", "arguments": {"text": "x"}})}
|
||||
return {"role": "assistant", "content": "done"}
|
||||
|
||||
|
||||
def test_content_json_action_call_asks_even_under_allow_policy(monkeypatch):
|
||||
"""A call recovered by guessing at `content` is weaker evidence than the
|
||||
API's own structured tool_calls field — a model can land on JSON shaped
|
||||
like a call while only meaning to describe one. It must still go through
|
||||
approval even when action_tool_policy is "allow", the default that lets a
|
||||
*native* tool_calls field run unattended."""
|
||||
from synapse import chat as chatmod
|
||||
|
||||
async def fake_dispatch(name, args):
|
||||
return "saved-ok"
|
||||
monkeypatch.setattr(tools, "dispatch", fake_dispatch)
|
||||
|
||||
async def run():
|
||||
messages = [{"role": "user", "content": "remember x"}]
|
||||
schemas = tools.schemas_for(["remember"])
|
||||
gen = chatmod._run_tool_loop(_ContentJsonActionManager(), messages, "m", schemas, None, None,
|
||||
conversation_id="conv", policy="allow")
|
||||
statuses = []
|
||||
async for s in gen:
|
||||
statuses.append(s)
|
||||
if s.startswith("__approve__"):
|
||||
w = chatmod.pending_approvals["conv"]
|
||||
w["decisions"] = {"remember": True}
|
||||
w["event"].set()
|
||||
return statuses
|
||||
|
||||
statuses = asyncio.run(run())
|
||||
assert any(s.startswith("__approve__") for s in statuses)
|
||||
assert "__status__remember" in statuses
|
||||
|
||||
|
||||
def test_action_tools_gated_by_consent():
|
||||
allow = ["search_memory", "web_search", "remember", "fetch_url"]
|
||||
on = [s["function"]["name"] for s in tools.schemas_for(allow, allow_actions=True)]
|
||||
|
||||
Reference in New Issue
Block a user