- doc/boot-sequence.md: init chain from main → fs.NewRoot → session.Init - Event protocol: comprehensive table on Event struct (all roles, semantics) - RPC server moved from cmd/ollie-remote into toolsrv/rpcserver.go (binary is now a 93-line thin wrapper) - SetSessionEnv folded into NewAgent (no post-construction wiring needed) - AgentCfg → AgentParams (distinguish from AgentConfig JSON schema) - ToolResult/ToolResultContent shared types in toolsrv/rpcwire.go - toolsrv/shell.go → toolsrv/exec.go (name matches purpose) - Resume unified: single path, always through Keeper - AGENTS.md: fix fs/builder.go → fsedsl/builder.go, handlers_*.go - fsedsl/Tree: document dual-mode (EDSL lazy vs Manual Mount) - Truncation stack documented at defaultToolResultMaxBytes - Integration test rewritten to use srv.ServeRPC directly |
||
|---|---|---|
| .. | ||
| LICENSE.md | ||
| README.md | ||
| builder.go | ||
| decl.go | ||
| decl_test.go | ||
| file.go | ||
| go.mod | ||
| help.go | ||
| tree.go | ||
| tree_test.go | ||
README.md
fsedsl
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.
The library produces a generic Tree that can be used with any filesystem protocol (9P, FUSE, etc.) or as an in-memory filesystem for testing.
Features
- Declarative filesystem specification: Define your namespace structure with
DirNode,FileNode, andEachconstructors - Generic context propagation: Type-safe context passing through the tree via
Binding.Applier - Dynamic bindings:
Eachnodes iterate over runtime-determined collections - Auto-generated help:
GenerateHelpproduces documentation fromDoc()annotations - Standard filesystem semantics: Implements read, write, stat, list, create, delete, rename
Installation
The library is currently embedded in the ollie repository at fsedsl/.
To use it in your project, add a replace directive to your go.mod:
# Clone or copy the fsedsl directory to your project, then:
go mod edit -replace=git.lneely.de/lkn/fsedsl=./fsedsl
Or reference it directly from the ollie repo:
go mod edit -replace=git.lneely.de/lkn/fsedsl=git.lneely.de/lkn/ollie/fsedsl@main
Quick Start
package main
import (
"fmt"
"git.lneely.de/lkn/fsedsl"
)
type Ctx struct {
User *User
}
type User struct {
ID string
Name string
}
var users = []*User{
{ID: "1", Name: "alice"},
{ID: "2", Name: "bob"},
}
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) {
return []byte("1.0\n"), nil
}),
),
fsedsl.Each[Ctx]("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) {
var out []fsedsl.Binding[Ctx]
for _, u := range users {
user := u // capture
out = append(out, fsedsl.Binding[Ctx]{
Name: user.Name,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx {
c.User = user
return c
},
})
}
return out, nil
},
fsedsl.FileNode[Ctx]("id", 0444,
fsedsl.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) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
),
)
// Build the tree
tree := fsedsl.BuildTree(spec, Ctx{})
// Use the tree
entries, _ := tree.List()
for _, e := range entries {
fmt.Println(e.Name())
}
f, _ := tree.Open("alice/name")
data, _ := f.Read()
fmt.Printf("alice/name: %s", data)
}
Usage
Define your context type, then create type and function aliases that bake it in:
package fs
import "git.lneely.de/lkn/fsedsl"
// Ctx carries runtime dependencies for handler functions.
type Ctx struct {
DB *Database
User *User
}
// Type aliases for external consumers
type (
FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
Binding = fsedsl.Binding[Ctx]
Tree = fsedsl.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]
// ... add others as needed
)
var Spec = DirNode("/",
FileNode("version", 0444,
Doc("API version"),
Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0.0\n"), nil
}),
),
Each("{user}", userBindings,
FileNode("name", 0444,
Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
),
)
func userBindings(ctx Ctx) ([]Binding, error) {
users, _ := ctx.DB.ListUsers()
var out []Binding
for _, u := range users {
user := u
out = append(out, Binding{
Name: user.Username,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx { c.User = user; return c },
})
}
return out, nil
}
Build and use:
tree := fsedsl.BuildTree(fs.Spec, fs.Ctx{DB: db})
entries, _ := tree.List()
f, _ := tree.Open("alice/name")
data, _ := f.Read()
API Reference
Constructors
| Function | Description |
|---|---|
DirNode[Ctx](name, children/options...) |
Static directory |
FileNode[Ctx](name, mode, options...) |
File with handlers |
Each[Ctx](pattern, bindingsFn, children/options...) |
Dynamic directory template |
Options
| Option | Description |
|---|---|
Read(fn) |
Read handler: func(Ctx) ([]byte, error) |
Write(fn) |
Write handler: func(Ctx, []byte) error |
Stream(fn) |
Streaming read (blocks indefinitely) |
BlockOnce(fn) |
Blocking read (returns once) |
Request(fn) |
Request/response pattern (rdwr) |
Doc(desc) |
Human-readable description |
UID(uid) |
Set owner |
GID(gid) |
Set group |
Alias(names...) |
Alternate lookup names |
StatOverride(fn) |
Custom stat handler |
CreateFile(fn) |
File creation handler (directories) |
RemoveNode(fn) |
Removal handler |
Binding
fsedsl.Binding[Ctx]{
Name: "filename", // Primary name
Aliases: []string{"alt-name"}, // Alternate lookup names
UID: "owner", // Owner (optional)
GID: "group", // Group (optional)
Applier: func(c Ctx) Ctx { // Context mutation
c.MyField = myValue
return c
},
Remove: func() { ... }, // Removal callback (optional)
Rename: func(newName string) error { ... }, // Rename callback (optional)
}
Tree Operations
tree := fsedsl.BuildTree(spec, rootCtx)
tree.List() // List entries
tree.Stat(name) // Get file info
tree.Open(name) // Open file for read/write
tree.Create(name) // Create file
tree.Delete(name) // Delete file
tree.Rename(old, new) // Rename file
tree.Readdir(path) // List subdirectory
Help Generation
help := fsedsl.GenerateHelp(spec)
// Returns formatted documentation from Doc() annotations
License
GPLv3