use 9fans client directly

This commit is contained in:
Ollie Agent 2026-08-16 19:00:48 +02:00
parent 16a2b72051
commit b321eed99f
5 changed files with 246 additions and 221 deletions

View File

@ -1,198 +0,0 @@
package client
import (
"fmt"
"io"
"os"
"strings"
"9fans.net/go/plan9"
p9client "9fans.net/go/plan9/client"
)
// Fsys is an attached 9P filesystem handle.
type Fsys struct {
fsys *p9client.Fsys
}
// Dial connects to the 9P server at the given address and attaches with the
// specified user identity and attach name. Address format: "unix!/path" or
// "/path" for unix sockets, "tcp!host:port" or "host:port" for TCP.
func Dial(address, user, aname string) (*Fsys, error) {
network := "unix"
dialAddr := address
if strings.HasPrefix(address, "tcp!") {
network = "tcp"
dialAddr = strings.TrimPrefix(address, "tcp!")
} else if strings.HasPrefix(address, "unix!") {
dialAddr = strings.TrimPrefix(address, "unix!")
} else if strings.Contains(address, ":") && !strings.HasPrefix(address, "/") {
network = "tcp"
}
conn, err := p9client.Dial(network, dialAddr)
if err != nil {
return nil, err
}
fsys, err := conn.Attach(nil, user, aname)
if err != nil {
conn.Close()
return nil, err
}
return &Fsys{fsys: fsys}, nil
}
// DialDefault connects using NAMESPACE env var (falls back to plan9 client
// namespace) and USER/OLLIE_UNAME for identity.
func DialDefault() (*Fsys, error) {
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = p9client.Namespace()
}
if ns == "" {
return nil, fmt.Errorf("cannot determine namespace; use -a flag or set $NAMESPACE")
}
address := ns + "/ollie"
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
return Dial(address, user, aname)
}
// Close releases the filesystem attachment.
func (f *Fsys) Close() error {
return f.fsys.Close()
}
// Read reads a file's contents.
func (f *Fsys) Read(path string) ([]byte, error) {
fid, err := f.fsys.Open(path, plan9.OREAD)
if err != nil {
return nil, err
}
defer fid.Close()
return io.ReadAll(fid)
}
// OpenRead opens a file for reading and returns an io.ReadCloser.
// The caller is responsible for closing it. Used for streaming reads
// where data should be consumed incrementally.
func (f *Fsys) OpenRead(path string) (io.ReadCloser, error) {
fid, err := f.fsys.Open(path, plan9.OREAD)
if err != nil {
return nil, err
}
return fid, nil
}
// Write writes data to a file. Opens OWRITE, writes, then closes (flush).
func (f *Fsys) Write(path string, data []byte) error {
fid, err := f.fsys.Open(path, plan9.OWRITE)
if err != nil {
return err
}
if _, err := fid.Write(data); err != nil {
fid.Close()
return err
}
return fid.Close()
}
// Rdwr performs write-then-read on a request/response file.
func (f *Fsys) Rdwr(path string, data []byte) ([]byte, error) {
fid, err := f.fsys.Open(path, plan9.ORDWR)
if err != nil {
return nil, err
}
if _, err := fid.Write(data); err != nil {
fid.Close()
return nil, err
}
if _, err := fid.Seek(0, 0); err != nil {
fid.Close()
return nil, err
}
resp, err := io.ReadAll(fid)
fid.Close()
if err != nil {
return nil, err
}
return resp, nil
}
// Stat returns file metadata.
func (f *Fsys) Stat(path string) (*plan9.Dir, error) {
return f.fsys.Stat(path)
}
// Ls lists directory contents, returning entry names.
func (f *Fsys) Ls(path string) ([]string, error) {
fid, err := f.fsys.Open(path, plan9.OREAD)
if err != nil {
return nil, err
}
defer fid.Close()
dirs, err := fid.Dirreadall()
if err != nil {
return nil, err
}
names := make([]string, len(dirs))
for i, d := range dirs {
names[i] = d.Name
}
return names, nil
}
// Create creates a file with the given permissions.
func (f *Fsys) Create(path string, perm plan9.Perm) error {
fid, err := f.fsys.Create(path, plan9.OWRITE, perm)
if err != nil {
return err
}
return fid.Close()
}
// Remove removes a file.
func (f *Fsys) Remove(path string) error {
return f.fsys.Remove(path)
}
// Mkdir creates a directory.
func (f *Fsys) Mkdir(path string) error {
fid, err := f.fsys.Create(path, plan9.OREAD, plan9.DMDIR|0755)
if err != nil {
return err
}
return fid.Close()
}
// Rename renames a file (Twstat with new name).
func (f *Fsys) Rename(oldPath, newName string) error {
return f.fsys.Wstat(oldPath, &plan9.Dir{Name: newName})
}
// ModeString returns a human-readable permission string like "-rw-r--r--".
func ModeString(mode plan9.Perm) string {
var buf [10]byte
if mode&plan9.DMDIR != 0 {
buf[0] = 'd'
} else {
buf[0] = '-'
}
const rwx = "rwx"
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if mode&(1<<uint(8-i*3-j)) != 0 {
buf[1+i*3+j] = rwx[j]
} else {
buf[1+i*3+j] = '-'
}
}
}
return string(buf[:])
}

