diff --git a/cmd/olliesrv/bypass_notify.go b/cmd/olliesrv/bypass_notify.go deleted file mode 100644 index 406975f..0000000 --- a/cmd/olliesrv/bypass_notify.go +++ /dev/null @@ -1,177 +0,0 @@ -package main - -import ( - "fmt" - "sync" - - toolclient "ollie/cmd/olliesrv/internal/toolclient" - "ollie/toolsrv/protocol" - - "github.com/godbus/dbus/v5" -) - -// bypassNotifier handles desktop notifications for bypass requests. -// When a notification action is clicked, it writes the resolution directly to toolsrvclient. -type bypassNotifier struct { - conn *dbus.Conn - - mu sync.Mutex - notifID map[uint32]*pendingRequest // notification ID -> request info -} - -type pendingRequest struct { - id string - session *sessionRef -} - -type sessionRef struct { - dialFn func() *toolclient.ToolsrvConn -} - -var notifier *bypassNotifier - -// initBypassNotifier sets up the notification action listener. -func initBypassNotifier(conn *dbus.Conn) { - if conn == nil { - return - } - notifier = &bypassNotifier{ - conn: conn, - notifID: make(map[uint32]*pendingRequest), - } - - // Listen for ActionInvoked signals from the notification daemon - conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='org.freedesktop.Notifications',member='ActionInvoked'") //nolint:errcheck - conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='org.freedesktop.Notifications',member='NotificationClosed'") //nolint:errcheck - - ch := make(chan *dbus.Signal, 32) - conn.Signal(ch) - go notifier.listenSignals(ch) -} - -// notifyBypass sends a desktop notification for a bypass request. -// When the user responds, it writes the resolution directly to toolsrvclient. -func notifyBypass(req *protocol.BypassRequest, sessionID string, dialFn func() *toolclient.ToolsrvConn) { - if notifier == nil { - return - } - notifier.sendNotification(req, sessionID, dialFn) -} - -func (n *bypassNotifier) sendNotification(req *protocol.BypassRequest, sessionID string, dialFn func() *toolclient.ToolsrvConn) { - obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") - - summary := "Elevation Request" - body := fmt.Sprintf("%s\ncwd: %s", escapeMarkup(req.Cmd), escapeMarkup(req.Cwd)) - if sessionID != "" { - body = fmt.Sprintf("session: %s\n%s", escapeMarkup(sessionID), body) - } - - actions := []string{ - "approve", "Approve", - "deny", "Deny", - } - - hints := map[string]dbus.Variant{ - "urgency": dbus.MakeVariant(byte(2)), // critical - } - - call := obj.Call("org.freedesktop.Notifications.Notify", 0, - "ollie", // app_name - uint32(0), // replaces_id - "dialog-warning", // icon - summary, - body, - actions, - hints, - int32(300000), // timeout ms (5 minutes) - ) - if call.Err != nil { - return - } - - var notifID uint32 - call.Store(¬ifID) - - n.mu.Lock() - n.notifID[notifID] = &pendingRequest{ - id: req.ID, - session: &sessionRef{dialFn: dialFn}, - } - n.mu.Unlock() -} - -func (n *bypassNotifier) listenSignals(ch chan *dbus.Signal) { - for sig := range ch { - switch sig.Name { - case "org.freedesktop.Notifications.ActionInvoked": - if len(sig.Body) < 2 { - continue - } - nid, _ := sig.Body[0].(uint32) - action, _ := sig.Body[1].(string) - - n.mu.Lock() - pr, ok := n.notifID[nid] - delete(n.notifID, nid) - n.mu.Unlock() - - if !ok || pr == nil { - continue - } - - approved := action == "approve" - go n.resolve(pr, approved) - - case "org.freedesktop.Notifications.NotificationClosed": - if len(sig.Body) < 2 { - continue - } - nid, _ := sig.Body[0].(uint32) - reason, _ := sig.Body[1].(uint32) - - n.mu.Lock() - pr, ok := n.notifID[nid] - delete(n.notifID, nid) - n.mu.Unlock() - - if !ok || pr == nil { - continue - } - - // reason 2 = dismissed by user, 1 = expired - // Treat dismiss/expire as deny - if reason == 1 || reason == 2 { - go n.resolve(pr, false) - } - } - } -} - -func (n *bypassNotifier) resolve(pr *pendingRequest, approved bool) { - conn := pr.session.dialFn() - if conn == nil { - return - } - conn.ResolveBypass(pr.id, approved, "") //nolint:errcheck - conn.Close() -} - -func escapeMarkup(s string) string { - var out []byte - for _, c := range []byte(s) { - switch c { - case '&': - out = append(out, []byte("&")...) - case '<': - out = append(out, []byte("<")...) - case '>': - out = append(out, []byte(">")...) - default: - out = append(out, c) - } - } - return string(out) -} diff --git a/cmd/olliesrv/internal/fs/newroot.go b/cmd/olliesrv/internal/fs/newroot.go index 044d360..5788f1c 100644 --- a/cmd/olliesrv/internal/fs/newroot.go +++ b/cmd/olliesrv/internal/fs/newroot.go @@ -11,29 +11,27 @@ import ( // Config carries all runtime dependencies for the filesystem tree. // It is the single input to NewRoot. type Config struct { - Ctx context.Context - Log *olog.Logger - Sink *olog.Sink - AgentsDir string - SessionsDir string - Yolo bool - ModelCache *ModelCache - BypassNotify session.BypassNotifyFunc - Shutdown func() - Invalidate func() + Ctx context.Context + Log *olog.Logger + Sink *olog.Sink + AgentsDir string + SessionsDir string + Yolo bool + ModelCache *ModelCache + Shutdown func() + Invalidate func() } // NewRoot creates the complete 9P filesystem tree from the spec. func NewRoot(cfg Config) *Tree { // Initialize session package session.Init(session.InitConfig{ - Ctx: cfg.Ctx, - Log: cfg.Log, - Sink: cfg.Sink, - AgentsDir: cfg.AgentsDir, - SessionsDir: cfg.SessionsDir, - Yolo: cfg.Yolo, - BypassNotify: cfg.BypassNotify, + Ctx: cfg.Ctx, + Log: cfg.Log, + Sink: cfg.Sink, + AgentsDir: cfg.AgentsDir, + SessionsDir: cfg.SessionsDir, + Yolo: cfg.Yolo, }) // Build the tree from the spec. diff --git a/cmd/olliesrv/internal/session/persist.go b/cmd/olliesrv/internal/session/persist.go index 4743d85..63c5072 100644 --- a/cmd/olliesrv/internal/session/persist.go +++ b/cmd/olliesrv/internal/session/persist.go @@ -303,9 +303,9 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) { sess.Uname = sess.Agents()[0].ID() Register(sess.Name(), sess) - // Start bypass approval loop for restored sessions (if notify function is configured) - if pkgBypassNotify != nil && !ps.Paused { - sess.StartBypassLoop(pkgBypassNotify) + // Start bypass approval loop for restored sessions (unless paused) + if !ps.Paused { + sess.StartBypassLoop() } toolCount := 0 diff --git a/cmd/olliesrv/internal/session/registry.go b/cmd/olliesrv/internal/session/registry.go index 3359611..60233b6 100644 --- a/cmd/olliesrv/internal/session/registry.go +++ b/cmd/olliesrv/internal/session/registry.go @@ -29,24 +29,22 @@ var ( sessions = make(map[string]*Session) // Package config, set via Init. - serverCtx context.Context - pkgLog *olog.Logger - pkgSink *olog.Sink - pkgAgentsDir string - pkgSessionsDir string - pkgYolo bool - pkgBypassNotify BypassNotifyFunc + serverCtx context.Context + pkgLog *olog.Logger + pkgSink *olog.Sink + pkgAgentsDir string + pkgSessionsDir string + pkgYolo bool ) // InitConfig configures the session package. type InitConfig struct { - Ctx context.Context - Log *olog.Logger - Sink *olog.Sink - AgentsDir string - SessionsDir string - Yolo bool - BypassNotify BypassNotifyFunc // may be nil if bypass is disabled + Ctx context.Context + Log *olog.Logger + Sink *olog.Sink + AgentsDir string + SessionsDir string + Yolo bool } // Init initializes the session package with the given configuration. @@ -57,7 +55,6 @@ func Init(cfg InitConfig) { pkgAgentsDir = cfg.AgentsDir pkgSessionsDir = cfg.SessionsDir pkgYolo = cfg.Yolo - pkgBypassNotify = cfg.BypassNotify } // Sessions returns a snapshot of all sessions. @@ -349,10 +346,8 @@ func CreateEmpty(name, remote string, yolo ...bool) (sess *Session, created bool sess.Keeper = infra.Keeper sess.SetToolsConn(infra.ToolsConn) - // Start bypass approval loop (if notify function is configured) - if pkgBypassNotify != nil { - sess.StartBypassLoop(pkgBypassNotify) - } + // Start bypass approval loop + sess.StartBypassLoop() // Atomic check-and-insert. If another caller won the race, discard our // freshly-built session and return theirs — still get-or-create. diff --git a/cmd/olliesrv/internal/session/session.go b/cmd/olliesrv/internal/session/session.go index 455151a..01919f5 100644 --- a/cmd/olliesrv/internal/session/session.go +++ b/cmd/olliesrv/internal/session/session.go @@ -144,17 +144,13 @@ func (s *Session) SetToolsConn(conn *toolclient.ToolsrvConn) { s.toolsConn = conn } -// BypassNotifyFunc is called when a bypass request arrives. -// It receives the request, session ID, and a dial function to get a connection for resolution. -type BypassNotifyFunc func(req *protocol.BypassRequest, sessionID string, dialFn func() *toolclient.ToolsrvConn) - // StartBypassLoop starts a goroutine that reads bypass requests from toolsrv -// and calls notifyFn for each one. This should be called after SetToolsConn. -func (s *Session) StartBypassLoop(notifyFn BypassNotifyFunc) { - go s.runBypassLoop(notifyFn) +// and stores them for 9P access. The event is published via SetBypassPending. +func (s *Session) StartBypassLoop() { + go s.runBypassLoop() } -func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) { +func (s *Session) runBypassLoop() { for { // Get a fresh connection for each request (blocking reads don't multiplex well) conn := s.DialToolServer() @@ -184,13 +180,8 @@ func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) { } } - // Store the pending request for 9P access + // Store the pending request for 9P access and publish event s.SetBypassPending((*protocol.BypassRequest)(req), s.DialToolServer) - - // Notify (non-blocking) - the notifier will write resolution when user responds - if notifyFn != nil { - notifyFn((*protocol.BypassRequest)(req), s.ID, s.DialToolServer) - } } } @@ -532,9 +523,7 @@ func (s *Session) Resume() error { s.log.Debug("Resume: marked active") // Start bypass approval loop for the new session context. - if pkgBypassNotify != nil { - s.StartBypassLoop(pkgBypassNotify) - } + s.StartBypassLoop() // Rebuild full runtimes for restored agents (they have stub runtimes from persist). for _, ag := range s.agents { diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index f35748e..fc4ada6 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -17,8 +17,6 @@ import ( "ollie/util" "9fans.net/go/plan9/client" - - "github.com/godbus/dbus/v5" ) const serviceName = "ollie" @@ -67,20 +65,14 @@ func runServer(sockPath string) { modelCache := fs.NewModelCache() - // Desktop notifications for bypass prompts - if conn, err := dbus.SessionBus(); err == nil { - initBypassNotifier(conn) - } - rootTree := fs.NewRoot(fs.Config{ - Ctx: daemonCtx, - AgentsDir: agentsDirs[0], - SessionsDir: sessionsDir, - Log: sink.NewLogger("9p"), - Sink: sink, - Yolo: *yolo, - ModelCache: modelCache, - BypassNotify: notifyBypass, + Ctx: daemonCtx, + AgentsDir: agentsDirs[0], + SessionsDir: sessionsDir, + Log: sink.NewLogger("9p"), + Sink: sink, + Yolo: *yolo, + ModelCache: modelCache, }) // Create 9P server diff --git a/go.mod b/go.mod index d7a1ff2..ed293e5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.25.6 require ( 9fans.net/go v0.0.7 github.com/JohannesKaufmann/html-to-markdown v1.6.0 - github.com/godbus/dbus/v5 v5.1.0 github.com/tree-sitter-grammars/tree-sitter-yaml v0.7.2 github.com/tree-sitter/go-tree-sitter v0.25.0 github.com/tree-sitter/tree-sitter-c v0.24.2 diff --git a/go.sum b/go.sum index ce5e762..9d3f0eb 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=