rename EDSL constructors for clarity

- Leaf → FileNode (file node)
- Dir → DirNode (directory node)
- Blueprint → Dynamic (state-dependent directory)

The names now clearly indicate what they create:
- FileNode: a file
- DirNode: a directory
- Dynamic: a directory whose children depend on runtime state
This commit is contained in:
Levi Neely 2026-08-04 10:45:28 +02:00
parent 99e88ef407
commit 7c6dff4a01
3 changed files with 113 additions and 113 deletions

View File

@ -5,11 +5,11 @@ import (
"testing"
)
// TestBlueprintBasic tests the basic Blueprint pattern:
// TestDynamicBasic tests the basic Blueprint pattern:
// - Declares structure statically
// - Instances function provides identity (name, aliases, UID/GID)
// - Handlers receive context with bound data
func TestBlueprintBasic(t *testing.T) {
func TestDynamicBasic(t *testing.T) {
// Test data that instances will bind to ctx
type testData struct {
ID string
@ -22,8 +22,8 @@ func TestBlueprintBasic(t *testing.T) {
}
// Create a Blueprint with static structure and dynamic instances
spec := Dir("/",
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
spec := DirNode("/",
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
var out []Instance
for _, d := range instances {
data := d // capture
@ -36,10 +36,10 @@ func TestBlueprintBasic(t *testing.T) {
}
return out, nil
},
Leaf("id", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("id", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("test-id\n"), nil
})),
Leaf("name", 0666,
FileNode("name", 0666,
Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("test-name\n"), nil
}),
@ -138,8 +138,8 @@ func TestBlueprintBasic(t *testing.T) {
}
}
// TestBlueprintNestedStructure tests nested Blueprint nodes
func TestBlueprintNestedStructure(t *testing.T) {
// TestDynamicNestedStructure tests nested Blueprint nodes
func TestDynamicNestedStructure(t *testing.T) {
// Outer instances (like sessions)
outerInstances := []struct {
ID string
@ -157,8 +157,8 @@ func TestBlueprintNestedStructure(t *testing.T) {
{ID: "agent-b", Name: "agentB"},
}
spec := Dir("/",
Blueprint("{outer}", func(ctx HandlerCtx) ([]Instance, error) {
spec := DirNode("/",
Dynamic("{outer}", func(ctx HandlerCtx) ([]Instance, error) {
var out []Instance
for _, d := range outerInstances {
data := d
@ -169,11 +169,11 @@ func TestBlueprintNestedStructure(t *testing.T) {
}
return out, nil
},
Leaf("outer-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("outer-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("outer\n"), nil
})),
Dir("inner-dir",
Blueprint("{inner}", func(ctx HandlerCtx) ([]Instance, error) {
DirNode("inner-dir",
Dynamic("{inner}", func(ctx HandlerCtx) ([]Instance, error) {
var out []Instance
for _, d := range innerInstances {
data := d
@ -184,7 +184,7 @@ func TestBlueprintNestedStructure(t *testing.T) {
}
return out, nil
},
Leaf("inner-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("inner-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("inner\n"), nil
})),
),
@ -230,13 +230,13 @@ func TestBlueprintNestedStructure(t *testing.T) {
}
}
// TestBlueprintContextPropagation tests that context is properly propagated
func TestBlueprintContextPropagation(t *testing.T) {
// TestDynamicContextPropagation tests that context is properly propagated
func TestDynamicContextPropagation(t *testing.T) {
// Use a channel to capture the context value seen by the handler
seen := make(chan string, 1)
spec := Dir("/",
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
spec := DirNode("/",
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return []Instance{
{
Name: "test-instance",
@ -245,7 +245,7 @@ func TestBlueprintContextPropagation(t *testing.T) {
},
}, nil
},
Leaf("check", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("check", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
seen <- "handler-called"
return []byte("ok\n"), nil
})),
@ -273,16 +273,16 @@ func TestBlueprintContextPropagation(t *testing.T) {
}
}
// TestBlueprintUIDGIDInheritance tests UID/GID resolution
func TestBlueprintUIDGIDInheritance(t *testing.T) {
spec := Dir("/", UID("root-uid"), GID("root-gid"),
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
// TestDynamicUIDGIDInheritance tests UID/GID resolution
func TestDynamicUIDGIDInheritance(t *testing.T) {
spec := DirNode("/", UID("root-uid"), GID("root-gid"),
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return []Instance{
{Name: "with-uid", UID: "custom-uid", GID: "custom-gid"},
{Name: "without-uid"}, // should inherit
}, nil
},
Leaf("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
@ -324,12 +324,12 @@ func TestBlueprintUIDGIDInheritance(t *testing.T) {
}
}
// TestBlueprintRename tests the rename callback on instances
func TestBlueprintRename(t *testing.T) {
// TestDynamicRename tests the rename callback on instances
func TestDynamicRename(t *testing.T) {
renamed := ""
spec := Dir("/",
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
spec := DirNode("/",
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return []Instance{
{
Name: "original",
@ -340,7 +340,7 @@ func TestBlueprintRename(t *testing.T) {
},
}, nil
},
Leaf("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
@ -359,13 +359,13 @@ func TestBlueprintRename(t *testing.T) {
}
}
// TestBlueprintEmptyInstances tests behavior when no instances exist
func TestBlueprintEmptyInstances(t *testing.T) {
spec := Dir("/",
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
// TestDynamicEmptyInstances tests behavior when no instances exist
func TestDynamicEmptyInstances(t *testing.T) {
spec := DirNode("/",
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return nil, nil // empty
},
Leaf("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
@ -389,24 +389,24 @@ func TestBlueprintEmptyInstances(t *testing.T) {
}
}
// TestBlueprintMixedStaticAndDynamic tests Blueprint alongside static children
func TestBlueprintMixedStaticAndDynamic(t *testing.T) {
spec := Dir("/",
Leaf("static-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
// TestDynamicMixedStaticAndDynamic tests Blueprint alongside static children
func TestDynamicMixedStaticAndDynamic(t *testing.T) {
spec := DirNode("/",
FileNode("static-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("static\n"), nil
})),
Dir("static-dir",
Leaf("nested", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
DirNode("static-dir",
FileNode("nested", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("nested-static\n"), nil
})),
),
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return []Instance{
{Name: "dynamic1"},
{Name: "dynamic2"},
}, nil
},
Leaf("dynamic-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("dynamic-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("dynamic\n"), nil
})),
),
@ -453,16 +453,16 @@ func TestBlueprintMixedStaticAndDynamic(t *testing.T) {
}
}
// TestBlueprintStatMode tests that blueprint instances get correct file mode
func TestBlueprintStatMode(t *testing.T) {
spec := Dir("/",
Blueprint("{id}", func(ctx HandlerCtx) ([]Instance, error) {
// TestDynamicStatMode tests that blueprint instances get correct file mode
func TestDynamicStatMode(t *testing.T) {
spec := DirNode("/",
Dynamic("{id}", func(ctx HandlerCtx) ([]Instance, error) {
return []Instance{{Name: "instance"}}, nil
},
Leaf("readable", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("readable", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("r\n"), nil
})),
Leaf("writable", 0666,
FileNode("writable", 0666,
Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("rw\n"), nil
}),
@ -470,7 +470,7 @@ func TestBlueprintStatMode(t *testing.T) {
return nil
}),
),
Leaf("executable", 0555, Read(func(ctx HandlerCtx) ([]byte, error) {
FileNode("executable", 0555, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("#!/bin/sh\n"), nil
})),
),

View File

@ -166,8 +166,8 @@ type NodeOption func(*FsNodeDecl)
// ── EDSL constructors ────────────────────────────────────────────
// Dir declares a static directory node, optionally with NodeOptions (GID, UID).
func Dir(name string, args ...any) FsNodeDecl {
// DirNode declares a static directory node, optionally with NodeOptions (GID, UID).
func DirNode(name string, args ...any) FsNodeDecl {
d := FsNodeDecl{Name: name}
for _, a := range args {
switch v := a.(type) {
@ -182,22 +182,22 @@ func Dir(name string, args ...any) FsNodeDecl {
// TemplateDir declares a dynamic directory node whose children are
// resolved at runtime by list. The list function is a factory that
// returns fully-built subtrees — no blueprint children needed here.
// returns fully-built subtrees — no Dynamic children needed here.
func TemplateDir(name string, list ListFn) FsNodeDecl {
return FsNodeDecl{Name: name, List: list}
}
// Blueprint declares a directory template with static structure.
// Dynamic declares a state-dependent directory with static structure.
// The instances function returns identity info for each instance;
// handlers access instance data via ctx.Session/ctx.Agent/ctx.AgentLog.
//
// Example:
//
// Blueprint("{id}", sessionInstances,
// Leaf("name", 0666, Read(readSessionName), Write(writeSessionName)),
// Leaf("state", 0444, Read(readSessionState)),
// Dynamic("{id}", sessionInstances,
// FileNode("name", 0666, Read(readSessionName), Write(writeSessionName)),
// FileNode("state", 0444, Read(readSessionState)),
// )
func Blueprint(name string, instances InstanceFn, args ...any) FsNodeDecl {
func Dynamic(name string, instances InstanceFn, args ...any) FsNodeDecl {
d := FsNodeDecl{Name: name, Instances: instances}
for _, a := range args {
switch v := a.(type) {
@ -210,8 +210,8 @@ func Blueprint(name string, instances InstanceFn, args ...any) FsNodeDecl {
return d
}
// Leaf declares a file node (non-directory).
func Leaf(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
// FileNode declares a file node (non-directory).
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
d := FsNodeDecl{Name: name, Mode: mode}
for _, o := range opts {
o(&d)

View File

@ -27,30 +27,30 @@ import (
var treeSpecUID, treeSpecGID string
var treeSpec = Dir("/",
var treeSpec = DirNode("/",
// Root ownership: UID/GID set at NewRoot() time.
// Every child inherits from root unless overridden.
// ── Root-level files (owned by system user, group "agent") ──
Leaf("backends", 0444, Read(readBackends), GID("agent")),
Leaf("help", 0444, Read(readHelp), GID("agent")),
Leaf("models", 0444, Read(readModels), GID("agent")),
Leaf("agents", 0444, Read(readAgents), GID("agent")),
Leaf("tools", 0444, Read(readTools), GID("agent")),
Leaf("ctl", 0666, Write(writeRootCtl), GID("agent")),
Leaf("eventwait", 0444,
FileNode("backends", 0444, Read(readBackends), GID("agent")),
FileNode("help", 0444, Read(readHelp), GID("agent")),
FileNode("models", 0444, Read(readModels), GID("agent")),
FileNode("agents", 0444, Read(readAgents), GID("agent")),
FileNode("tools", 0444, Read(readTools), GID("agent")),
FileNode("ctl", 0666, Write(writeRootCtl), GID("agent")),
FileNode("eventwait", 0444,
Read(readEventwait),
Stream(blockEventwait),
GID("agent"),
),
Leaf("complete", 0666, Request(requestComplete), GID("agent")),
Leaf("generate", 0666, Request(requestGenerate), GID("agent")),
Leaf("route", 0666, Request(requestRoute), GID("agent")),
Leaf("aliases", 0444, Read(readAliases), GID("agent")),
FileNode("complete", 0666, Request(requestComplete), GID("agent")),
FileNode("generate", 0666, Request(requestGenerate), GID("agent")),
FileNode("route", 0666, Request(requestRoute), GID("agent")),
FileNode("aliases", 0444, Read(readAliases), GID("agent")),
// ── elevate/ (owned by system user, group "agent") ──────────
Dir("elevate", GID("agent"),
Leaf("policy", 0666,
DirNode("elevate", GID("agent"),
FileNode("policy", 0666,
Read(readElevatePolicy),
Write(writeElevatePolicy),
),
@ -59,66 +59,66 @@ var treeSpec = Dir("/",
),
// ── session/ ────────────────────────────────────────────────
Dir("session", GID("agent"),
DirNode("session", GID("agent"),
// session/new and session/idx are owned by system user
Leaf("new", 0666, Request(requestSessionNew)),
Leaf("idx", 0444, Read(readSessionIdx)),
FileNode("new", 0666, Request(requestSessionNew)),
FileNode("idx", 0444, Read(readSessionIdx)),
// {id} — dynamic per-session directory.
// Handlers access session via ctx.Session.
Blueprint("{id}", sessionInstances,
Dynamic("{id}", sessionInstances,
// env — session environment
Leaf("env", 0444, Read(readSessionEnv)),
FileNode("env", 0444, Read(readSessionEnv)),
// paused — session pause status
Leaf("paused", 0444, Read(readSessionPaused)),
FileNode("paused", 0444, Read(readSessionPaused)),
// connected — RPC connection status
Leaf("connected", 0444, Read(readSessionConnected)),
FileNode("connected", 0444, Read(readSessionConnected)),
// ctl — session control
Leaf("ctl", 0666, Read(nopRead), Write(writeSessionCtl)),
FileNode("ctl", 0666, Read(nopRead), Write(writeSessionCtl)),
// name — mutable session name
Leaf("name", 0666, Read(readSessionName), Write(writeSessionName)),
FileNode("name", 0666, Read(readSessionName), Write(writeSessionName)),
// id — immutable session ID
Leaf("id", 0444, Read(readSessionID)),
FileNode("id", 0444, Read(readSessionID)),
// elevate — session elevation policy
Leaf("elevate", 0666, Read(readSessionElevate), Write(writeSessionElevate)),
FileNode("elevate", 0666, Read(readSessionElevate), Write(writeSessionElevate)),
// agent/ — contains per-agent subdirectories
Dir("agent",
Leaf("new", 0666, Request(requestAgentNew)),
DirNode("agent",
FileNode("new", 0666, Request(requestAgentNew)),
// {aid} — dynamic per-agent directory.
// Handlers access agent via ctx.Agent and ctx.AgentLog.
Blueprint("{aid}", agentInstances,
Leaf("prompt", 0666, Write(writeAgentPrompt)),
Leaf("prompt.prev", 0444, Read(readAgentPromptPrev)),
Leaf("fifo.in", 0222, Write(writeAgentFifoIn)),
Leaf("fifo.out", 0444, Read(readAgentFifoOut)),
Leaf("chat", 0444, Stat(statAgentChat), Read(readAgentChat), Stream(streamAgentChat)),
Leaf("connection", 0444, Read(readAgentConnection)),
Leaf("state", 0444, Read(readAgentState)),
Leaf("statewait", 0444, Read(readAgentState), BlockOnce(blockAgentStateWait)),
Leaf("log", 0444, Read(readAgentLog)),
Leaf("offset", 0444, Read(readAgentOffset)),
Leaf("plan", 0666, Read(readAgentPlan), Write(writeAgentPlan)),
Leaf("cfg", 0666, Read(readAgentCfg), Write(writeAgentCfg)),
Leaf("ctl", 0666, Write(writeAgentCtl)),
Leaf("cwd", 0666, Read(readAgentCwd), Write(writeAgentCwd)),
Leaf("usage", 0444, Read(readAgentUsage)),
Leaf("cost", 0444, Read(readAgentCost)),
Leaf("ctxsz", 0444, Read(readAgentCtxsz)),
Leaf("models", 0444, Read(readAgentModels)),
Leaf("systemprompt", 0444, Read(readAgentSystemprompt)),
Leaf("context", 0444, Read(readAgentContext)),
Leaf("tail", 0555, Read(readAgentTail)),
Leaf("tools", 0666, Read(readAgentTools), Write(writeAgentTools)),
Leaf("name", 0666, Read(readAgentName), Write(writeAgentName)),
Leaf("id", 0444, Read(readAgentID)),
Dynamic("{aid}", agentInstances,
FileNode("prompt", 0666, Write(writeAgentPrompt)),
FileNode("prompt.prev", 0444, Read(readAgentPromptPrev)),
FileNode("fifo.in", 0222, Write(writeAgentFifoIn)),
FileNode("fifo.out", 0444, Read(readAgentFifoOut)),
FileNode("chat", 0444, Stat(statAgentChat), Read(readAgentChat), Stream(streamAgentChat)),
FileNode("connection", 0444, Read(readAgentConnection)),
FileNode("state", 0444, Read(readAgentState)),
FileNode("statewait", 0444, Read(readAgentState), BlockOnce(blockAgentStateWait)),
FileNode("log", 0444, Read(readAgentLog)),
FileNode("offset", 0444, Read(readAgentOffset)),
FileNode("plan", 0666, Read(readAgentPlan), Write(writeAgentPlan)),
FileNode("cfg", 0666, Read(readAgentCfg), Write(writeAgentCfg)),
FileNode("ctl", 0666, Write(writeAgentCtl)),
FileNode("cwd", 0666, Read(readAgentCwd), Write(writeAgentCwd)),
FileNode("usage", 0444, Read(readAgentUsage)),
FileNode("cost", 0444, Read(readAgentCost)),
FileNode("ctxsz", 0444, Read(readAgentCtxsz)),
FileNode("models", 0444, Read(readAgentModels)),
FileNode("systemprompt", 0444, Read(readAgentSystemprompt)),
FileNode("context", 0444, Read(readAgentContext)),
FileNode("tail", 0555, Read(readAgentTail)),
FileNode("tools", 0666, Read(readAgentTools), Write(writeAgentTools)),
FileNode("name", 0666, Read(readAgentName), Write(writeAgentName)),
FileNode("id", 0444, Read(readAgentID)),
// proc/ — detached processes (still uses TemplateDir for simplicity)
TemplateDir("proc", listProcesses),
@ -310,7 +310,7 @@ func listElevatePending(ctx HandlerCtx) ([]FsNodeDecl, error) {
out := make([]FsNodeDecl, len(pending))
for i, r := range pending {
id, req := r.ID, r
out[i] = Leaf(id, 0666,
out[i] = FileNode(id, 0666,
Read(func(_ HandlerCtx) ([]byte, error) {
return []byte(req.Summary() + "\n"), nil
}),
@ -867,7 +867,7 @@ func listProcesses(ctx HandlerCtx) ([]FsNodeDecl, error) {
for i, p := range procs {
pid := p.PID
pidStr := strconv.Itoa(pid)
out[i] = Leaf(pidStr, 0444,
out[i] = FileNode(pidStr, 0444,
Read(func(ctx HandlerCtx) ([]byte, error) {
output, err := ctx.Agent.GetDetachedOutput(pid)
if err != nil {