436 lines
13 KiB
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
|
|
}
|