View File

@ -3,12 +3,15 @@ package agent
import (
"context"
"fmt"
"io"
"os"
"runtime"
"slices"
"sync"
"sync/atomic"
"9fans.net/go/plan9"
p9client "9fans.net/go/plan9/client"
client "ollie/client"
"ollie/cmd/olliesrv/internal/backend"
olog "ollie/log"
@ -652,17 +655,40 @@ func (ag *Agent) FeedWrite(data []byte) {
// Exits when ctx is cancelled or on read error.
func ConsumeFeed(ctx context.Context, ag *Agent) {
path := fmt.Sprintf("session/%s/agent/%s/feed", ag.sessionID, ag.id)
client, err := client.DialDefault()
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = p9client.Namespace()
}
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
conn, err := p9client.Dial("unix", ns+"/ollie")
if err != nil {
ag.log.Error("feed consumer: dial: %v", err)
return
}
fsys, err := conn.Attach(nil, user, aname)
if err != nil {
conn.Close()
ag.log.Error("feed consumer: attach: %v", err)
return
}
go func() {
<-ctx.Done()
client.Close()
fsys.Close()
}()
for ctx.Err() == nil {
data, err := client.Read(path)
fid, err := fsys.Open(path, plan9.OREAD)
if err != nil {
return
}
data, err := io.ReadAll(fid)
fid.Close()
if err != nil {
return
}

View File

@ -2,9 +2,12 @@ package agent
import (
"context"
"io"
"os"
"strings"
client "ollie/client"
"9fans.net/go/plan9"
p9client "9fans.net/go/plan9/client"
)
// HandleCommand intercepts /commands before they reach the LLM.
@ -18,14 +21,45 @@ func (ag *Agent) HandleCommand(_ context.Context, input string) bool {
if rest == "" {
return false
}
fsys, err := client.DialDefault()
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = p9client.Namespace()
}
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
conn, err := p9client.Dial("unix", ns+"/ollie")
if err != nil {
ag.emit(infoEvent("error: " + err.Error()))
return true
}
fsys, err := conn.Attach(nil, user, aname)
if err != nil {
conn.Close()
ag.emit(infoEvent("error: " + err.Error()))
return true
}
defer fsys.Close()
ctlPath := "session/" + ag.sessionID + "/agent/" + ag.id + "/ctl"
out, err := fsys.Rdwr(ctlPath, []byte(rest))
fid, err := fsys.Open(ctlPath, plan9.ORDWR)
if err == nil {
_, err = fid.Write([]byte(rest))
}
var out []byte
if err == nil {
_, err = fid.Seek(0, 0)
}
if err == nil {
out, err = io.ReadAll(fid)
}
if fid != nil {
fid.Close()
}
if err != nil {
ag.emit(infoEvent("error: " + err.Error()))
return true

View File

@ -26,7 +26,9 @@ import (
"syscall"
"time"
client "ollie/client"
"9fans.net/go/plan9"
p9client "9fans.net/go/plan9/client"
"ollie/cmd/toolsrv/internal/registry"
"ollie/cmd/toolsrv/internal/server"
@ -242,16 +244,32 @@ func pushProcInterrupt(proc *server.Proc) {
}
sockPath := fmt.Sprintf("/tmp/ns.%s.%s/ollie", u.Username, display)
fsys, err := client.Dial(sockPath, u.Username, "")
conn, err := p9client.Dial("unix", sockPath)
if err != nil {
logger.Warn("pushProcInterrupt: failed to connect to olliesrv: %v", err)
return
}
fsys, err := conn.Attach(nil, u.Username, "")
if err != nil {
conn.Close()
logger.Warn("pushProcInterrupt: failed to attach to olliesrv: %v", err)
return
}
defer fsys.Close()
// Write to session/{sid}/agent/{aid}/prompt
promptPath := fmt.Sprintf("session/%s/agent/%s/prompt", sid, aid)
if err := fsys.Write(promptPath, []byte(msg)); err != nil {
fid, err := fsys.Open(promptPath, plan9.OWRITE)
if err != nil {
logger.Warn("pushProcInterrupt: failed to open prompt: %v", err)
return
}
_, err = fid.Write([]byte(msg))
closeErr := fid.Close()
if err == nil {
err = closeErr
}
if err != nil {
logger.Warn("pushProcInterrupt: failed to write prompt: %v", err)
return
}

View File

@ -11,18 +11,20 @@ package main
import "C"
import (
"fmt"
"io"
"os"
"strings"
"sync"
"unsafe"
"9fans.net/go/plan9"
client "ollie/client"
p9client "9fans.net/go/plan9/client"
)
// Connection handle - opaque to C callers
type connHandle struct {
fsys *client.Fsys
fsys *p9client.Fsys
}
// File handle - for streaming reads
@ -124,11 +126,17 @@ func ollie9p_connect(addr *C.char, user *C.char, aname *C.char) C.int {
goUser := C.GoString(user)
goAname := C.GoString(aname)
fsys, err := client.Dial(goAddr, goUser, goAname)
conn, err := dial(goAddr)
if err != nil {
setError(err)
return -1
}
fsys, err := conn.Attach(nil, goUser, goAname)
if err != nil {
conn.Close()
setError(err)
return -1
}
h := &connHandle{fsys: fsys}
setError(nil)
@ -140,7 +148,7 @@ func ollie9p_connect(addr *C.char, user *C.char, aname *C.char) C.int {
//
//export ollie9p_connect_default
func ollie9p_connect_default() C.int {
fsys, err := client.DialDefault()
fsys, err := dialDefault()
if err != nil {
setError(err)
return -1
@ -175,7 +183,7 @@ func ollie9p_read(handle C.int, path *C.char, outLen *C.int) *C.char {
return nil
}
data, err := h.fsys.Read(C.GoString(path))
data, err := readFile(h.fsys, C.GoString(path))
if err != nil {
setError(err)
return nil
@ -201,7 +209,7 @@ func ollie9p_read_bytes(handle C.int, path *C.char, buf *C.char, bufLen C.int) C
return -1
}
data, err := h.fsys.Read(C.GoString(path))
data, err := readFile(h.fsys, C.GoString(path))
if err != nil {
setError(err)
return -1
@ -230,7 +238,7 @@ func ollie9p_write(handle C.int, path *C.char, data *C.char, dataLen C.int) C.in
}
goData := C.GoBytes(unsafe.Pointer(data), dataLen)
if err := h.fsys.Write(C.GoString(path), goData); err != nil {
if err := writeFile(h.fsys, C.GoString(path), goData); err != nil {
setError(err)
return -1
}
@ -251,7 +259,7 @@ func ollie9p_rdwr(handle C.int, path *C.char, data *C.char, dataLen C.int, outLe
}
goData := C.GoBytes(unsafe.Pointer(data), dataLen)
resp, err := h.fsys.Rdwr(C.GoString(path), goData)
resp, err := rdwr(h.fsys, C.GoString(path), goData)
if err != nil {
setError(err)
return nil
@ -281,7 +289,7 @@ func ollie9p_stat(handle C.int, path *C.char) *C.char {
setError(nil)
result := strings.Join([]string{
client.ModeString(dir.Mode),
modeString(dir.Mode),
itoa(int64(dir.Length)),
itoa(int64(dir.Mtime)),
dir.Name,
@ -300,7 +308,7 @@ func ollie9p_ls(handle C.int, path *C.char) *C.char {
return nil
}
names, err := h.fsys.Ls(C.GoString(path))
names, err := listDir(h.fsys, C.GoString(path))
if err != nil {
setError(err)
return nil
@ -341,7 +349,7 @@ func ollie9p_create(handle C.int, path *C.char, perm C.uint) C.int {
return -1
}
if err := h.fsys.Create(C.GoString(path), plan9.Perm(perm)); err != nil {
if err := create(h.fsys, C.GoString(path), plan9.Perm(perm)); err != nil {
setError(err)
return -1
}
@ -361,7 +369,7 @@ func ollie9p_mkdir(handle C.int, path *C.char) C.int {
return -1
}
if err := h.fsys.Mkdir(C.GoString(path)); err != nil {
if err := mkdir(h.fsys, C.GoString(path)); err != nil {
setError(err)
return -1
}
@ -381,7 +389,7 @@ func ollie9p_rename(handle C.int, oldPath *C.char, newName *C.char) C.int {
return -1
}
if err := h.fsys.Rename(C.GoString(oldPath), C.GoString(newName)); err != nil {
if err := h.fsys.Wstat(C.GoString(oldPath), &plan9.Dir{Name: C.GoString(newName)}); err != nil {
setError(err)
return -1
}
@ -404,7 +412,7 @@ func ollie9p_open(handle C.int, path *C.char) C.int {
return -1
}
reader, err := h.fsys.OpenRead(C.GoString(path))
reader, err := h.fsys.Open(C.GoString(path), plan9.OREAD)
if err != nil {
setError(err)
return -1
@ -459,6 +467,143 @@ func ollie9p_close_fid(fid C.int) {
freeFid(fid)
}
func dial(address string) (*p9client.Conn, error) {
network := "unix"
dialAddr := address
if strings.HasPrefix(address, "tcp!") {
network = "tcp"
dialAddr = strings.TrimPrefix(address, "tcp!")
} else if strings.HasPrefix(address, "unix!") {
dialAddr = strings.TrimPrefix(address, "unix!")
} else if strings.Contains(address, ":") && !strings.HasPrefix(address, "/") {
network = "tcp"
}
return p9client.Dial(network, dialAddr)
}
func dialDefault() (*p9client.Fsys, error) {
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = p9client.Namespace()
}
if ns == "" {
return nil, fmt.Errorf("cannot determine namespace")
}
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
conn, err := dial(ns + "/ollie")
if err != nil {
return nil, err
}
fsys, err := conn.Attach(nil, user, aname)
if err != nil {
conn.Close()
return nil, err
}
return fsys, nil
}
func readFile(fsys *p9client.Fsys, path string) ([]byte, error) {
fid, err := fsys.Open(path, plan9.OREAD)
if err != nil {
return nil, err
}
defer fid.Close()
return io.ReadAll(fid)
}
func writeFile(fsys *p9client.Fsys, path string, data []byte) error {
fid, err := fsys.Open(path, plan9.OWRITE)
if err != nil {
return err
}
if _, err := fid.Write(data); err != nil {
fid.Close()
return err
}
return fid.Close()
}
func rdwr(fsys *p9client.Fsys, path string, data []byte) ([]byte, error) {
fid, err := fsys.Open(path, plan9.ORDWR)
if err != nil {
return nil, err
}
if _, err := fid.Write(data); err != nil {
fid.Close()
return nil, err
}
if _, err := fid.Seek(0, 0); err != nil {
fid.Close()
return nil, err
}
result, err := io.ReadAll(fid)
closeErr := fid.Close()
if err != nil {
return nil, err
}
return result, closeErr
}
func create(fsys *p9client.Fsys, path string, perm plan9.Perm) error {
fid, err := fsys.Create(path, plan9.OWRITE, perm)
if err != nil {
return err
}
return fid.Close()
}
func mkdir(fsys *p9client.Fsys, path string) error {
fid, err := fsys.Create(path, plan9.OREAD, plan9.DMDIR|0755)
if err != nil {
return err
}
return fid.Close()
}
func listDir(fsys *p9client.Fsys, path string) ([]string, error) {
fid, err := fsys.Open(path, plan9.OREAD)
if err != nil {
return nil, err
}
dirs, err := fid.Dirreadall()
fid.Close()
if err != nil {
return nil, err
}
names := make([]string, len(dirs))
for i, dir := range dirs {
names[i] = dir.Name
}
return names, nil
}
func modeString(mode plan9.Perm) string {
var buf [10]byte
if mode&plan9.DMDIR != 0 {
buf[0] = 'd'
} else {
buf[0] = '-'
}
const rwx = "rwx"
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if mode&(1<<uint(8-i*3-j)) != 0 {
buf[1+i*3+j] = rwx[j]
} else {
buf[1+i*3+j] = '-'
}
}
}
return string(buf[:])
}
// Helper types
type errString struct{ s string }