toolsrv: migrate to closure-capture fsedsl API

Spec() now takes *Server and all handlers are closures that capture it.
Removed Ctx type, type aliases, and separate handler functions.
The tree structure and behavior are declared together in one place.
This commit is contained in:
Ollie Agent 2026-08-11 16:56:34 +02:00
parent f4a6829d89
commit 53b4618561
3 changed files with 86 additions and 151 deletions

View File

@ -11,162 +11,99 @@ import (
"ollie/fsedsl"
)
// Ctx is the context passed through the fsedsl tree.
type Ctx struct {
Server *Server
Proc *Proc // set when traversing into /proc/{pid}
}
// Type aliases from fsedsl, specialized to Ctx.
type (
FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
Binding = fsedsl.Binding[Ctx]
)
// Specialized fsedsl constructors for Ctx.
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]
BlockOnce = fsedsl.BlockOnce[Ctx]
Request = fsedsl.Request[Ctx]
)
// Spec returns the fsedsl specification for the toolsrv namespace.
func Spec() FsNodeDecl {
return DirNode("/",
FileNode("ctl", 0222,
Doc("Control: write 'load <tool>' or 'unload <tool>'"),
Write(handleCtlWrite),
// 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 {
return srv.Fs.HandleCtl(strings.TrimSpace(string(data)))
}),
),
FileNode("tools", 0666,
Doc("Tools: write agent ID, read tool list (rdwr)"),
Request(handleToolsRequest),
fsedsl.FileNode("tools", 0666,
fsedsl.Doc("Tools: write agent ID, read tool list (rdwr)"),
fsedsl.Request(func(data []byte) ([]byte, error) {
agentID := strings.TrimSpace(string(data))
if agentID == "" {
return nil, fmt.Errorf("agent ID required")
}
return []byte(srv.Fs.HandleToolsRequest(agentID)), nil
}),
),
FileNode("info", 0444,
Doc("Host info: platform, arch"),
Read(handleInfoRead),
fsedsl.FileNode("info", 0444,
fsedsl.Doc("Host info: platform, arch"),
fsedsl.Read(func() ([]byte, error) {
return []byte(srv.HostInfo()), nil
}),
),
DirNode("proc",
FileNode("new", 0666,
Doc("Execute tool: write token + tool + args, read result (blocking)"),
Request(handleProcNew),
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) {
result, err := srv.Fs.HandleProcNew(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}),
),
FileNode("new.bg", 0666,
Doc("Execute tool in background: write token + tool + args, read pid"),
Request(handleProcNewBg),
),
Each("{pid}", procBindings,
FileNode("out", 0444,
Doc("Process output"),
Read(handleProcOutRead),
),
FileNode("wait", 0444,
Doc("Block until process exits, returns exit code"),
BlockOnce(handleProcWait),
),
FileNode("stat", 0444,
Doc("Process status: running/exited, runtime, tool"),
Read(handleProcStatRead),
),
FileNode("ctl", 0222,
Doc("Process control: write 'signal <N>' or 'dismiss'"),
Write(handleProcCtlWrite),
),
fsedsl.FileNode("new.bg", 0666,
fsedsl.Doc("Execute tool in background: write token + tool + args, read pid"),
fsedsl.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNewBg(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}),
),
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) {
pids := srv.Fs.ListProcs()
var bindings []fsedsl.Binding
for _, pid := range pids {
proc := srv.Fs.GetProc(pid)
if proc == nil {
continue
}
p := proc
bindings = append(bindings, fsedsl.Binding{
Name: strconv.Itoa(pid),
Children: []fsedsl.FsNodeDecl{
fsedsl.FileNode("out", 0444,
fsedsl.Doc("Process output"),
fsedsl.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) {
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) {
return []byte(p.Stat()), nil
}),
),
fsedsl.FileNode("ctl", 0222,
fsedsl.Doc("Process control: write 'signal <N>' or 'dismiss'"),
fsedsl.Write(func(data []byte) error {
return srv.Fs.HandleProcCtl(p.ID, strings.TrimSpace(string(data)))
}),
),
},
})
}
return bindings, nil
}),
),
)
}
// --- Handlers ---
func handleCtlWrite(ctx Ctx, data []byte) error {
return ctx.Server.Fs.HandleCtl(strings.TrimSpace(string(data)))
}
func handleToolsRequest(ctx Ctx, data []byte) ([]byte, error) {
agentID := strings.TrimSpace(string(data))
if agentID == "" {
return nil, fmt.Errorf("agent ID required")
}
return []byte(ctx.Server.Fs.HandleToolsRequest(agentID)), nil
}
func handleInfoRead(ctx Ctx) ([]byte, error) {
return []byte(ctx.Server.HostInfo()), nil
}
func handleProcNew(ctx Ctx, data []byte) ([]byte, error) {
result, err := ctx.Server.Fs.HandleProcNew(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}
func handleProcNewBg(ctx Ctx, data []byte) ([]byte, error) {
result, err := ctx.Server.Fs.HandleProcNewBg(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}
// procBindings returns bindings for each running process.
func procBindings(ctx Ctx) ([]Binding, error) {
pids := ctx.Server.Fs.ListProcs()
var bindings []Binding
for _, pid := range pids {
proc := ctx.Server.Fs.GetProc(pid)
if proc == nil {
continue
}
p := proc // capture for closure
bindings = append(bindings, Binding{
Name: strconv.Itoa(pid),
Applier: func(c Ctx) Ctx {
c.Proc = p
return c
},
})
}
return bindings, nil
}
func handleProcOutRead(ctx Ctx) ([]byte, error) {
if ctx.Proc == nil {
return nil, fmt.Errorf("no process context")
}
return []byte(ctx.Proc.Output()), nil
}
func handleProcWait(ctx Ctx, cctx context.Context, base string) ([]byte, string, error) {
if ctx.Proc == nil {
return nil, "", fmt.Errorf("no process context")
}
exitCode := ctx.Proc.Wait()
return []byte(fmt.Sprintf("%d\n", exitCode)), "", nil
}
func handleProcStatRead(ctx Ctx) ([]byte, error) {
if ctx.Proc == nil {
return nil, fmt.Errorf("no process context")
}
return []byte(ctx.Proc.Stat()), nil
}
func handleProcCtlWrite(ctx Ctx, data []byte) error {
if ctx.Proc == nil {
return fmt.Errorf("no process context")
}
return ctx.Server.Fs.HandleProcCtl(ctx.Proc.ID, strings.TrimSpace(string(data)))
}
// Mode constants for convenience.
const (
ModeReadOnly os.FileMode = 0444

View File

@ -84,8 +84,7 @@ func (s *Server) HostInfo() string {
// BuildTree creates the fsedsl tree for this server.
func (s *Server) BuildTree() *fsedsl.Tree {
ctx := Ctx{Server: s}
return fsedsl.BuildTree(Spec(), ctx)
return fsedsl.BuildTree(Spec(s))
}
// GenerateSecret generates a random shared secret for toolsrv auth.

View File

@ -27,7 +27,7 @@ import (
"ollie/cmd/toolsrv/internal/fs"
"ollie/cmd/toolsrv/internal/registry"
"ollie/env"
"ollie/fsedsl"
olog "ollie/log"
"ollie/paths"
)
@ -88,8 +88,7 @@ func main() {
}
// Build the filesystem tree
ctx := fs.Ctx{Server: srv}
tree := fsedsl.BuildTree(fs.Spec(), ctx)
tree := srv.BuildTree()
if tree == nil {
logger.Error("failed to build filesystem tree")
os.Exit(1)