[eric] hardening: legacy pre-collapse tool ids canonicalize to core, and the docked mini gets a load listener plus a 1s belt so a silent layout shift can't strand it

This commit is contained in:
ciregenz
2026-08-08 20:48:47 -07:00
parent 16d6b5e378
commit c9840eb00b
3 changed files with 24 additions and 3 deletions
+4 -3
View File
@@ -208,12 +208,13 @@ def resolve_policy_slot(tool_name: str, tools: list[ToolDefinition]) -> PolicySl
differently. That divergence was the bug behind 'Always approve' acting like a
one-time accept: writes landed under the raw mcp__server__action name while the
gate read the parsed inner action, so the next call never saw the policy."""
# 1.7.5 sessions/logs may still carry the pre-collapse server names; they are the same builtin tools, so any legacy prefix canonicalizes to core before slot resolution.
tool_name = re.sub(
r"^mcp__openswarm-(?:browser-agent|invoke-agent|spawn-agent|skill|ui|schedule|web|mcp-meta|settings-meta|apps)__",
"mcp__openswarm-core__", tool_name)
bm = re.match(r"mcp__openswarm-core__(.+)", tool_name)
if bm:
return PolicySlot("builtin", bm.group(1), None)
im = re.match(r"mcp__openswarm-core__(.+)", tool_name)
if im:
return PolicySlot("builtin", im.group(1), None)
m = re.match(r"mcp__([^_]+(?:-[^_]+)*)__(.+)", tool_name)
if m:
server_slug, action = m.group(1), m.group(2)
+12
View File
@@ -136,3 +136,15 @@ def test_mcp_policy_survives_a_real_tool_file_reload(tmp_path, monkeypatch):
rslot = tl.resolve_policy_slot(name, tools2)
got = next(t for t in tools2 if t.id == rslot.key)
assert got.tool_permissions.get(rslot.action) == "always_allow"
def test_legacy_precollapse_ids_canonicalize_to_core():
# A 1.7.5 session/log can still name the old per-server ids; they must land in the SAME builtin slot as their openswarm-core successors, or a remembered approval silently stops matching after the upgrade.
for legacy, inner in (
("mcp__openswarm-browser-agent__BrowserAgent", "BrowserAgent"),
("mcp__openswarm-web__WebSearch", "WebSearch"),
("mcp__openswarm-settings-meta__SettingsWrite", "SettingsWrite"),
("mcp__openswarm-schedule__ScheduleWorkflow", "ScheduleWorkflow"),
):
assert resolve_policy_slot(legacy, []) == resolve_policy_slot(f"mcp__openswarm-core__{inner}", [])
assert resolve_policy_slot(legacy, []).key == inner
@@ -271,8 +271,11 @@ const BrowserCard: React.FC<Props> = ({
ro.observe(slot);
if (slot.parentElement) ro.observe(slot.parentElement);
scrollHost?.removeEventListener('scroll', onScroll);
scrollHost?.removeEventListener('load', measure, true);
scrollHost = slot.closest('[data-chat-transcript]');
scrollHost?.addEventListener('scroll', onScroll, { passive: true });
// An image/iframe finishing load ABOVE the slot shifts it with no scroll, resize, or mutation; load doesn't bubble but it does capture.
scrollHost?.addEventListener('load', measure, true);
// Streaming appends move/remount the slot without resizing or scrolling anything observed; the mutation observer is what keeps the mini glued during a live turn.
mo.disconnect();
if (scrollHost) mo.observe(scrollHost, { childList: true, subtree: true });
@@ -317,6 +320,9 @@ const BrowserCard: React.FC<Props> = ({
if ((e as CustomEvent).detail?.id === dockedTo) measure();
};
window.addEventListener('openswarm:browser-slot-mounted', onSlotMounted);
// A 1s belt bounds any move-without-event gap nobody has named yet. Only alive while a mini is
// docked, so it costs one layout read per second during active docking instead of 6.7/sec forever.
const belt = window.setInterval(measure, 1000);
return () => {
ro.disconnect();
mo.disconnect();
@@ -324,9 +330,11 @@ const BrowserCard: React.FC<Props> = ({
window.removeEventListener('openswarm:canvas-pan-changed', measure);
document.removeEventListener('visibilitychange', measure);
window.removeEventListener('openswarm:browser-slot-mounted', onSlotMounted);
scrollHost?.removeEventListener('load', measure, true);
scrollHost?.removeEventListener('scroll', onScroll);
if (scrollRaf) cancelAnimationFrame(scrollRaf);
timers.forEach((tm) => window.clearTimeout(tm));
window.clearInterval(belt);
};
// dockParentCard x/y/w/h are re-measure triggers: the slot's client rect moves with the chat card.
}, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);