ollie/cmd/olliesrv/internal/fs/spec.go

436 lines
13 KiB
Go

package fs
// Filesystem specification — the single source of truth for the 9P namespace.
// Constructed via buildTreeSpec with closure capture.
import (
"context"
"os/user"
"ollie/cmd/olliesrv/internal/agent"
"ollie/cmd/olliesrv/internal/bypass"
"ollie/cmd/olliesrv/internal/session"
"ollie/fsedsl"
olog "ollie/log"
)
// Hctx is an internal adapter — holds scope-level dependencies for handlers
// during the incremental migration to pure closures. Will be removed.
type Hctx struct {
context.Context
RootState *RootState
Log *olog.Logger
Models *ModelCache
Bypass *bypass.Broker
Invalidate func()
Shutdown func()
Session *SessionNode
Remove func()
Rename func(string) error
Agent *agent.Agent
AgentLog *AgentLog
Data any
}
// Type aliases for fsedsl types used throughout the package.
type (
Tree = fsedsl.Tree
File = fsedsl.File
SyntheticFileInfo = fsedsl.SyntheticFileInfo
)
// Structural permissions.
const (
PermChildDir = fsedsl.PermChildDir
PermIdx = fsedsl.PermIdx
PermMkdir = fsedsl.PermMkdir
PermMkdirPrivate = fsedsl.PermMkdirPrivate
)
// internalBinding is used by old handler functions during migration.
// It carries the data needed by the adapter functions in this file.
type internalBinding struct {
Name string
Aliases []string
data any
}
var treeSpecUID, treeSpecGID string
// buildTreeSpec constructs the full 9P namespace as an fsedsl.FsNodeDecl.
// All handlers are closures that capture the root context (rs, log, etc.).
func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
// Root-level Hctx for handlers that need it during migration.
rootCtx := func() Hctx {
return Hctx{
Context: rs.Cfg.Ctx,
RootState: rs,
Log: rs.Cfg.Log,
Models: rs.Cfg.ModelCache,
Bypass: rs.Cfg.BypassBroker,
Shutdown: rs.Cfg.Shutdown,
Invalidate: rs.Cfg.Invalidate,
}
}
// Helper: session-scoped Hctx
sessCtx := func(node *SessionNode) Hctx {
ctx := rootCtx()
ctx.Session = node
return ctx
}
// Helper: agent-scoped Hctx
agentCtx := func(node *SessionNode, ag *agent.Agent, al *AgentLog) Hctx {
ctx := sessCtx(node)
ctx.Agent = ag
ctx.AgentLog = al
return ctx
}
u, _ := user.Current()
treeSpecUID = "ollie"
treeSpecGID = "agent"
if u != nil {
treeSpecUID = u.Username
}
return fsedsl.DirNode("/",
fsedsl.UID(treeSpecUID),
fsedsl.GID(treeSpecGID),
fsedsl.FileNode("backends", 0444,
fsedsl.Doc("Available backend names, one per line"),
fsedsl.Read(func() ([]byte, error) { return readBackends(rootCtx()) }),
),
fsedsl.FileNode("help", 0444,
fsedsl.Doc("Filesystem reference"),
fsedsl.Read(func() ([]byte, error) { return readHelp(rootCtx()) }),
),
fsedsl.FileNode("models", 0444,
fsedsl.Doc("Available models. Format: backend<tab>model per line."),
fsedsl.Read(func() ([]byte, error) { return readModels(rootCtx()) }),
),
fsedsl.FileNode("agents", 0444,
fsedsl.Doc("Available agent profiles, one per line"),
fsedsl.Read(func() ([]byte, error) { return readAgents(rootCtx()) }),
),
fsedsl.FileNode("tools", 0444,
fsedsl.Doc("Available tools. Format: name<spaces>description per line."),
fsedsl.Read(func() ([]byte, error) { return readTools(rootCtx()) }),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"),
fsedsl.Request(func(data []byte) ([]byte, error) { return requestRootCtl(rootCtx(), data) }),
),
fsedsl.FileNode("eventwait", 0444,
fsedsl.Doc("Blocking read for server events"),
fsedsl.Read(func() ([]byte, error) { return readEventwait(rootCtx()) }),
fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) {
return blockEventwait(rootCtx(), cctx, base)
}),
),
fsedsl.FileNode("generate", 0666,
fsedsl.Doc("One-shot LLM generation"),
fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(rootCtx(), data) }),
),
fsedsl.FileNode("aliases", 0444,
fsedsl.Doc("Alias table. Format: id<tab>path per line."),
fsedsl.Read(func() ([]byte, error) { return readAliases(rootCtx()) }),
),
fsedsl.DirNode("bypass",
fsedsl.Doc("Bypass broker for sandboxed command approval"),
fsedsl.FileNode("policy", 0666,
fsedsl.Doc("Global bypass policy"),
fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(rootCtx()) }),
fsedsl.Write(func(data []byte) error { return writeBypassPolicy(rootCtx(), data) }),
),
fsedsl.Each("{reqid}", func() ([]fsedsl.Binding, error) {
ctx := rootCtx()
return adaptBypassBindings(ctx)
}),
),
fsedsl.DirNode("session",
fsedsl.Doc("Session management"),
fsedsl.FileNode("new", 0666,
fsedsl.Doc("Create session"),
fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionNew(rootCtx(), data) }),
),
fsedsl.FileNode("idx", 0444,
fsedsl.Doc("Session index"),
fsedsl.Read(func() ([]byte, error) { return readSessionIdx(rootCtx()) }),
),
fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) {
return buildSessionBindings(rs, rootCtx, sessCtx, agentCtx)
}),
),
)
}
// buildSessionBindings produces bindings for each session.
func buildSessionBindings(
rs *RootState,
rootCtx func() Hctx,
sessCtx func(*SessionNode) Hctx,
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
) ([]fsedsl.Binding, error) {
sessions := session.Sessions()
var out []fsedsl.Binding
for name, sess := range sessions {
node := rs.GetOrCreateNode(sess)
s := node // capture
n := name // capture
removeFn := func() {
session.Kill(n)
rs.RemoveNode(n)
}
renameFn := func(newName string) error {
if err := session.Rename(n, newName); err != nil {
return err
}
rs.RenameNode(n, newName)
return nil
}
out = append(out, fsedsl.Binding{
Name: sess.Name(),
Aliases: []string{sess.ID},
Remove: removeFn,
Rename: renameFn,
Children: buildSessionChildren(s, rs, removeFn, renameFn, rootCtx, sessCtx, agentCtx),
})
}
return out, nil
}
// buildSessionChildren returns the file nodes for a single session.
func buildSessionChildren(
s *SessionNode,
rs *RootState,
removeFn func(),
renameFn func(string) error,
rootCtx func() Hctx,
sessCtx func(*SessionNode) Hctx,
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
) []fsedsl.FsNodeDecl {
sc := func() Hctx {
ctx := sessCtx(s)
ctx.Remove = removeFn
ctx.Rename = renameFn
return ctx
}
return []fsedsl.FsNodeDecl{
fsedsl.FileNode("env", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionEnv(sc()) }),
),
fsedsl.FileNode("paused", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionPaused(sc()) }),
),
fsedsl.FileNode("connected", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionConnected(sc()) }),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionCtl(sc(), data) }),
),
fsedsl.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionName(sc()) }),
fsedsl.Write(func(data []byte) error { return writeSessionName(sc(), data) }),
),
fsedsl.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionID(sc()) }),
),
fsedsl.FileNode("bypass", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionBypass(sc()) }),
fsedsl.Write(func(data []byte) error { return writeSessionBypass(sc(), data) }),
),
fsedsl.DirNode("agent",
fsedsl.FileNode("new", 0666,
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(sc(), data) }),
),
fsedsl.FileNode("idx", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentIdx(sc()) }),
),
fsedsl.Each("{aname}", func() ([]fsedsl.Binding, error) {
return buildAgentBindings(s, rs, sessCtx, agentCtx)
}),
),
}
}
// buildAgentBindings produces bindings for each agent in a session.
func buildAgentBindings(
s *SessionNode,
rs *RootState,
sessCtx func(*SessionNode) Hctx,
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
) ([]fsedsl.Binding, error) {
agents := s.Session.Agents()
var out []fsedsl.Binding
for _, ag := range agents {
a := ag
al := s.AgentLogFor(a.ID())
if al == nil {
al = NewAgentLog(s.Session.Remote)
s.SetAgentLog(a.ID(), al)
wireAgentEvents(s.Session.ID, a, al)
}
ac := func() Hctx { return agentCtx(s, a, al) }
out = append(out, fsedsl.Binding{
Name: a.Name(),
Aliases: []string{a.ID()},
Remove: func() {
s.Session.RemoveAgent(a.ID())
session.PublishEvent("session."+s.ID()+".agent."+a.ID()+".kill", "")
},
Children: buildAgentChildren(a, al, s, ac),
})
}
return out, nil
}
// buildAgentChildren returns the file nodes for a single agent.
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func() Hctx) []fsedsl.FsNodeDecl {
return []fsedsl.FsNodeDecl{
fsedsl.FileNode("prompt", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentPrompt(ac(), data) }),
),
fsedsl.FileNode("fifo", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(ac(), data) }),
fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(ac()) }),
),
fsedsl.FileNode("chat", 0444,
fsedsl.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChatText(ac()) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChatText(ac(), cctx, base)
}),
),
fsedsl.FileNode("chat.raw", 0444,
fsedsl.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChat(ac()) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChat(ac(), cctx, base)
}),
),
fsedsl.FileNode("statewait", 0444,
fsedsl.Read(func() ([]byte, error) {
return []byte(a.State() + "\n"), nil
}),
fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) {
return blockAgentStateWait(ac(), cctx, base)
}),
),
fsedsl.FileNode("log", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentLog(ac()) }),
),
fsedsl.FileNode("plan", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentPlan(ac()) }),
fsedsl.Write(func(data []byte) error { return writeAgentPlan(ac(), data) }),
),
fsedsl.FileNode("cfg", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentCfg(ac()) }),
fsedsl.Write(func(data []byte) error { return writeAgentCfg(ac(), data) }),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentCtl(ac(), data) }),
),
fsedsl.FileNode("stats", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentStats(ac()) }),
),
fsedsl.FileNode("tools", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentTools(ac()) }),
fsedsl.Write(func(data []byte) error { return writeAgentTools(ac(), data) }),
),
fsedsl.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentName(ac()) }),
fsedsl.Write(func(data []byte) error { return writeAgentName(ac(), data) }),
),
fsedsl.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentID(ac()) }),
),
fsedsl.DirNode("proc",
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) {
return adaptProcessBindings(ac())
}),
),
}
}
// adaptBypassBindings converts old-style bypass bindings to new fsedsl.Binding.
func adaptBypassBindings(ctx Hctx) ([]fsedsl.Binding, error) {
oldBindings, err := bypassBindings(ctx)
if err != nil {
return nil, err
}
var out []fsedsl.Binding
for _, ob := range oldBindings {
b := ob
// Bypass requests are file-like (read/write), not directories
out = append(out, fsedsl.Binding{
Name: b.Name,
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("request", 0666,
fsedsl.Read(func() ([]byte, error) {
c := ctx
c.Data = b.data
return readBypassRequest(c)
}),
fsedsl.Write(func(data []byte) error {
c := ctx
c.Data = b.data
return writeBypassRequest(c, data)
}),
),
},
})
}
return out, nil
}
// adaptProcessBindings converts old-style process bindings to new fsedsl.Binding.
func adaptProcessBindings(ctx Hctx) ([]fsedsl.Binding, error) {
oldBindings, err := processBindings(ctx)
if err != nil {
return nil, err
}
var out []fsedsl.Binding
for _, ob := range oldBindings {
b := ob
out = append(out, fsedsl.Binding{
Name: b.Name,
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("out", 0444,
fsedsl.Read(func() ([]byte, error) {
c := ctx
c.Data = b.data
return readProcess(c)
}),
),
fsedsl.FileNode("ctl", 0222,
fsedsl.Write(func(data []byte) error {
c := ctx
c.Data = b.data
return writeProcessCtl(c, data)
}),
),
fsedsl.FileNode("status", 0444,
fsedsl.Read(func() ([]byte, error) {
c := ctx
c.Data = b.data
return readProcessStatus(c)
}),
),
},
})
}
return out, nil
}