remove desktop notification for bypass requests
Bypass approval now flows through: 1. GUI - via event stream and banner 2. CLI - via agent loop (to be implemented) Removed: - bypass_notify.go (D-Bus notification) - BypassNotifyFunc type and all references - godbus/dbus dependency The bypass event is still published via SetBypassPending.
This commit is contained in:
parent
5882192ae9
commit
7b870443bb
|
|
@ -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("<b>%s</b>\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)
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@ type Config struct {
|
||||||
SessionsDir string
|
SessionsDir string
|
||||||
Yolo bool
|
Yolo bool
|
||||||
ModelCache *ModelCache
|
ModelCache *ModelCache
|
||||||
BypassNotify session.BypassNotifyFunc
|
|
||||||
Shutdown func()
|
Shutdown func()
|
||||||
Invalidate func()
|
Invalidate func()
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +32,6 @@ func NewRoot(cfg Config) *Tree {
|
||||||
AgentsDir: cfg.AgentsDir,
|
AgentsDir: cfg.AgentsDir,
|
||||||
SessionsDir: cfg.SessionsDir,
|
SessionsDir: cfg.SessionsDir,
|
||||||
Yolo: cfg.Yolo,
|
Yolo: cfg.Yolo,
|
||||||
BypassNotify: cfg.BypassNotify,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build the tree from the spec.
|
// Build the tree from the spec.
|
||||||
|
|
|
||||||
|
|
@ -303,9 +303,9 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
|
||||||
sess.Uname = sess.Agents()[0].ID()
|
sess.Uname = sess.Agents()[0].ID()
|
||||||
Register(sess.Name(), sess)
|
Register(sess.Name(), sess)
|
||||||
|
|
||||||
// Start bypass approval loop for restored sessions (if notify function is configured)
|
// Start bypass approval loop for restored sessions (unless paused)
|
||||||
if pkgBypassNotify != nil && !ps.Paused {
|
if !ps.Paused {
|
||||||
sess.StartBypassLoop(pkgBypassNotify)
|
sess.StartBypassLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
toolCount := 0
|
toolCount := 0
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ var (
|
||||||
pkgAgentsDir string
|
pkgAgentsDir string
|
||||||
pkgSessionsDir string
|
pkgSessionsDir string
|
||||||
pkgYolo bool
|
pkgYolo bool
|
||||||
pkgBypassNotify BypassNotifyFunc
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitConfig configures the session package.
|
// InitConfig configures the session package.
|
||||||
|
|
@ -46,7 +45,6 @@ type InitConfig struct {
|
||||||
AgentsDir string
|
AgentsDir string
|
||||||
SessionsDir string
|
SessionsDir string
|
||||||
Yolo bool
|
Yolo bool
|
||||||
BypassNotify BypassNotifyFunc // may be nil if bypass is disabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the session package with the given configuration.
|
// Init initializes the session package with the given configuration.
|
||||||
|
|
@ -57,7 +55,6 @@ func Init(cfg InitConfig) {
|
||||||
pkgAgentsDir = cfg.AgentsDir
|
pkgAgentsDir = cfg.AgentsDir
|
||||||
pkgSessionsDir = cfg.SessionsDir
|
pkgSessionsDir = cfg.SessionsDir
|
||||||
pkgYolo = cfg.Yolo
|
pkgYolo = cfg.Yolo
|
||||||
pkgBypassNotify = cfg.BypassNotify
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sessions returns a snapshot of all sessions.
|
// 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.Keeper = infra.Keeper
|
||||||
sess.SetToolsConn(infra.ToolsConn)
|
sess.SetToolsConn(infra.ToolsConn)
|
||||||
|
|
||||||
// Start bypass approval loop (if notify function is configured)
|
// Start bypass approval loop
|
||||||
if pkgBypassNotify != nil {
|
sess.StartBypassLoop()
|
||||||
sess.StartBypassLoop(pkgBypassNotify)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic check-and-insert. If another caller won the race, discard our
|
// Atomic check-and-insert. If another caller won the race, discard our
|
||||||
// freshly-built session and return theirs — still get-or-create.
|
// freshly-built session and return theirs — still get-or-create.
|
||||||
|
|
|
||||||
|
|
@ -144,17 +144,13 @@ func (s *Session) SetToolsConn(conn *toolclient.ToolsrvConn) {
|
||||||
s.toolsConn = conn
|
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
|
// StartBypassLoop starts a goroutine that reads bypass requests from toolsrv
|
||||||
// and calls notifyFn for each one. This should be called after SetToolsConn.
|
// and stores them for 9P access. The event is published via SetBypassPending.
|
||||||
func (s *Session) StartBypassLoop(notifyFn BypassNotifyFunc) {
|
func (s *Session) StartBypassLoop() {
|
||||||
go s.runBypassLoop(notifyFn)
|
go s.runBypassLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) {
|
func (s *Session) runBypassLoop() {
|
||||||
for {
|
for {
|
||||||
// Get a fresh connection for each request (blocking reads don't multiplex well)
|
// Get a fresh connection for each request (blocking reads don't multiplex well)
|
||||||
conn := s.DialToolServer()
|
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)
|
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")
|
s.log.Debug("Resume: marked active")
|
||||||
|
|
||||||
// Start bypass approval loop for the new session context.
|
// Start bypass approval loop for the new session context.
|
||||||
if pkgBypassNotify != nil {
|
s.StartBypassLoop()
|
||||||
s.StartBypassLoop(pkgBypassNotify)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild full runtimes for restored agents (they have stub runtimes from persist).
|
// Rebuild full runtimes for restored agents (they have stub runtimes from persist).
|
||||||
for _, ag := range s.agents {
|
for _, ag := range s.agents {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import (
|
||||||
"ollie/util"
|
"ollie/util"
|
||||||
|
|
||||||
"9fans.net/go/plan9/client"
|
"9fans.net/go/plan9/client"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceName = "ollie"
|
const serviceName = "ollie"
|
||||||
|
|
@ -67,11 +65,6 @@ func runServer(sockPath string) {
|
||||||
|
|
||||||
modelCache := fs.NewModelCache()
|
modelCache := fs.NewModelCache()
|
||||||
|
|
||||||
// Desktop notifications for bypass prompts
|
|
||||||
if conn, err := dbus.SessionBus(); err == nil {
|
|
||||||
initBypassNotifier(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
rootTree := fs.NewRoot(fs.Config{
|
rootTree := fs.NewRoot(fs.Config{
|
||||||
Ctx: daemonCtx,
|
Ctx: daemonCtx,
|
||||||
AgentsDir: agentsDirs[0],
|
AgentsDir: agentsDirs[0],
|
||||||
|
|
@ -80,7 +73,6 @@ func runServer(sockPath string) {
|
||||||
Sink: sink,
|
Sink: sink,
|
||||||
Yolo: *yolo,
|
Yolo: *yolo,
|
||||||
ModelCache: modelCache,
|
ModelCache: modelCache,
|
||||||
BypassNotify: notifyBypass,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create 9P server
|
// Create 9P server
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -5,7 +5,6 @@ go 1.25.6
|
||||||
require (
|
require (
|
||||||
9fans.net/go v0.0.7
|
9fans.net/go v0.0.7
|
||||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
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-grammars/tree-sitter-yaml v0.7.2
|
||||||
github.com/tree-sitter/go-tree-sitter v0.25.0
|
github.com/tree-sitter/go-tree-sitter v0.25.0
|
||||||
github.com/tree-sitter/tree-sitter-c v0.24.2
|
github.com/tree-sitter/tree-sitter-c v0.24.2
|
||||||
|
|
|
||||||
2
go.sum
2
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue