mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(gateway): gate session.tool mirrors behind session subscriptions (#124732)
session.tool mirrors the raw agent tool event (full args/result snapshots) onto session subscribers, but the event name was missing from SESSION_SUBSCRIPTION_EVENTS. Clients with the session-scoped-events cap (or browser copilot) that subscribed to one session's messages therefore received every session's tool payloads through the mirror — the exact data the registry gate suppresses on the raw agent event. session.observer was added to the set for precisely this mirror-event class; session.tool carries strictly more data. Root cause: the gate is an allowlist and the newer mirror event never joined it. One set entry; the payload already carries sessionKey so registry keying works unchanged, and unscoped Control UI clients keep full fanout. Regression: new board test drives the server-chat-style targeted fanout and asserts a scoped client subscribed to a different session receives nothing — fails pre-fix.
This commit is contained in:
committed by
GitHub
parent
b7fb951a94
commit
1bcec211d9
@@ -164,6 +164,38 @@ describe("collaboration event scope guards", () => {
|
||||
expect(unsubscribed.socket.events).toEqual([]);
|
||||
});
|
||||
|
||||
it("suppresses session.tool mirrors for scoped clients without a matching subscription", () => {
|
||||
const subscribed = makeClient("subscribed", "operator", ["operator.read"]);
|
||||
const otherSession = makeClient("other-session", "operator", ["operator.read"]);
|
||||
const unscoped = makeClient("unscoped", "operator", ["operator.read"]);
|
||||
for (const entry of [subscribed, otherSession]) {
|
||||
entry.client.connect.caps = [GATEWAY_CLIENT_CAPS.SESSION_SCOPED_EVENTS];
|
||||
}
|
||||
const sessionMessageSubscribers = createSessionMessageSubscriberRegistry();
|
||||
sessionMessageSubscribers.subscribe(subscribed.client.connId, "agent:main:main");
|
||||
sessionMessageSubscribers.subscribe(otherSession.client.connId, "agent:main:other");
|
||||
const { broadcastToConnIds } = createGatewayBroadcaster({
|
||||
clients: new Set([subscribed.client, otherSession.client, unscoped.client]),
|
||||
sessionMessageSubscribers,
|
||||
});
|
||||
|
||||
// Mirrors the server-chat session.tool fanout: targeted at all session
|
||||
// subscribers, session identity carried in the payload.
|
||||
broadcastToConnIds(
|
||||
"session.tool",
|
||||
{ sessionKey: "agent:main:main", tool: { name: "exec", args: { command: "ls" } } },
|
||||
new Set(["subscribed", "other-session", "unscoped"]),
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
|
||||
expect(subscribed.socket.events).toEqual(["session.tool"]);
|
||||
// The scoped client subscribed to a different session must not receive
|
||||
// another session's tool args via the mirror event.
|
||||
expect(otherSession.socket.events).toEqual([]);
|
||||
// Unscoped Control UI clients keep full fanout.
|
||||
expect(unscoped.socket.events).toEqual(["session.tool"]);
|
||||
});
|
||||
|
||||
it("delivers prepared global observer audiences exactly once", () => {
|
||||
const main = makeClient("main", "operator", ["operator.read"]);
|
||||
const legacy = makeClient("legacy", "operator", ["operator.read"]);
|
||||
|
||||
@@ -103,6 +103,10 @@ const SESSION_SUBSCRIPTION_EVENTS = new Set([
|
||||
"chat",
|
||||
"chat.side_result",
|
||||
"session.observer",
|
||||
// Mirrors the raw agent tool event (full args/result snapshots) onto
|
||||
// session subscribers; omitting it here would hand scoped clients the
|
||||
// exact payload the registry gate suppresses on the `agent` event.
|
||||
"session.tool",
|
||||
]);
|
||||
|
||||
function serializeFrameField(name: "payload" | "stateVersion", value: unknown): string {
|
||||
|
||||
Reference in New Issue
Block a user