rename fsedsl → virtfs

The package has outgrown its original 'eDSL' framing. It's a virtual
filesystem library. Rename to match.
This commit is contained in:
Ollie Agent 2026-08-11 17:21:44 +02:00
parent d71ff80993
commit ee7426d628
18 changed files with 248 additions and 248 deletions

View File

@ -11,7 +11,7 @@ import (
"ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/agent"
"ollie/cmd/olliesrv/internal/backend" "ollie/cmd/olliesrv/internal/backend"
"ollie/cmd/olliesrv/internal/bypass" "ollie/cmd/olliesrv/internal/bypass"
"ollie/fsedsl" "ollie/virtfs"
"ollie/cmd/olliesrv/internal/session" "ollie/cmd/olliesrv/internal/session"
"ollie/toolsrv" "ollie/toolsrv"
) )
@ -44,7 +44,7 @@ func readBackends() ([]byte, error) {
var helpFn func() string var helpFn func() string
// helpSpec stores the spec for help generation. Set during tree build. // helpSpec stores the spec for help generation. Set during tree build.
var helpSpec *fsedsl.FsNodeDecl var helpSpec *virtfs.FsNodeDecl
func readHelp() ([]byte, error) { func readHelp() ([]byte, error) {
if helpFn == nil { if helpFn == nil {
@ -57,7 +57,7 @@ func init() {
helpFn = func() string { helpFn = func() string {
helpOnce.Do(func() { helpOnce.Do(func() {
if helpSpec != nil { if helpSpec != nil {
helpText = fsedsl.GenerateHelp(*helpSpec) helpText = virtfs.GenerateHelp(*helpSpec)
} }
}) })
return helpText return helpText

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"ollie/cmd/olliesrv/internal/bypass" "ollie/cmd/olliesrv/internal/bypass"
"ollie/fsedsl" "ollie/virtfs"
olog "ollie/log" olog "ollie/log"
"ollie/cmd/olliesrv/internal/session" "ollie/cmd/olliesrv/internal/session"
) )
@ -45,7 +45,7 @@ func NewRoot(cfg Config) *Tree {
// Build the tree from the spec. // Build the tree from the spec.
spec := buildTreeSpec(rs) spec := buildTreeSpec(rs)
helpSpec = &spec helpSpec = &spec
tree := fsedsl.BuildTree(spec) tree := virtfs.BuildTree(spec)
tree.Data = rs tree.Data = rs
// Restore persisted sessions from disk. // Restore persisted sessions from disk.

View File

@ -11,29 +11,29 @@ import (
"ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/agent"
"ollie/cmd/olliesrv/internal/bypass" "ollie/cmd/olliesrv/internal/bypass"
"ollie/cmd/olliesrv/internal/session" "ollie/cmd/olliesrv/internal/session"
"ollie/fsedsl" "ollie/virtfs"
) )
// Type aliases for fsedsl types used throughout the package. // Type aliases for virtfs types used throughout the package.
type ( type (
Tree = fsedsl.Tree Tree = virtfs.Tree
File = fsedsl.File File = virtfs.File
SyntheticFileInfo = fsedsl.SyntheticFileInfo SyntheticFileInfo = virtfs.SyntheticFileInfo
) )
// Structural permissions. // Structural permissions.
const ( const (
PermChildDir = fsedsl.PermChildDir PermChildDir = virtfs.PermChildDir
PermIdx = fsedsl.PermIdx PermIdx = virtfs.PermIdx
PermMkdir = fsedsl.PermMkdir PermMkdir = virtfs.PermMkdir
PermMkdirPrivate = fsedsl.PermMkdirPrivate PermMkdirPrivate = virtfs.PermMkdirPrivate
) )
var treeSpecUID, treeSpecGID string var treeSpecUID, treeSpecGID string
// buildTreeSpec constructs the full 9P namespace as an fsedsl.FsNodeDecl. // buildTreeSpec constructs the full 9P namespace as an virtfs.FsNodeDecl.
// All handlers are closures that capture their dependencies at binding time. // All handlers are closures that capture their dependencies at binding time.
func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl { func buildTreeSpec(rs *RootState) virtfs.FsNodeDecl {
cfg := &rs.Cfg cfg := &rs.Cfg
mc := cfg.ModelCache mc := cfg.ModelCache
broker := cfg.BypassBroker broker := cfg.BypassBroker
@ -45,71 +45,71 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
treeSpecUID = u.Username treeSpecUID = u.Username
} }
return fsedsl.DirNode("/", return virtfs.DirNode("/",
fsedsl.UID(treeSpecUID), virtfs.UID(treeSpecUID),
fsedsl.GID(treeSpecGID), virtfs.GID(treeSpecGID),
fsedsl.FileNode("backends", 0444, virtfs.FileNode("backends", 0444,
fsedsl.Doc("Available backend names, one per line"), virtfs.Doc("Available backend names, one per line"),
fsedsl.Read(readBackends), virtfs.Read(readBackends),
), ),
fsedsl.FileNode("help", 0444, virtfs.FileNode("help", 0444,
fsedsl.Doc("Filesystem reference"), virtfs.Doc("Filesystem reference"),
fsedsl.Read(readHelp), virtfs.Read(readHelp),
), ),
fsedsl.FileNode("models", 0444, virtfs.FileNode("models", 0444,
fsedsl.Doc("Available models. Format: backend<tab>model per line."), virtfs.Doc("Available models. Format: backend<tab>model per line."),
fsedsl.Read(func() ([]byte, error) { return readModels(mc) }), virtfs.Read(func() ([]byte, error) { return readModels(mc) }),
), ),
fsedsl.FileNode("agents", 0444, virtfs.FileNode("agents", 0444,
fsedsl.Doc("Available agent profiles, one per line"), virtfs.Doc("Available agent profiles, one per line"),
fsedsl.Read(readAgents), virtfs.Read(readAgents),
), ),
fsedsl.FileNode("tools", 0444, virtfs.FileNode("tools", 0444,
fsedsl.Doc("Available tools. Format: name<spaces>description per line."), virtfs.Doc("Available tools. Format: name<spaces>description per line."),
fsedsl.Read(readTools), virtfs.Read(readTools),
), ),
fsedsl.FileNode("ctl", 0666, virtfs.FileNode("ctl", 0666,
fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"), virtfs.Doc("Server control. Write: 'invalidate', 'kill'"),
fsedsl.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)), virtfs.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)),
), ),
fsedsl.FileNode("eventwait", 0444, virtfs.FileNode("eventwait", 0444,
fsedsl.Doc("Blocking read for server events"), virtfs.Doc("Blocking read for server events"),
fsedsl.Read(readEventwait), virtfs.Read(readEventwait),
fsedsl.BlockOnce(blockEventwait), virtfs.BlockOnce(blockEventwait),
), ),
fsedsl.FileNode("generate", 0666, virtfs.FileNode("generate", 0666,
fsedsl.Doc("One-shot LLM generation"), virtfs.Doc("One-shot LLM generation"),
fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }), virtfs.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }),
), ),
fsedsl.FileNode("aliases", 0444, virtfs.FileNode("aliases", 0444,
fsedsl.Doc("Alias table. Format: id<tab>path per line."), virtfs.Doc("Alias table. Format: id<tab>path per line."),
fsedsl.Read(readAliases), virtfs.Read(readAliases),
), ),
fsedsl.DirNode("bypass", virtfs.DirNode("bypass",
fsedsl.Doc("Bypass broker for sandboxed command approval"), virtfs.Doc("Bypass broker for sandboxed command approval"),
fsedsl.FileNode("policy", 0666, virtfs.FileNode("policy", 0666,
fsedsl.Doc("Global bypass policy"), virtfs.Doc("Global bypass policy"),
fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(broker) }), virtfs.Read(func() ([]byte, error) { return readBypassPolicy(broker) }),
fsedsl.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }), virtfs.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }),
), ),
fsedsl.Each("{reqid}", func() ([]fsedsl.Binding, error) { virtfs.Each("{reqid}", func() ([]virtfs.Binding, error) {
return adaptBypassBindings(broker) return adaptBypassBindings(broker)
}), }),
), ),
fsedsl.DirNode("session", virtfs.DirNode("session",
fsedsl.Doc("Session management"), virtfs.Doc("Session management"),
fsedsl.FileNode("new", 0666, virtfs.FileNode("new", 0666,
fsedsl.Doc("Create session"), virtfs.Doc("Create session"),
fsedsl.Request(requestSessionNew), virtfs.Request(requestSessionNew),
), ),
fsedsl.FileNode("idx", 0444, virtfs.FileNode("idx", 0444,
fsedsl.Doc("Session index"), virtfs.Doc("Session index"),
fsedsl.Read(readSessionIdx), virtfs.Read(readSessionIdx),
), ),
fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) { virtfs.Each("{sname}", func() ([]virtfs.Binding, error) {
return buildSessionBindings(rs, broker) return buildSessionBindings(rs, broker)
}), }),
), ),
@ -117,9 +117,9 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
} }
// buildSessionBindings produces bindings for each session. // buildSessionBindings produces bindings for each session.
func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]fsedsl.Binding, error) { func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]virtfs.Binding, error) {
sessions := session.Sessions() sessions := session.Sessions()
var out []fsedsl.Binding var out []virtfs.Binding
for name, sess := range sessions { for name, sess := range sessions {
node := rs.GetOrCreateNode(sess) node := rs.GetOrCreateNode(sess)
@ -138,7 +138,7 @@ func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]fsedsl.Bindin
return nil return nil
} }
out = append(out, fsedsl.Binding{ out = append(out, virtfs.Binding{
Name: sess.Name(), Name: sess.Name(),
Aliases: []string{sess.ID}, Aliases: []string{sess.ID},
Remove: removeFn, Remove: removeFn,
@ -156,39 +156,39 @@ func buildSessionChildren(
broker *bypass.Broker, broker *bypass.Broker,
removeFn func(), removeFn func(),
renameFn func(string) error, renameFn func(string) error,
) []fsedsl.FsNodeDecl { ) []virtfs.FsNodeDecl {
return []fsedsl.FsNodeDecl{ return []virtfs.FsNodeDecl{
fsedsl.FileNode("env", 0444, virtfs.FileNode("env", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionEnv(s) }), virtfs.Read(func() ([]byte, error) { return readSessionEnv(s) }),
), ),
fsedsl.FileNode("paused", 0444, virtfs.FileNode("paused", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionPaused(s) }), virtfs.Read(func() ([]byte, error) { return readSessionPaused(s) }),
), ),
fsedsl.FileNode("connected", 0444, virtfs.FileNode("connected", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionConnected(s) }), virtfs.Read(func() ([]byte, error) { return readSessionConnected(s) }),
), ),
fsedsl.FileNode("ctl", 0666, virtfs.FileNode("ctl", 0666,
fsedsl.Request(sessionCtlHandler(s, removeFn, renameFn)), virtfs.Request(sessionCtlHandler(s, removeFn, renameFn)),
), ),
fsedsl.FileNode("name", 0666, virtfs.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionName(s) }), virtfs.Read(func() ([]byte, error) { return readSessionName(s) }),
fsedsl.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }), virtfs.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }),
), ),
fsedsl.FileNode("id", 0444, virtfs.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionID(s) }), virtfs.Read(func() ([]byte, error) { return readSessionID(s) }),
), ),
fsedsl.FileNode("bypass", 0666, virtfs.FileNode("bypass", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }), virtfs.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }),
fsedsl.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }), virtfs.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }),
), ),
fsedsl.DirNode("agent", virtfs.DirNode("agent",
fsedsl.FileNode("new", 0666, virtfs.FileNode("new", 0666,
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }), virtfs.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }),
), ),
fsedsl.FileNode("idx", 0444, virtfs.FileNode("idx", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }), virtfs.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }),
), ),
fsedsl.Each("{aname}", func() ([]fsedsl.Binding, error) { virtfs.Each("{aname}", func() ([]virtfs.Binding, error) {
return buildAgentBindings(s, rs) return buildAgentBindings(s, rs)
}), }),
), ),
@ -196,9 +196,9 @@ func buildSessionChildren(
} }
// buildAgentBindings produces bindings for each agent in a session. // buildAgentBindings produces bindings for each agent in a session.
func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error) { func buildAgentBindings(s *SessionNode, rs *RootState) ([]virtfs.Binding, error) {
agents := s.Agents() agents := s.Agents()
var out []fsedsl.Binding var out []virtfs.Binding
for _, ag := range agents { for _, ag := range agents {
a := ag a := ag
@ -209,7 +209,7 @@ func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error)
wireAgentEvents(s.ID, a, al) wireAgentEvents(s.ID, a, al)
} }
out = append(out, fsedsl.Binding{ out = append(out, virtfs.Binding{
Name: a.Name(), Name: a.Name(),
Aliases: []string{a.ID()}, Aliases: []string{a.ID()},
Remove: func() { Remove: func() {
@ -223,67 +223,67 @@ func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error)
} }
// buildAgentChildren returns the file nodes for a single agent. // buildAgentChildren returns the file nodes for a single agent.
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []fsedsl.FsNodeDecl { func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []virtfs.FsNodeDecl {
return []fsedsl.FsNodeDecl{ return []virtfs.FsNodeDecl{
fsedsl.FileNode("prompt", 0666, virtfs.FileNode("prompt", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }), virtfs.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }),
), ),
fsedsl.FileNode("fifo", 0666, virtfs.FileNode("fifo", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }), virtfs.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }),
fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(a) }), virtfs.Read(func() ([]byte, error) { return readAgentFifoOut(a) }),
), ),
fsedsl.FileNode("chat", 0444, virtfs.FileNode("chat", 0444,
fsedsl.StatOverride(statAgentChat), virtfs.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChatText(al) }), virtfs.Read(func() ([]byte, error) { return readAgentChatText(al) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { virtfs.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChatText(s, al, cctx, base) return streamAgentChatText(s, al, cctx, base)
}), }),
), ),
fsedsl.FileNode("chat.raw", 0444, virtfs.FileNode("chat.raw", 0444,
fsedsl.StatOverride(statAgentChat), virtfs.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChat(al) }), virtfs.Read(func() ([]byte, error) { return readAgentChat(al) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { virtfs.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChat(s, al, cctx, base) return streamAgentChat(s, al, cctx, base)
}), }),
), ),
fsedsl.FileNode("statewait", 0444, virtfs.FileNode("statewait", 0444,
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return []byte(a.State() + "\n"), nil return []byte(a.State() + "\n"), nil
}), }),
fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) { virtfs.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) {
return blockAgentStateWait(s, a, cctx, base) return blockAgentStateWait(s, a, cctx, base)
}), }),
), ),
fsedsl.FileNode("log", 0444, virtfs.FileNode("log", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentLog(al) }), virtfs.Read(func() ([]byte, error) { return readAgentLog(al) }),
), ),
fsedsl.FileNode("plan", 0666, virtfs.FileNode("plan", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentPlan(al) }), virtfs.Read(func() ([]byte, error) { return readAgentPlan(al) }),
fsedsl.Write(func(data []byte) error { return writeAgentPlan(al, data) }), virtfs.Write(func(data []byte) error { return writeAgentPlan(al, data) }),
), ),
fsedsl.FileNode("cfg", 0666, virtfs.FileNode("cfg", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }), virtfs.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }),
fsedsl.Write(func(data []byte) error { return writeAgentCfg(a, data) }), virtfs.Write(func(data []byte) error { return writeAgentCfg(a, data) }),
), ),
fsedsl.FileNode("ctl", 0666, virtfs.FileNode("ctl", 0666,
fsedsl.Request(agentCtlHandler(s, a)), virtfs.Request(agentCtlHandler(s, a)),
), ),
fsedsl.FileNode("stats", 0444, virtfs.FileNode("stats", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentStats(a) }), virtfs.Read(func() ([]byte, error) { return readAgentStats(a) }),
), ),
fsedsl.FileNode("tools", 0666, virtfs.FileNode("tools", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentTools(a) }), virtfs.Read(func() ([]byte, error) { return readAgentTools(a) }),
fsedsl.Write(func(data []byte) error { return writeAgentTools(a, data) }), virtfs.Write(func(data []byte) error { return writeAgentTools(a, data) }),
), ),
fsedsl.FileNode("name", 0666, virtfs.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentName(a) }), virtfs.Read(func() ([]byte, error) { return readAgentName(a) }),
fsedsl.Write(func(data []byte) error { return writeAgentName(s, a, data) }), virtfs.Write(func(data []byte) error { return writeAgentName(s, a, data) }),
), ),
fsedsl.FileNode("id", 0444, virtfs.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentID(a) }), virtfs.Read(func() ([]byte, error) { return readAgentID(a) }),
), ),
fsedsl.DirNode("proc", virtfs.DirNode("proc",
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) { virtfs.Each("{pid}", func() ([]virtfs.Binding, error) {
return adaptProcessBindings(s) return adaptProcessBindings(s)
}), }),
), ),
@ -291,22 +291,22 @@ func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []fsedsl.F
} }
// adaptBypassBindings returns bindings for pending bypass requests. // adaptBypassBindings returns bindings for pending bypass requests.
func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) { func adaptBypassBindings(broker *bypass.Broker) ([]virtfs.Binding, error) {
if broker == nil { if broker == nil {
return nil, errNoBypass return nil, errNoBypass
} }
pending := broker.Pending() pending := broker.Pending()
var out []fsedsl.Binding var out []virtfs.Binding
for _, r := range pending { for _, r := range pending {
req := r req := r
out = append(out, fsedsl.Binding{ out = append(out, virtfs.Binding{
Name: req.ID, Name: req.ID,
Children: []fsedsl.FsNodeDecl{ Children: []virtfs.FsNodeDecl{
fsedsl.FileNode("request", 0666, virtfs.FileNode("request", 0666,
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return readBypassRequest(req) return readBypassRequest(req)
}), }),
fsedsl.Write(func(data []byte) error { virtfs.Write(func(data []byte) error {
return writeBypassRequest(broker, req, data) return writeBypassRequest(broker, req, data)
}), }),
), ),
@ -317,7 +317,7 @@ func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) {
} }
// adaptProcessBindings returns bindings for detached processes. // adaptProcessBindings returns bindings for detached processes.
func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) { func adaptProcessBindings(s *SessionNode) ([]virtfs.Binding, error) {
if s == nil { if s == nil {
return nil, nil return nil, nil
} }
@ -327,26 +327,26 @@ func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) {
} }
defer conn.Close() defer conn.Close()
raw := conn.ListDetachedRaw() raw := conn.ListDetachedRaw()
var out []fsedsl.Binding var out []virtfs.Binding
for _, r := range raw { for _, r := range raw {
if m, ok := r.(map[string]any); ok { if m, ok := r.(map[string]any); ok {
if id, ok := m["pid"].(int); ok { if id, ok := m["pid"].(int); ok {
pid := id pid := id
out = append(out, fsedsl.Binding{ out = append(out, virtfs.Binding{
Name: strconv.Itoa(pid), Name: strconv.Itoa(pid),
Children: []fsedsl.FsNodeDecl{ Children: []virtfs.FsNodeDecl{
fsedsl.FileNode("out", 0444, virtfs.FileNode("out", 0444,
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return readProcess(s, pid) return readProcess(s, pid)
}), }),
), ),
fsedsl.FileNode("ctl", 0222, virtfs.FileNode("ctl", 0222,
fsedsl.Write(func(data []byte) error { virtfs.Write(func(data []byte) error {
return writeProcessCtl(s, pid, data) return writeProcessCtl(s, pid, data)
}), }),
), ),
fsedsl.FileNode("status", 0444, virtfs.FileNode("status", 0444,
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return readProcessStatus(s, pid) return readProcessStatus(s, pid)
}), }),
), ),

View File

@ -1,4 +1,4 @@
// spec.go - fsedsl-based namespace specification for toolsrv. // spec.go - virtfs-based namespace specification for toolsrv.
package fs package fs
import ( import (
@ -8,22 +8,22 @@ import (
"strconv" "strconv"
"strings" "strings"
"ollie/fsedsl" "ollie/virtfs"
) )
// Spec returns the fsedsl specification for the toolsrv namespace. // Spec returns the virtfs specification for the toolsrv namespace.
// The server is captured by closures — no context threading needed. // The server is captured by closures — no context threading needed.
func Spec(srv *Server) fsedsl.FsNodeDecl { func Spec(srv *Server) virtfs.FsNodeDecl {
return fsedsl.DirNode("/", return virtfs.DirNode("/",
fsedsl.FileNode("ctl", 0222, virtfs.FileNode("ctl", 0222,
fsedsl.Doc("Control: write 'load <agentID> <tool>' or 'unload <agentID> <tool>'"), virtfs.Doc("Control: write 'load <agentID> <tool>' or 'unload <agentID> <tool>'"),
fsedsl.Write(func(data []byte) error { virtfs.Write(func(data []byte) error {
return srv.Fs.HandleCtl(strings.TrimSpace(string(data))) return srv.Fs.HandleCtl(strings.TrimSpace(string(data)))
}), }),
), ),
fsedsl.FileNode("tools", 0666, virtfs.FileNode("tools", 0666,
fsedsl.Doc("Tools: write agent ID, read tool list (rdwr)"), virtfs.Doc("Tools: write agent ID, read tool list (rdwr)"),
fsedsl.Request(func(data []byte) ([]byte, error) { virtfs.Request(func(data []byte) ([]byte, error) {
agentID := strings.TrimSpace(string(data)) agentID := strings.TrimSpace(string(data))
if agentID == "" { if agentID == "" {
return nil, fmt.Errorf("agent ID required") return nil, fmt.Errorf("agent ID required")
@ -31,16 +31,16 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(srv.Fs.HandleToolsRequest(agentID)), nil return []byte(srv.Fs.HandleToolsRequest(agentID)), nil
}), }),
), ),
fsedsl.FileNode("info", 0444, virtfs.FileNode("info", 0444,
fsedsl.Doc("Host info: platform, arch"), virtfs.Doc("Host info: platform, arch"),
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return []byte(srv.HostInfo()), nil return []byte(srv.HostInfo()), nil
}), }),
), ),
fsedsl.DirNode("proc", virtfs.DirNode("proc",
fsedsl.FileNode("new", 0666, virtfs.FileNode("new", 0666,
fsedsl.Doc("Execute tool: write token + tool + args, read result (blocking)"), virtfs.Doc("Execute tool: write token + tool + args, read result (blocking)"),
fsedsl.Request(func(data []byte) ([]byte, error) { virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNew(context.Background(), string(data)) result, err := srv.Fs.HandleProcNew(context.Background(), string(data))
if err != nil { if err != nil {
return nil, err return nil, err
@ -48,9 +48,9 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(result), nil return []byte(result), nil
}), }),
), ),
fsedsl.FileNode("new.bg", 0666, virtfs.FileNode("new.bg", 0666,
fsedsl.Doc("Execute tool in background: write token + tool + args, read pid"), virtfs.Doc("Execute tool in background: write token + tool + args, read pid"),
fsedsl.Request(func(data []byte) ([]byte, error) { virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNewBg(context.Background(), string(data)) result, err := srv.Fs.HandleProcNewBg(context.Background(), string(data))
if err != nil { if err != nil {
return nil, err return nil, err
@ -58,40 +58,40 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(result), nil return []byte(result), nil
}), }),
), ),
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) { virtfs.Each("{pid}", func() ([]virtfs.Binding, error) {
pids := srv.Fs.ListProcs() pids := srv.Fs.ListProcs()
var bindings []fsedsl.Binding var bindings []virtfs.Binding
for _, pid := range pids { for _, pid := range pids {
proc := srv.Fs.GetProc(pid) proc := srv.Fs.GetProc(pid)
if proc == nil { if proc == nil {
continue continue
} }
p := proc p := proc
bindings = append(bindings, fsedsl.Binding{ bindings = append(bindings, virtfs.Binding{
Name: strconv.Itoa(pid), Name: strconv.Itoa(pid),
Children: []fsedsl.FsNodeDecl{ Children: []virtfs.FsNodeDecl{
fsedsl.FileNode("out", 0444, virtfs.FileNode("out", 0444,
fsedsl.Doc("Process output"), virtfs.Doc("Process output"),
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return []byte(p.Output()), nil return []byte(p.Output()), nil
}), }),
), ),
fsedsl.FileNode("wait", 0444, virtfs.FileNode("wait", 0444,
fsedsl.Doc("Block until process exits, returns exit code"), virtfs.Doc("Block until process exits, returns exit code"),
fsedsl.BlockOnce(func(_ context.Context, _ string) ([]byte, string, error) { virtfs.BlockOnce(func(_ context.Context, _ string) ([]byte, string, error) {
exitCode := p.Wait() exitCode := p.Wait()
return []byte(fmt.Sprintf("%d\n", exitCode)), "", nil return []byte(fmt.Sprintf("%d\n", exitCode)), "", nil
}), }),
), ),
fsedsl.FileNode("stat", 0444, virtfs.FileNode("stat", 0444,
fsedsl.Doc("Process status: running/exited, runtime, tool"), virtfs.Doc("Process status: running/exited, runtime, tool"),
fsedsl.Read(func() ([]byte, error) { virtfs.Read(func() ([]byte, error) {
return []byte(p.Stat()), nil return []byte(p.Stat()), nil
}), }),
), ),
fsedsl.FileNode("ctl", 0222, virtfs.FileNode("ctl", 0222,
fsedsl.Doc("Process control: write 'signal <N>' or 'dismiss'"), virtfs.Doc("Process control: write 'signal <N>' or 'dismiss'"),
fsedsl.Write(func(data []byte) error { virtfs.Write(func(data []byte) error {
return srv.Fs.HandleProcCtl(p.ID, strings.TrimSpace(string(data))) return srv.Fs.HandleProcCtl(p.ID, strings.TrimSpace(string(data)))
}), }),
), ),

View File

@ -9,7 +9,7 @@ import (
"sync" "sync"
"ollie/cmd/toolsrv/internal/registry" "ollie/cmd/toolsrv/internal/registry"
"ollie/fsedsl" "ollie/virtfs"
) )
// Server holds the state for the toolsrv 9P server. // Server holds the state for the toolsrv 9P server.
@ -82,9 +82,9 @@ func (s *Server) HostInfo() string {
return fmt.Sprintf("platform=%s\narch=%s\n", runtime.GOOS, runtime.GOARCH) return fmt.Sprintf("platform=%s\narch=%s\n", runtime.GOOS, runtime.GOARCH)
} }
// BuildTree creates the fsedsl tree for this server. // BuildTree creates the virtfs tree for this server.
func (s *Server) BuildTree() *fsedsl.Tree { func (s *Server) BuildTree() *virtfs.Tree {
return fsedsl.BuildTree(Spec(s)) return virtfs.BuildTree(Spec(s))
} }
// GenerateSecret generates a random shared secret for toolsrv auth. // GenerateSecret generates a random shared secret for toolsrv auth.

View File

@ -10,7 +10,7 @@ import (
"sync" "sync"
"ollie/cmd/toolsrv/internal/fs" "ollie/cmd/toolsrv/internal/fs"
"ollie/fsedsl" "ollie/virtfs"
"9fans.net/go/plan9" "9fans.net/go/plan9"
) )
@ -21,7 +21,7 @@ type fid struct {
qid plan9.Qid qid plan9.Qid
mode uint8 mode uint8
opened bool opened bool
entry fsedsl.File entry virtfs.File
readBuf []byte // buffer for read data readBuf []byte // buffer for read data
readDone bool // true if file has been read readDone bool // true if file has been read
dirCache []byte dirCache []byte
@ -46,8 +46,8 @@ type connState struct {
noAuth bool // skip authentication (for debugging) noAuth bool // skip authentication (for debugging)
} }
// serve9P handles a single 9P connection using the fsedsl tree. // serve9P handles a single 9P connection using the virtfs tree.
func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree, srv *fs.Server, noAuth bool) { func serve9P(ctx context.Context, conn net.Conn, tree *virtfs.Tree, srv *fs.Server, noAuth bool) {
defer conn.Close() defer conn.Close()
cs := &connState{ cs := &connState{
@ -81,7 +81,7 @@ func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree, srv *fs.Serv
} }
} }
func handleFcall(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleFcall(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
switch fc.Type { switch fc.Type {
case plan9.Tversion: case plan9.Tversion:
return handleVersion(fc) return handleVersion(fc)
@ -133,7 +133,7 @@ func handleAuth(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
} }
} }
func handleAttach(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleAttach(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
// Skip auth check if noAuth is set (for debugging) // Skip auth check if noAuth is set (for debugging)
if cs.noAuth { if cs.noAuth {
cs.mu.Lock() cs.mu.Lock()
@ -210,7 +210,7 @@ func handleAuthRead(cs *connState, fc *plan9.Fcall, af *authFid) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: data[offset:end]} return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: data[offset:end]}
} }
func handleWalk(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleWalk(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
cs.mu.RLock() cs.mu.RLock()
f, ok := cs.fids[fc.Fid] f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock() cs.mu.RUnlock()
@ -288,7 +288,7 @@ func handleWalk(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall
return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: qids} return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: qids}
} }
func handleOpen(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleOpen(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
cs.mu.Lock() cs.mu.Lock()
defer cs.mu.Unlock() defer cs.mu.Unlock()
@ -325,7 +325,7 @@ func handleOpen(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192} return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192}
} }
func handleRead(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleRead(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
// Check if reading from auth fid // Check if reading from auth fid
cs.mu.RLock() cs.mu.RLock()
af, isAuthFid := cs.authFids[fc.Fid] af, isAuthFid := cs.authFids[fc.Fid]
@ -380,7 +380,7 @@ func handleRead(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.readBuf[offset:end]} return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.readBuf[offset:end]}
} }
func handleDirRead(cs *connState, f *fid, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleDirRead(cs *connState, f *fid, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
if fc.Offset == 0 || f.dirCache == nil { if fc.Offset == 0 || f.dirCache == nil {
path := f.path path := f.path
if path == "/" { if path == "/" {
@ -428,7 +428,7 @@ func handleDirRead(cs *connState, f *fid, fc *plan9.Fcall, tree *fsedsl.Tree) *p
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.dirCache[offset:end]} return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.dirCache[offset:end]}
} }
func handleWrite(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleWrite(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
// Check if writing to auth fid // Check if writing to auth fid
cs.mu.Lock() cs.mu.Lock()
af, isAuthFid := cs.authFids[fc.Fid] af, isAuthFid := cs.authFids[fc.Fid]
@ -465,7 +465,7 @@ func handleClunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag} return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
} }
func handleStat(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall { func handleStat(cs *connState, fc *plan9.Fcall, tree *virtfs.Tree) *plan9.Fcall {
cs.mu.RLock() cs.mu.RLock()
f, ok := cs.fids[fc.Fid] f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock() cs.mu.RUnlock()

View File

@ -1,3 +0,0 @@
module ollie/fsedsl
go 1.25.6

4
go.mod
View File

@ -9,9 +9,9 @@ require (
github.com/simonfxr/pubsub v0.0.5 github.com/simonfxr/pubsub v0.0.5
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
ollie/fsedsl v0.0.0 ollie/virtfs v0.0.0
) )
require golang.org/x/sys v0.47.0 // indirect require golang.org/x/sys v0.47.0 // indirect
replace ollie/fsedsl => ./fsedsl replace ollie/virtfs => ./virtfs

View File

@ -1,4 +1,4 @@
# fsedsl # virtfs
A Go library providing an embedded domain-specific language (EDSL) for declaring synthetic filesystems. Define your namespace structure statically while populating it dynamically at runtime. A Go library providing an embedded domain-specific language (EDSL) for declaring synthetic filesystems. Define your namespace structure statically while populating it dynamically at runtime.
@ -14,19 +14,19 @@ The library produces a generic `Tree` that can be used with any filesystem proto
## Installation ## Installation
The library is currently embedded in the [ollie](https://git.lneely.de/lkn/ollie) repository at `fsedsl/`. The library is currently embedded in the [ollie](https://git.lneely.de/lkn/ollie) repository at `virtfs/`.
To use it in your project, add a replace directive to your `go.mod`: To use it in your project, add a replace directive to your `go.mod`:
```bash ```bash
# Clone or copy the fsedsl directory to your project, then: # Clone or copy the virtfs directory to your project, then:
go mod edit -replace=git.lneely.de/lkn/fsedsl=./fsedsl go mod edit -replace=git.lneely.de/lkn/virtfs=./virtfs
``` ```
Or reference it directly from the ollie repo: Or reference it directly from the ollie repo:
```bash ```bash
go mod edit -replace=git.lneely.de/lkn/fsedsl=git.lneely.de/lkn/ollie/fsedsl@main go mod edit -replace=git.lneely.de/lkn/virtfs=git.lneely.de/lkn/ollie/virtfs@main
``` ```
## Quick Start ## Quick Start
@ -36,7 +36,7 @@ package main
import ( import (
"fmt" "fmt"
"git.lneely.de/lkn/fsedsl" "git.lneely.de/lkn/virtfs"
) )
type Ctx struct { type Ctx struct {
@ -55,18 +55,18 @@ var users = []*User{
func main() { func main() {
// Declare the filesystem spec // Declare the filesystem spec
spec := fsedsl.DirNode[Ctx]("/", spec := virtfs.DirNode[Ctx]("/",
fsedsl.FileNode[Ctx]("version", 0444, virtfs.FileNode[Ctx]("version", 0444,
fsedsl.Doc[Ctx]("API version"), virtfs.Doc[Ctx]("API version"),
fsedsl.Read(func(ctx Ctx) ([]byte, error) { virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0\n"), nil return []byte("1.0\n"), nil
}), }),
), ),
fsedsl.Each[Ctx]("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) { virtfs.Each[Ctx]("{user}", func(ctx Ctx) ([]virtfs.Binding[Ctx], error) {
var out []fsedsl.Binding[Ctx] var out []virtfs.Binding[Ctx]
for _, u := range users { for _, u := range users {
user := u // capture user := u // capture
out = append(out, fsedsl.Binding[Ctx]{ out = append(out, virtfs.Binding[Ctx]{
Name: user.Name, Name: user.Name,
Aliases: []string{user.ID}, Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx { Applier: func(c Ctx) Ctx {
@ -77,13 +77,13 @@ func main() {
} }
return out, nil return out, nil
}, },
fsedsl.FileNode[Ctx]("id", 0444, virtfs.FileNode[Ctx]("id", 0444,
fsedsl.Read(func(ctx Ctx) ([]byte, error) { virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.ID + "\n"), nil return []byte(ctx.User.ID + "\n"), nil
}), }),
), ),
fsedsl.FileNode[Ctx]("name", 0444, virtfs.FileNode[Ctx]("name", 0444,
fsedsl.Read(func(ctx Ctx) ([]byte, error) { virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil return []byte(ctx.User.Name + "\n"), nil
}), }),
), ),
@ -91,7 +91,7 @@ func main() {
) )
// Build the tree // Build the tree
tree := fsedsl.BuildTree(spec, Ctx{}) tree := virtfs.BuildTree(spec, Ctx{})
// Use the tree // Use the tree
entries, _ := tree.List() entries, _ := tree.List()
@ -112,7 +112,7 @@ Define your context type, then create type and function aliases that bake it in:
```go ```go
package fs package fs
import "git.lneely.de/lkn/fsedsl" import "git.lneely.de/lkn/virtfs"
// Ctx carries runtime dependencies for handler functions. // Ctx carries runtime dependencies for handler functions.
type Ctx struct { type Ctx struct {
@ -122,19 +122,19 @@ type Ctx struct {
// Type aliases for external consumers // Type aliases for external consumers
type ( type (
FsNodeDecl = fsedsl.FsNodeDecl[Ctx] FsNodeDecl = virtfs.FsNodeDecl[Ctx]
Binding = fsedsl.Binding[Ctx] Binding = virtfs.Binding[Ctx]
Tree = fsedsl.Tree Tree = virtfs.Tree
) )
// Function aliases for clean spec definitions // Function aliases for clean spec definitions
var ( var (
DirNode = fsedsl.DirNode[Ctx] DirNode = virtfs.DirNode[Ctx]
FileNode = fsedsl.FileNode[Ctx] FileNode = virtfs.FileNode[Ctx]
Each = fsedsl.Each[Ctx] Each = virtfs.Each[Ctx]
Doc = fsedsl.Doc[Ctx] Doc = virtfs.Doc[Ctx]
Read = fsedsl.Read[Ctx] Read = virtfs.Read[Ctx]
Write = fsedsl.Write[Ctx] Write = virtfs.Write[Ctx]
// ... add others as needed // ... add others as needed
) )
@ -172,7 +172,7 @@ func userBindings(ctx Ctx) ([]Binding, error) {
Build and use: Build and use:
```go ```go
tree := fsedsl.BuildTree(fs.Spec, fs.Ctx{DB: db}) tree := virtfs.BuildTree(fs.Spec, fs.Ctx{DB: db})
entries, _ := tree.List() entries, _ := tree.List()
f, _ := tree.Open("alice/name") f, _ := tree.Open("alice/name")
data, _ := f.Read() data, _ := f.Read()
@ -208,7 +208,7 @@ data, _ := f.Read()
### Binding ### Binding
```go ```go
fsedsl.Binding[Ctx]{ virtfs.Binding[Ctx]{
Name: "filename", // Primary name Name: "filename", // Primary name
Aliases: []string{"alt-name"}, // Alternate lookup names Aliases: []string{"alt-name"}, // Alternate lookup names
UID: "owner", // Owner (optional) UID: "owner", // Owner (optional)
@ -225,7 +225,7 @@ fsedsl.Binding[Ctx]{
### Tree Operations ### Tree Operations
```go ```go
tree := fsedsl.BuildTree(spec, rootCtx) tree := virtfs.BuildTree(spec, rootCtx)
tree.List() // List entries tree.List() // List entries
tree.Stat(name) // Get file info tree.Stat(name) // Get file info
@ -239,7 +239,7 @@ tree.Readdir(path) // List subdirectory
### Help Generation ### Help Generation
```go ```go
help := fsedsl.GenerateHelp(spec) help := virtfs.GenerateHelp(spec)
// Returns formatted documentation from Doc() annotations // Returns formatted documentation from Doc() annotations
``` ```

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
// Package fsedsl provides an embedded domain-specific language for declaring // Package virtfs provides an embedded domain-specific language for declaring
// synthetic filesystems. It is designed for building 9P namespaces where the // synthetic filesystems. It is designed for building 9P namespaces where the
// entire filesystem structure is declared statically but populated dynamically. // entire filesystem structure is declared statically but populated dynamically.
// //
@ -30,7 +30,7 @@
// ) // )
// //
// tree := BuildTree(spec) // tree := BuildTree(spec)
package fsedsl package virtfs
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"context" "context"

3
virtfs/go.mod Normal file
View File

@ -0,0 +1,3 @@
module ollie/virtfs
go 1.25.6

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package fsedsl package virtfs
import ( import (
"context" "context"