Feat v0.5.0 realtime primitive + dialog audit + permissions UI
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 18s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m59s
CI/CD / build-and-deploy (pull_request) Successful in 1m24s

Add realtime pub/sub: Starlark realtime.publish() module gated by
new realtime.publish permission, WS room protocol (room.subscribe/
room.unsubscribe intercepted in readPump with 100-room cap), and
SDK sw.realtime.subscribe() with auto room join/leave and reconnect
recovery.

Migrate 5 bare confirm() calls to sw.confirm() with destructive
styling in tasks, schedules, notes (×2), and editor packages.

Add admin permissions UI: per-permission grant/revoke drawer,
Grant All bulk action, pending_review/suspended status badges,
disabled enable toggle when pending. Closes v0.4.7 permissions
UI gap.

8 new Go tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 10:40:24 +00:00
parent eb9a2d7d27
commit 0af1c51ae9
21 changed files with 680 additions and 22 deletions

View File

@@ -117,6 +117,12 @@ func TestRouteFor(t *testing.T) {
// Extension lifecycle
{"extension.loaded", DirLocal},
{"extension.error", DirLocal},
// Realtime (v0.5.0)
{"realtime.chat.message", DirToClient},
{"realtime.custom.event", DirToClient},
// Room management (v0.5.0)
{"room.subscribe", DirFromClient},
{"room.unsubscribe", DirFromClient},
}
for _, tt := range tests {
@@ -190,6 +196,27 @@ func TestWaitFor_CleansUpSubscription(t *testing.T) {
bus.Publish(Event{Label: "tool.result.cleanup", Payload: json.RawMessage(`{}`)})
}
func TestRealtimeRouting(t *testing.T) {
// realtime.* events should be sent to clients but not accepted from them
if !ShouldSendToClient("realtime.chat.message") {
t.Error("realtime.* should be sent to client")
}
if ShouldAcceptFromClient("realtime.chat.message") {
t.Error("realtime.* should NOT be accepted from client")
}
// room.subscribe/unsubscribe should be accepted from clients
if !ShouldAcceptFromClient("room.subscribe") {
t.Error("room.subscribe should be accepted from client")
}
if !ShouldAcceptFromClient("room.unsubscribe") {
t.Error("room.unsubscribe should be accepted from client")
}
if ShouldSendToClient("room.subscribe") {
t.Error("room.subscribe should NOT be sent to client")
}
}
func TestToolCallRouteToClient(t *testing.T) {
if !ShouldSendToClient("tool.call.abc123") {
t.Error("tool.call.* should be sent to client")

View File

@@ -87,6 +87,13 @@ var routeTable = map[string]Direction{
"trigger.fired": DirLocal, // Trigger invocation event (observability)
"trigger.error": DirLocal, // Trigger execution error
// Realtime (v0.5.0): extension-published events, room-scoped
"realtime.": DirToClient,
// Room management (v0.5.0): client room join/leave
"room.subscribe": DirFromClient,
"room.unsubscribe": DirFromClient,
// Heartbeat
"ping": DirFromClient,
"pong": DirToClient,

View File

@@ -316,6 +316,26 @@ func (c *Conn) readPump() {
continue
}
// v0.5.0: Room management — intercept before bus publish (like ping)
if event.Label == "room.subscribe" {
var req struct {
Room string `json:"room"`
}
if json.Unmarshal(event.Payload, &req) == nil && req.Room != "" && len(c.rooms) < 100 {
c.JoinRoom(req.Room)
}
continue
}
if event.Label == "room.unsubscribe" {
var req struct {
Room string `json:"room"`
}
if json.Unmarshal(event.Payload, &req) == nil && req.Room != "" {
c.LeaveRoom(req.Room)
}
continue
}
// Validate: only accept events allowed from clients
if !ShouldAcceptFromClient(event.Label) {
continue