package gateway // This file owns the outbound WS frame role gate (design/11 §7.1 「WS 下行帧的 // 角色门」, decided by the owner on 2026-10-03). It is the ONE place that decides // which connections a downstream frame may reach: // // - a frame whose type is listed in frameReachesAll is a 「状态机信号」: it // carries no content, so every authenticated connection receives it; // - EVERY other frame type — reply text (message.*), thinking, tool calls and // results, persisted/edited/deleted messages, graph updates, sub-agent and // task traffic, private persona/device state — goes to owner + admin only. // // The classification is fail-closed: a new frame type answers false until someone // deliberately registers it here AND in design/11 §7.1, so forgetting to classify // a type can never leak content. No call site decides visibility for itself (a // per-call-site flag is exactly how one missed spot becomes a leak), which is why // hub.broadcast looks the class up from the frame it was handed. // // This gate decides who is PUSHED to. Data visibility stays with the REST gates: // a viewer still reads its 12 read-only monitoring endpoints through viewerAuth // (design/11 §7.1 「语义澄清」). // frameReachesAll reports whether a downstream frame type may be delivered to // every authenticated connection. Only the 「状态机信号」 of the design/11 §7.1 // whitelist qualify: session_busy / pending_ack / pending_drain / session_idle / // session_interrupted / pending_full / session_read. Each one only reports that a // session changed busy state, so a read-only monitor can follow the conversation's // activity without receiving the conversation itself. // // Widening this list is a contract change: register the reason (「不含内容」) in // design/11 §7.1 and mirror it in ws_frame_scope_test.go first. func frameReachesAll(typ string) bool { switch typ { case EventSessionBusy, EventPendingAck, EventPendingDrain, EventSessionIdle, EventSessionInterrupt, EventPendingFull, wsSessionRead: return true default: return false } }