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

View File

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

View File

@ -11,29 +11,29 @@ import (
"ollie/cmd/olliesrv/internal/agent"
"ollie/cmd/olliesrv/internal/bypass"
"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 (
Tree = fsedsl.Tree
File = fsedsl.File
SyntheticFileInfo = fsedsl.SyntheticFileInfo
Tree = virtfs.Tree
File = virtfs.File
SyntheticFileInfo = virtfs.SyntheticFileInfo
)
// Structural permissions.
const (
PermChildDir = fsedsl.PermChildDir
PermIdx = fsedsl.PermIdx
PermMkdir = fsedsl.PermMkdir
PermMkdirPrivate = fsedsl.PermMkdirPrivate
PermChildDir = virtfs.PermChildDir
PermIdx = virtfs.PermIdx
PermMkdir = virtfs.PermMkdir
PermMkdirPrivate = virtfs.PermMkdirPrivate
)
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.
func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
func buildTreeSpec(rs *RootState) virtfs.FsNodeDecl {
cfg := &rs.Cfg
mc := cfg.ModelCache
broker := cfg.BypassBroker
@ -45,71 +45,71 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
treeSpecUID = u.Username
}
return fsedsl.DirNode("/",
fsedsl.UID(treeSpecUID),
fsedsl.GID(treeSpecGID),
return virtfs.DirNode("/",
virtfs.UID(treeSpecUID),
virtfs.GID(treeSpecGID),
fsedsl.FileNode("backends", 0444,
fsedsl.Doc("Available backend names, one per line"),
fsedsl.Read(readBackends),
virtfs.FileNode("backends", 0444,
virtfs.Doc("Available backend names, one per line"),
virtfs.Read(readBackends),
),
fsedsl.FileNode("help", 0444,
fsedsl.Doc("Filesystem reference"),
fsedsl.Read(readHelp),
virtfs.FileNode("help", 0444,
virtfs.Doc("Filesystem reference"),
virtfs.Read(readHelp),
),
fsedsl.FileNode("models", 0444,
fsedsl.Doc("Available models. Format: backend<tab>model per line."),
fsedsl.Read(func() ([]byte, error) { return readModels(mc) }),
virtfs.FileNode("models", 0444,
virtfs.Doc("Available models. Format: backend<tab>model per line."),
virtfs.Read(func() ([]byte, error) { return readModels(mc) }),
),
fsedsl.FileNode("agents", 0444,
fsedsl.Doc("Available agent profiles, one per line"),
fsedsl.Read(readAgents),
virtfs.FileNode("agents", 0444,
virtfs.Doc("Available agent profiles, one per line"),
virtfs.Read(readAgents),
),
fsedsl.FileNode("tools", 0444,
fsedsl.Doc("Available tools. Format: name<spaces>description per line."),
fsedsl.Read(readTools),
virtfs.FileNode("tools", 0444,
virtfs.Doc("Available tools. Format: name<spaces>description per line."),
virtfs.Read(readTools),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"),
fsedsl.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)),
virtfs.FileNode("ctl", 0666,
virtfs.Doc("Server control. Write: 'invalidate', 'kill'"),
virtfs.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)),
),
fsedsl.FileNode("eventwait", 0444,
fsedsl.Doc("Blocking read for server events"),
fsedsl.Read(readEventwait),
fsedsl.BlockOnce(blockEventwait),
virtfs.FileNode("eventwait", 0444,
virtfs.Doc("Blocking read for server events"),
virtfs.Read(readEventwait),
virtfs.BlockOnce(blockEventwait),
),
fsedsl.FileNode("generate", 0666,
fsedsl.Doc("One-shot LLM generation"),
fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }),
virtfs.FileNode("generate", 0666,
virtfs.Doc("One-shot LLM generation"),
virtfs.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }),
),
fsedsl.FileNode("aliases", 0444,
fsedsl.Doc("Alias table. Format: id<tab>path per line."),
fsedsl.Read(readAliases),
virtfs.FileNode("aliases", 0444,
virtfs.Doc("Alias table. Format: id<tab>path per line."),
virtfs.Read(readAliases),
),
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(broker) }),
fsedsl.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }),
virtfs.DirNode("bypass",
virtfs.Doc("Bypass broker for sandboxed command approval"),
virtfs.FileNode("policy", 0666,
virtfs.Doc("Global bypass policy"),
virtfs.Read(func() ([]byte, error) { return readBypassPolicy(broker) }),
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)
}),
),
fsedsl.DirNode("session",
fsedsl.Doc("Session management"),
fsedsl.FileNode("new", 0666,
fsedsl.Doc("Create session"),
fsedsl.Request(requestSessionNew),
virtfs.DirNode("session",
virtfs.Doc("Session management"),
virtfs.FileNode("new", 0666,
virtfs.Doc("Create session"),
virtfs.Request(requestSessionNew),
),
fsedsl.FileNode("idx", 0444,
fsedsl.Doc("Session index"),
fsedsl.Read(readSessionIdx),
virtfs.FileNode("idx", 0444,
virtfs.Doc("Session index"),
virtfs.Read(readSessionIdx),
),
fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) {
virtfs.Each("{sname}", func() ([]virtfs.Binding, error) {
return buildSessionBindings(rs, broker)
}),
),
@ -117,9 +117,9 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
}
// 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()
var out []fsedsl.Binding
var out []virtfs.Binding
for name, sess := range sessions {
node := rs.GetOrCreateNode(sess)
@ -138,7 +138,7 @@ func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]fsedsl.Bindin
return nil
}
out = append(out, fsedsl.Binding{
out = append(out, virtfs.Binding{
Name: sess.Name(),
Aliases: []string{sess.ID},
Remove: removeFn,
@ -156,39 +156,39 @@ func buildSessionChildren(
broker *bypass.Broker,
removeFn func(),
renameFn func(string) error,
) []fsedsl.FsNodeDecl {
return []fsedsl.FsNodeDecl{
fsedsl.FileNode("env", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionEnv(s) }),
) []virtfs.FsNodeDecl {
return []virtfs.FsNodeDecl{
virtfs.FileNode("env", 0444,
virtfs.Read(func() ([]byte, error) { return readSessionEnv(s) }),
),
fsedsl.FileNode("paused", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionPaused(s) }),
virtfs.FileNode("paused", 0444,
virtfs.Read(func() ([]byte, error) { return readSessionPaused(s) }),
),
fsedsl.FileNode("connected", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionConnected(s) }),
virtfs.FileNode("connected", 0444,
virtfs.Read(func() ([]byte, error) { return readSessionConnected(s) }),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Request(sessionCtlHandler(s, removeFn, renameFn)),
virtfs.FileNode("ctl", 0666,
virtfs.Request(sessionCtlHandler(s, removeFn, renameFn)),
),
fsedsl.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionName(s) }),
fsedsl.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }),
virtfs.FileNode("name", 0666,
virtfs.Read(func() ([]byte, error) { return readSessionName(s) }),
virtfs.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }),
),
fsedsl.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readSessionID(s) }),
virtfs.FileNode("id", 0444,
virtfs.Read(func() ([]byte, error) { return readSessionID(s) }),
),
fsedsl.FileNode("bypass", 0666,
fsedsl.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }),
fsedsl.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }),
virtfs.FileNode("bypass", 0666,
virtfs.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }),
virtfs.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }),
),
fsedsl.DirNode("agent",
fsedsl.FileNode("new", 0666,
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }),
virtfs.DirNode("agent",
virtfs.FileNode("new", 0666,
virtfs.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }),
),
fsedsl.FileNode("idx", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }),
virtfs.FileNode("idx", 0444,
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)
}),
),
@ -196,9 +196,9 @@ func buildSessionChildren(
}
// 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()
var out []fsedsl.Binding
var out []virtfs.Binding
for _, ag := range agents {
a := ag
@ -209,7 +209,7 @@ func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error)
wireAgentEvents(s.ID, a, al)
}
out = append(out, fsedsl.Binding{
out = append(out, virtfs.Binding{
Name: a.Name(),
Aliases: []string{a.ID()},
Remove: func() {
@ -223,67 +223,67 @@ func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error)
}
// buildAgentChildren returns the file nodes for a single agent.
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []fsedsl.FsNodeDecl {
return []fsedsl.FsNodeDecl{
fsedsl.FileNode("prompt", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }),
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []virtfs.FsNodeDecl {
return []virtfs.FsNodeDecl{
virtfs.FileNode("prompt", 0666,
virtfs.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }),
),
fsedsl.FileNode("fifo", 0666,
fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }),
fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(a) }),
virtfs.FileNode("fifo", 0666,
virtfs.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }),
virtfs.Read(func() ([]byte, error) { return readAgentFifoOut(a) }),
),
fsedsl.FileNode("chat", 0444,
fsedsl.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChatText(al) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
virtfs.FileNode("chat", 0444,
virtfs.StatOverride(statAgentChat),
virtfs.Read(func() ([]byte, error) { return readAgentChatText(al) }),
virtfs.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChatText(s, al, cctx, base)
}),
),
fsedsl.FileNode("chat.raw", 0444,
fsedsl.StatOverride(statAgentChat),
fsedsl.Read(func() ([]byte, error) { return readAgentChat(al) }),
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
virtfs.FileNode("chat.raw", 0444,
virtfs.StatOverride(statAgentChat),
virtfs.Read(func() ([]byte, error) { return readAgentChat(al) }),
virtfs.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
return streamAgentChat(s, al, cctx, base)
}),
),
fsedsl.FileNode("statewait", 0444,
fsedsl.Read(func() ([]byte, error) {
virtfs.FileNode("statewait", 0444,
virtfs.Read(func() ([]byte, error) {
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)
}),
),
fsedsl.FileNode("log", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentLog(al) }),
virtfs.FileNode("log", 0444,
virtfs.Read(func() ([]byte, error) { return readAgentLog(al) }),
),
fsedsl.FileNode("plan", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentPlan(al) }),
fsedsl.Write(func(data []byte) error { return writeAgentPlan(al, data) }),
virtfs.FileNode("plan", 0666,
virtfs.Read(func() ([]byte, error) { return readAgentPlan(al) }),
virtfs.Write(func(data []byte) error { return writeAgentPlan(al, data) }),
),
fsedsl.FileNode("cfg", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }),
fsedsl.Write(func(data []byte) error { return writeAgentCfg(a, data) }),
virtfs.FileNode("cfg", 0666,
virtfs.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }),
virtfs.Write(func(data []byte) error { return writeAgentCfg(a, data) }),
),
fsedsl.FileNode("ctl", 0666,
fsedsl.Request(agentCtlHandler(s, a)),
virtfs.FileNode("ctl", 0666,
virtfs.Request(agentCtlHandler(s, a)),
),
fsedsl.FileNode("stats", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentStats(a) }),
virtfs.FileNode("stats", 0444,
virtfs.Read(func() ([]byte, error) { return readAgentStats(a) }),
),
fsedsl.FileNode("tools", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentTools(a) }),
fsedsl.Write(func(data []byte) error { return writeAgentTools(a, data) }),
virtfs.FileNode("tools", 0666,
virtfs.Read(func() ([]byte, error) { return readAgentTools(a) }),
virtfs.Write(func(data []byte) error { return writeAgentTools(a, data) }),
),
fsedsl.FileNode("name", 0666,
fsedsl.Read(func() ([]byte, error) { return readAgentName(a) }),
fsedsl.Write(func(data []byte) error { return writeAgentName(s, a, data) }),
virtfs.FileNode("name", 0666,
virtfs.Read(func() ([]byte, error) { return readAgentName(a) }),
virtfs.Write(func(data []byte) error { return writeAgentName(s, a, data) }),
),
fsedsl.FileNode("id", 0444,
fsedsl.Read(func() ([]byte, error) { return readAgentID(a) }),
virtfs.FileNode("id", 0444,
virtfs.Read(func() ([]byte, error) { return readAgentID(a) }),
),
fsedsl.DirNode("proc",
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) {
virtfs.DirNode("proc",
virtfs.Each("{pid}", func() ([]virtfs.Binding, error) {
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.
func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) {
func adaptBypassBindings(broker *bypass.Broker) ([]virtfs.Binding, error) {
if broker == nil {
return nil, errNoBypass
}
pending := broker.Pending()
var out []fsedsl.Binding
var out []virtfs.Binding
for _, r := range pending {
req := r
out = append(out, fsedsl.Binding{
out = append(out, virtfs.Binding{
Name: req.ID,
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("request", 0666,
fsedsl.Read(func() ([]byte, error) {
Children: []virtfs.FsNodeDecl{
virtfs.FileNode("request", 0666,
virtfs.Read(func() ([]byte, error) {
return readBypassRequest(req)
}),
fsedsl.Write(func(data []byte) error {
virtfs.Write(func(data []byte) error {
return writeBypassRequest(broker, req, data)
}),
),
@ -317,7 +317,7 @@ func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) {
}
// adaptProcessBindings returns bindings for detached processes.
func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) {
func adaptProcessBindings(s *SessionNode) ([]virtfs.Binding, error) {
if s == nil {
return nil, nil
}
@ -327,26 +327,26 @@ func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) {
}
defer conn.Close()
raw := conn.ListDetachedRaw()
var out []fsedsl.Binding
var out []virtfs.Binding
for _, r := range raw {
if m, ok := r.(map[string]any); ok {
if id, ok := m["pid"].(int); ok {
pid := id
out = append(out, fsedsl.Binding{
out = append(out, virtfs.Binding{
Name: strconv.Itoa(pid),
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("out", 0444,
fsedsl.Read(func() ([]byte, error) {
Children: []virtfs.FsNodeDecl{
virtfs.FileNode("out", 0444,
virtfs.Read(func() ([]byte, error) {
return readProcess(s, pid)
}),
),
fsedsl.FileNode("ctl", 0222,
fsedsl.Write(func(data []byte) error {
virtfs.FileNode("ctl", 0222,
virtfs.Write(func(data []byte) error {
return writeProcessCtl(s, pid, data)
}),
),
fsedsl.FileNode("status", 0444,
fsedsl.Read(func() ([]byte, error) {
virtfs.FileNode("status", 0444,
virtfs.Read(func() ([]byte, error) {
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
import (
@ -8,22 +8,22 @@ import (
"strconv"
"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.
func Spec(srv *Server) fsedsl.FsNodeDecl {
return fsedsl.DirNode("/",
fsedsl.FileNode("ctl", 0222,
fsedsl.Doc("Control: write 'load <agentID> <tool>' or 'unload <agentID> <tool>'"),
fsedsl.Write(func(data []byte) error {
func Spec(srv *Server) virtfs.FsNodeDecl {
return virtfs.DirNode("/",
virtfs.FileNode("ctl", 0222,
virtfs.Doc("Control: write 'load <agentID> <tool>' or 'unload <agentID> <tool>'"),
virtfs.Write(func(data []byte) error {
return srv.Fs.HandleCtl(strings.TrimSpace(string(data)))
}),
),
fsedsl.FileNode("tools", 0666,
fsedsl.Doc("Tools: write agent ID, read tool list (rdwr)"),
fsedsl.Request(func(data []byte) ([]byte, error) {
virtfs.FileNode("tools", 0666,
virtfs.Doc("Tools: write agent ID, read tool list (rdwr)"),
virtfs.Request(func(data []byte) ([]byte, error) {
agentID := strings.TrimSpace(string(data))
if agentID == "" {
return nil, fmt.Errorf("agent ID required")
@ -31,16 +31,16 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(srv.Fs.HandleToolsRequest(agentID)), nil
}),
),
fsedsl.FileNode("info", 0444,
fsedsl.Doc("Host info: platform, arch"),
fsedsl.Read(func() ([]byte, error) {
virtfs.FileNode("info", 0444,
virtfs.Doc("Host info: platform, arch"),
virtfs.Read(func() ([]byte, error) {
return []byte(srv.HostInfo()), nil
}),
),
fsedsl.DirNode("proc",
fsedsl.FileNode("new", 0666,
fsedsl.Doc("Execute tool: write token + tool + args, read result (blocking)"),
fsedsl.Request(func(data []byte) ([]byte, error) {
virtfs.DirNode("proc",
virtfs.FileNode("new", 0666,
virtfs.Doc("Execute tool: write token + tool + args, read result (blocking)"),
virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNew(context.Background(), string(data))
if err != nil {
return nil, err
@ -48,9 +48,9 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(result), nil
}),
),
fsedsl.FileNode("new.bg", 0666,
fsedsl.Doc("Execute tool in background: write token + tool + args, read pid"),
fsedsl.Request(func(data []byte) ([]byte, error) {
virtfs.FileNode("new.bg", 0666,
virtfs.Doc("Execute tool in background: write token + tool + args, read pid"),
virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNewBg(context.Background(), string(data))
if err != nil {
return nil, err
@ -58,40 +58,40 @@ func Spec(srv *Server) fsedsl.FsNodeDecl {
return []byte(result), nil
}),
),
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) {
virtfs.Each("{pid}", func() ([]virtfs.Binding, error) {
pids := srv.Fs.ListProcs()
var bindings []fsedsl.Binding
var bindings []virtfs.Binding
for _, pid := range pids {
proc := srv.Fs.GetProc(pid)
if proc == nil {
continue
}
p := proc
bindings = append(bindings, fsedsl.Binding{
bindings = append(bindings, virtfs.Binding{
Name: strconv.Itoa(pid),
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("out", 0444,
fsedsl.Doc("Process output"),
fsedsl.Read(func() ([]byte, error) {
Children: []virtfs.FsNodeDecl{
virtfs.FileNode("out", 0444,
virtfs.Doc("Process output"),
virtfs.Read(func() ([]byte, error) {
return []byte(p.Output()), nil
}),
),
fsedsl.FileNode("wait", 0444,
fsedsl.Doc("Block until process exits, returns exit code"),
fsedsl.BlockOnce(func(_ context.Context, _ string) ([]byte, string, error) {
virtfs.FileNode("wait", 0444,
virtfs.Doc("Block until process exits, returns exit code"),
virtfs.BlockOnce(func(_ context.Context, _ string) ([]byte, string, error) {
exitCode := p.Wait()
return []byte(fmt.Sprintf("%d\n", exitCode)), "", nil
}),
),
fsedsl.FileNode("stat", 0444,
fsedsl.Doc("Process status: running/exited, runtime, tool"),
fsedsl.Read(func() ([]byte, error) {
virtfs.FileNode("stat", 0444,
virtfs.Doc("Process status: running/exited, runtime, tool"),
virtfs.Read(func() ([]byte, error) {
return []byte(p.Stat()), nil
}),
),
fsedsl.FileNode("ctl", 0222,
fsedsl.Doc("Process control: write 'signal <N>' or 'dismiss'"),
fsedsl.Write(func(data []byte) error {
virtfs.FileNode("ctl", 0222,
virtfs.Doc("Process control: write 'signal <N>' or 'dismiss'"),
virtfs.Write(func(data []byte) error {
return srv.Fs.HandleProcCtl(p.ID, strings.TrimSpace(string(data)))
}),
),

View File

@ -9,7 +9,7 @@ import (
"sync"
"ollie/cmd/toolsrv/internal/registry"
"ollie/fsedsl"
"ollie/virtfs"
)
// 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)
}
// BuildTree creates the fsedsl tree for this server.
func (s *Server) BuildTree() *fsedsl.Tree {
return fsedsl.BuildTree(Spec(s))
// BuildTree creates the virtfs tree for this server.
func (s *Server) BuildTree() *virtfs.Tree {
return virtfs.BuildTree(Spec(s))
}
// GenerateSecret generates a random shared secret for toolsrv auth.

View File

@ -10,7 +10,7 @@ import (
"sync"
"ollie/cmd/toolsrv/internal/fs"
"ollie/fsedsl"
"ollie/virtfs"
"9fans.net/go/plan9"
)
@ -21,7 +21,7 @@ type fid struct {
qid plan9.Qid
mode uint8
opened bool
entry fsedsl.File
entry virtfs.File
readBuf []byte // buffer for read data
readDone bool // true if file has been read
dirCache []byte
@ -46,8 +46,8 @@ type connState struct {
noAuth bool // skip authentication (for debugging)
}
// serve9P handles a single 9P connection using the fsedsl tree.
func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree, srv *fs.Server, noAuth bool) {
// serve9P handles a single 9P connection using the virtfs tree.
func serve9P(ctx context.Context, conn net.Conn, tree *virtfs.Tree, srv *fs.Server, noAuth bool) {
defer conn.Close()
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 {
case plan9.Tversion:
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)
if cs.noAuth {
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]}
}
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()
f, ok := cs.fids[fc.Fid]
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}
}
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()
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}
}
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
cs.mu.RLock()
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]}
}
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 {
path := f.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]}
}
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
cs.mu.Lock()
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}
}
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()
f, ok := cs.fids[fc.Fid]
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
golang.org/x/time v0.15.0
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
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.
@ -14,19 +14,19 @@ The library produces a generic `Tree` that can be used with any filesystem proto
## 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`:
```bash
# Clone or copy the fsedsl directory to your project, then:
go mod edit -replace=git.lneely.de/lkn/fsedsl=./fsedsl
# Clone or copy the virtfs directory to your project, then:
go mod edit -replace=git.lneely.de/lkn/virtfs=./virtfs
```
Or reference it directly from the ollie repo:
```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
@ -36,7 +36,7 @@ package main
import (
"fmt"
"git.lneely.de/lkn/fsedsl"
"git.lneely.de/lkn/virtfs"
)
type Ctx struct {
@ -55,18 +55,18 @@ var users = []*User{
func main() {
// Declare the filesystem spec
spec := fsedsl.DirNode[Ctx]("/",
fsedsl.FileNode[Ctx]("version", 0444,
fsedsl.Doc[Ctx]("API version"),
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
spec := virtfs.DirNode[Ctx]("/",
virtfs.FileNode[Ctx]("version", 0444,
virtfs.Doc[Ctx]("API version"),
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0\n"), nil
}),
),
fsedsl.Each[Ctx]("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) {
var out []fsedsl.Binding[Ctx]
virtfs.Each[Ctx]("{user}", func(ctx Ctx) ([]virtfs.Binding[Ctx], error) {
var out []virtfs.Binding[Ctx]
for _, u := range users {
user := u // capture
out = append(out, fsedsl.Binding[Ctx]{
out = append(out, virtfs.Binding[Ctx]{
Name: user.Name,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx {
@ -77,13 +77,13 @@ func main() {
}
return out, nil
},
fsedsl.FileNode[Ctx]("id", 0444,
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
virtfs.FileNode[Ctx]("id", 0444,
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.ID + "\n"), nil
}),
),
fsedsl.FileNode[Ctx]("name", 0444,
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
virtfs.FileNode[Ctx]("name", 0444,
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
@ -91,7 +91,7 @@ func main() {
)
// Build the tree
tree := fsedsl.BuildTree(spec, Ctx{})
tree := virtfs.BuildTree(spec, Ctx{})
// Use the tree
entries, _ := tree.List()
@ -112,7 +112,7 @@ Define your context type, then create type and function aliases that bake it in:
```go
package fs
import "git.lneely.de/lkn/fsedsl"
import "git.lneely.de/lkn/virtfs"
// Ctx carries runtime dependencies for handler functions.
type Ctx struct {
@ -122,19 +122,19 @@ type Ctx struct {
// Type aliases for external consumers
type (
FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
Binding = fsedsl.Binding[Ctx]
Tree = fsedsl.Tree
FsNodeDecl = virtfs.FsNodeDecl[Ctx]
Binding = virtfs.Binding[Ctx]
Tree = virtfs.Tree
)
// Function aliases for clean spec definitions
var (
DirNode = fsedsl.DirNode[Ctx]
FileNode = fsedsl.FileNode[Ctx]
Each = fsedsl.Each[Ctx]
Doc = fsedsl.Doc[Ctx]
Read = fsedsl.Read[Ctx]
Write = fsedsl.Write[Ctx]
DirNode = virtfs.DirNode[Ctx]
FileNode = virtfs.FileNode[Ctx]
Each = virtfs.Each[Ctx]
Doc = virtfs.Doc[Ctx]
Read = virtfs.Read[Ctx]
Write = virtfs.Write[Ctx]
// ... add others as needed
)
@ -172,7 +172,7 @@ func userBindings(ctx Ctx) ([]Binding, error) {
Build and use:
```go
tree := fsedsl.BuildTree(fs.Spec, fs.Ctx{DB: db})
tree := virtfs.BuildTree(fs.Spec, fs.Ctx{DB: db})
entries, _ := tree.List()
f, _ := tree.Open("alice/name")
data, _ := f.Read()
@ -208,7 +208,7 @@ data, _ := f.Read()
### Binding
```go
fsedsl.Binding[Ctx]{
virtfs.Binding[Ctx]{
Name: "filename", // Primary name
Aliases: []string{"alt-name"}, // Alternate lookup names
UID: "owner", // Owner (optional)
@ -225,7 +225,7 @@ fsedsl.Binding[Ctx]{
### Tree Operations
```go
tree := fsedsl.BuildTree(spec, rootCtx)
tree := virtfs.BuildTree(spec, rootCtx)
tree.List() // List entries
tree.Stat(name) // Get file info
@ -239,7 +239,7 @@ tree.Readdir(path) // List subdirectory
### Help Generation
```go
help := fsedsl.GenerateHelp(spec)
help := virtfs.GenerateHelp(spec)
// Returns formatted documentation from Doc() annotations
```

View File

@ -1,4 +1,4 @@
package fsedsl
package virtfs
import (
"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
// entire filesystem structure is declared statically but populated dynamically.
//
@ -30,7 +30,7 @@
// )
//
// tree := BuildTree(spec)
package fsedsl
package virtfs
import (
"context"

View File

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

View File

@ -1,4 +1,4 @@
package fsedsl
package virtfs
import (
"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 (
"fmt"

View File

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

View File

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