// olliesrv - 9P server for ollie sessions package main import ( "context" "flag" "fmt" "net" "os" "os/signal" "path/filepath" "strings" "sync" "syscall" "9fans.net/go/plan9/client" "ollie/pkg/agent" "ollie/pkg/backend" "ollie/pkg/elevate" "ollie/pkg/env" olog "ollie/pkg/log" "ollie/pkg/paths" "ollie/pkg/tools/execute" fs "olliesrv/fs" "olliesrv/mount" "olliesrv/server" "olliesrv/session" ) const serviceName = "ollie" var tcpAddr = flag.String("tcp", "", "also listen on TCP address (e.g. :564)") var strict = flag.Bool("strict", false, "only allow tool steps; reject inline code") var yolo = flag.Bool("yolo", false, "skip landrun sandbox for execute_code") var nodbus = flag.Bool("nodbus", false, "disable D-Bus adapter (for headless/container use)") var no9p = flag.Bool("no9p", false, "disable 9P listener (D-Bus only mode)") // ModelCache caches the list of models from all backends. type ModelCache struct { mu sync.RWMutex data []byte fetched bool } var modelCache = &ModelCache{} // Get returns the cached models, fetching if not yet cached. func (c *ModelCache) Get() []byte { c.mu.RLock() if c.fetched { data := c.data c.mu.RUnlock() return data } c.mu.RUnlock() return c.Refresh() } // Refresh fetches models from all backends and updates the cache. func (c *ModelCache) Refresh() []byte { var sb strings.Builder for _, name := range backend.Backends() { b, err := backend.NewWithName(name) if err != nil { continue } for _, m := range b.Models(context.Background()) { sb.WriteString(name) sb.WriteByte('\t') sb.WriteString(m) sb.WriteByte('\n') } } data := []byte(sb.String()) c.mu.Lock() c.data = data c.fetched = true c.mu.Unlock() return data } // Invalidate clears the cache, forcing a refresh on next Get. func (c *ModelCache) Invalidate() { c.mu.Lock() c.fetched = false c.data = nil c.mu.Unlock() } func main() { flag.CommandLine.Parse(os.Args[1:]) //nolint:errcheck // Handle mount subcommand (kept as a convenience) if flag.NArg() > 0 && flag.Arg(0) == "mount" { cmdMount() return } ns := client.Namespace() if ns == "" { fmt.Fprintln(os.Stderr, "no namespace") os.Exit(1) } sockPath := filepath.Join(ns, serviceName) runServer(sockPath) } func cmdMount() { if flag.NArg() < 1 { fmt.Fprintln(os.Stderr, "usage: olliesrv mount
[mountpoint]") os.Exit(1) } addr := flag.Arg(0) mnt := flag.Arg(1) if mnt == "" { home, _ := os.UserHomeDir() mnt = filepath.Join(home, "mnt", addr) } if err := os.MkdirAll(mnt, fs.PermMkdir); err != nil { fmt.Fprintf(os.Stderr, "cannot create mount dir: %v\n", err) os.Exit(1) } network := "unix" if strings.Contains(addr, ":") { network = "tcp" } fmt.Printf("mounting %s at %s\n", addr, mnt) if err := mount.Run(mount.Config{ Network: network, Address: addr, Mountpoint: mnt, }); err != nil { fmt.Fprintf(os.Stderr, "mount: %v\n", err) os.Exit(1) } } func runServer(sockPath string) { env.EnsureDefaults() // -no9p requires D-Bus (otherwise there's no interface at all) if *no9p && *nodbus { fmt.Fprintln(os.Stderr, "error: -no9p and -nodbus are mutually exclusive") os.Exit(1) } // Remove stale socket if !*no9p { if _, err := os.Stat(sockPath); err == nil { os.Remove(sockPath) //nolint:errcheck } } // Write PID file sink := olog.NewSink(os.Stdout, os.Stderr, olog.ParseLevel(os.Getenv("OLLIE_LOG"), olog.LevelWarn)) agentsDirs := agent.AgentsDirs() sessionsDir := paths.DataDir() + "/sessions" // D-Bus adapter (initialized after manager so callbacks can reference it). var dbusAdapter *DBusAdapter mgr := session.NewManager(session.ManagerConfig{ AgentsDir: agentsDirs[0], SessionsDir: sessionsDir, Log: sink.NewLogger("9p"), Sink: sink, Strict: *strict, Yolo: *yolo, NoMount: *no9p || *tcpAddr != "", Enable9P: !*no9p, EnableDBus: !*nodbus && (*no9p || *tcpAddr == ""), InvalidateModels: modelCache.Invalidate, OnSessionCreated: func(id string, sess *session.Session) { if dbusAdapter != nil { dbusAdapter.OnSessionCreated(id, sess) } }, OnSessionKilled: func(id string) { if dbusAdapter != nil { dbusAdapter.OnSessionKilled(id) } }, OnSessionRenamed: func(oldID, newID string) { if dbusAdapter != nil { dbusAdapter.OnSessionRenamed(oldID, newID) } }, }) var srv *server.Server // Server creation deferred until after elevate broker is ready (see below) // Start D-Bus adapter. // -tcp implies -nodbus (remote/headless deployments have no session bus). // -no9p forces D-Bus on (it's the only interface). if !*nodbus && (*no9p || *tcpAddr == "") { dbusAdapter = startDBus(mgr) if dbusAdapter != nil { dbusAdapter.WatchExisting() } if *no9p && dbusAdapter == nil { fmt.Fprintln(os.Stderr, "error: -no9p mode requires D-Bus but session bus is unavailable") os.Exit(1) } } // Start elevation broker elevateSocket := os.Getenv("OLLIE_ELEVATE_SOCKET") if elevateSocket == "" { xdg := os.Getenv("XDG_RUNTIME_DIR") if xdg == "" { xdg = fmt.Sprintf("/run/user/%d", os.Getuid()) } elevateSocket = filepath.Join(xdg, "ollie", "elevate.sock") } policyPath := filepath.Join(paths.DataDir(), "elevate-policy.yaml") var elevateBroker *elevate.Broker { notifyFn := func(req *elevate.Request) { if dbusAdapter != nil { dbusAdapter.NotifyElevation(req) } } var err error elevateBroker, err = elevate.NewBroker(elevate.BrokerConfig{ SocketPath: elevateSocket, PolicyPath: policyPath, Notify: notifyFn, Logf: func(f string, a ...any) { fmt.Printf(f+"\n", a...) }, }) if err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) } else { defer elevateBroker.Close() if dbusAdapter != nil { initElevateNotifier(dbusAdapter.conn, elevateBroker) } } } // Create 9P server (after broker is ready so it can be passed in) if !*no9p { srv = server.New(server.Config{ Sink: sink, SessionMgr: mgr, RootStore: NewRootStore(), ElevateBroker: elevateBroker, InvalidateModels: modelCache.Invalidate, ToolPrompt: execute.ToolPrompt, }) } // Prefetch models asynchronously at startup go modelCache.Refresh() var listener net.Listener if !*no9p { var err error listener, err = net.Listen("unix", sockPath) if err != nil { fmt.Fprintf(os.Stderr, "listen: %v\n", err) os.Exit(1) } go func() { for { conn, err := listener.Accept() if err != nil { return } go srv.Serve(conn) } }() fmt.Printf("olliesrv listening on %s\n", sockPath) } var tcpListener net.Listener if !*no9p && *tcpAddr != "" { var err error tcpListener, err = net.Listen("tcp", *tcpAddr) if err != nil { fmt.Fprintf(os.Stderr, "tcp listen: %v\n", err) os.Exit(1) } go func() { for { conn, err := tcpListener.Accept() if err != nil { return } go srv.Serve(conn) } }() fmt.Printf("olliesrv listening on tcp %s\n", *tcpAddr) } if *no9p { fmt.Println("olliesrv running in D-Bus only mode") } sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan fmt.Println("shutting down") if dbusAdapter != nil { dbusAdapter.Close() } if srv != nil { srv.Shutdown() } if listener != nil { listener.Close() //nolint:errcheck } if tcpListener != nil { tcpListener.Close() //nolint:errcheck } if !*no9p { os.Remove(sockPath) } sink.Flush() } // ToolIndex generates a tool index from a tree's file listing. // NewRootStore returns a read-only FileTree for synthetic root-level files. func NewRootStore() *fs.Tree { helpPath := paths.CfgDir() + "/help.md" readEntries := map[string]func() ([]byte, error){ "backends": func() ([]byte, error) { return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil }, "models": func() ([]byte, error) { return modelCache.Get(), nil }, "help": func() ([]byte, error) { return os.ReadFile(helpPath) }, "agents": func() ([]byte, error) { var sb strings.Builder for _, dir := range agent.AgentsDirs() { entries, err := os.ReadDir(dir) if err != nil { continue } for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".json") { sb.WriteString(strings.TrimSuffix(e.Name(), ".json")) sb.WriteByte('\n') } } } return []byte(sb.String()), nil }, } notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } readOnly := func([]byte) error { return fmt.Errorf("read-only") } // ctl commands ctlHandler := func(data []byte) error { cmd := strings.TrimSpace(string(data)) switch cmd { case "invalidate": modelCache.Invalidate() return nil default: return fmt.Errorf("unknown ctl command: %s", cmd) } } return fs.NewTree(nil, fs.Perms[fs.PathRoot].FileMode, fs.WithResolver(func(_ []string, name string) (string, error) { if _, ok := readEntries[name]; ok { return name, nil } if name == "ctl" || name == "complete" || name == "generate" || name == "route" || name == "tools" { return name, nil } return "", fmt.Errorf("%s: not found", name) }), fs.WithLister(func(_ []string) ([]os.DirEntry, error) { return []os.DirEntry{ fs.FileEntry("backends", fs.Perms[fs.PathRoot].FileMode), fs.FileEntry("ctl", 0666), fs.DirEntry("elevate", 0755), fs.FileEntry("generate", 0666), fs.FileEntry("models", fs.Perms[fs.PathRoot].FileMode), fs.FileEntry("help", fs.Perms[fs.PathRoot].FileMode), fs.FileEntry("route", 0666), fs.FileEntry("tools", 0666), }, nil }), fs.WithOpener(func(_ []string, name string) (fs.File, error) { if name == "complete" { return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "complete", Mode_: 0666}, nil }, ReadFn: func() ([]byte, error) { return nil, nil }, WriteFn: func([]byte) error { return nil }, BlockingReadFn: notBlocking, }, nil } if name == "generate" { return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "generate", Mode_: 0666}, nil }, ReadFn: func() ([]byte, error) { return nil, nil }, WriteFn: func([]byte) error { return nil }, BlockingReadFn: notBlocking, }, nil } if name == "route" { return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "route", Mode_: 0666}, nil }, ReadFn: func() ([]byte, error) { return nil, nil }, WriteFn: func([]byte) error { return nil }, BlockingReadFn: notBlocking, }, nil } if name == "tools" { var query string return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "tools", Mode_: 0666}, nil }, ReadFn: func() ([]byte, error) { if query != "" { result := execute.ToolPrompt(query) query = "" // clear after read if result == "" { return []byte("no prompt found for: " + query + "\n"), nil } return []byte(result + "\n"), nil } // Surface listing infos := execute.DiscoverTools() var sb strings.Builder for _, ti := range infos { if ti.Description != "" { fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description) } else { sb.WriteString(ti.Name + "\n") } } return []byte(sb.String()), nil }, WriteFn: func(data []byte) error { query = strings.TrimSpace(string(data)) return nil }, BlockingReadFn: notBlocking, }, nil } if name == "ctl" { return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "ctl", Mode_: 0666}, nil }, ReadFn: func() ([]byte, error) { return nil, nil }, WriteFn: ctlHandler, BlockingReadFn: notBlocking, }, nil } readFn, ok := readEntries[name] if !ok { return nil, fmt.Errorf("%s: not found", name) } return &fs.FileConfig{ StatFn: func() (os.FileInfo, error) { data, _ := readFn() return &fs.SyntheticFileInfo{Name_: name, Mode_: fs.Perms[fs.PathRoot].FileMode, Size_: int64(len(data))}, nil }, ReadFn: readFn, WriteFn: readOnly, BlockingReadFn: notBlocking, }, nil }), ) }