5.8 KiB
EDSL for 9P Filesystem Specification
The entire 9P namespace is declared as a single tree using an embedded
domain-specific language (EDSL) in Go. The declaration lives in
fs/spec.go and is the single source of truth for every file
and directory in the system.
Core Type
type FsNodeDecl struct {
Name string
Aliases []string // alternate walk names (invisible in listings)
UID string // "" = inherit from parent
GID string // "" = inherit from parent
Mode os.FileMode // 0 = inherit parent default
Stat func() os.FileInfo // metadata override
Children []FsNodeDecl // static children (implies directory)
List func(HandlerCtx) ([]FsNodeDecl, error) // dynamic children
Read func(HandlerCtx) ([]byte, error)
Write func(HandlerCtx, []byte) error
BlockOnce func(HandlerCtx, context.Context, string) ([]byte, string, error)
Stream func(HandlerCtx, context.Context, string) ([]byte, string, error)
Rdwr func(HandlerCtx, []byte) ([]byte, error)
Create func(HandlerCtx, string) error
Remove func(HandlerCtx) error
}
Nodes are either directories (have Children or List) or leaf files
(have read/write/blocking handlers). At most one blocking variant
(BlockOnce, Stream, Rdwr) may be set per node.
Constructors
Dir(name, children_or_options...)
Declares a static directory. Accepts a mix of child FsNodeDecl values
and NodeOption functions:
Dir("session",
Leaf("new", 0666, Rdwr(requestSessionNew)),
Leaf("idx", 0444, Read(readSessionIdx)),
GID("agent"),
)
TemplateDir(name, listFn)
Declares a dynamic directory whose children are resolved at runtime by
listFn. The list function receives a HandlerCtx and returns fully
built []FsNodeDecl:
TemplateDir("{id}", listSessions)
Leaf(name, mode, options...)
Declares a file node. Options specify handlers and ownership:
Leaf("env", 0444, Read(readSessionEnv))
Leaf("ctl", 0666, Read(nopRead), Write(writeSessionCtl))
Leaf("chat", 0444, Read(readAgentChat), Stream(blockAgentChat))
Node Options
| Option | Signature | Sets |
|---|---|---|
Read(fn) |
func(HandlerCtx) ([]byte, error) |
d.Read |
Write(fn) |
func(HandlerCtx, []byte) error |
d.Write |
Stream(fn) |
func(HandlerCtx, ctx, base) (data, base, err) |
d.Stream |
BlockOnce(fn) |
func(HandlerCtx, ctx, base) (data, base, err) |
d.BlockOnce |
Rdwr(fn) |
func(HandlerCtx, []byte) ([]byte, error) |
d.Rdwr |
CreateFile(fn) |
func(HandlerCtx, string) error |
d.Create |
Remove(fn) |
func(HandlerCtx) error |
d.Remove |
Stat(fn) |
func() os.FileInfo |
d.Stat |
UID(s) |
string | d.UID |
GID(s) |
string | d.GID |
Alias(names...) |
...string |
d.Aliases |
Handler Context
All handler functions receive a HandlerCtx that carries runtime
dependencies. The context is populated per scope by the wire step:
type HandlerCtx struct {
// Root-level (always available)
Root *Tree
Log *olog.Logger
ToolReg *toolsrv.Registry
Skills *skills.Registry
Models *ModelCache
Elevate *bypass.Broker
Invalidate func()
Shutdown func()
// Session scope
Session *Session
Kill func()
Rename func(string) error
SaveFn func([]byte) error
// Agent scope
Agent *agent.Agent
AgentLog *AgentLog
}
Template Directories (Factories)
Template directories use the List function as a factory. The factory
receives the current HandlerCtx, builds fully-wired subtrees using the
EDSL constructors, and returns them:
func listSessions(ctx HandlerCtx) ([]FsNodeDecl, error) {
rs := ctx.Root.Data.(*rootState)
for _, sess := range rs.sessions {
// capture session deps
sessionDir := Dir(sess.Name(),
UID(sess.Uname()), GID("agent"),
Leaf("env", 0444, Read(func(_ HandlerCtx) ([]byte, error) {
return []byte(sessionEnvContent(sess)), nil
})),
// ... more files
)
out = append(out, sessionDir)
}
return out, nil
}
Rules
- Directories cannot have leaf handlers. A node with
ChildrenorListmust not setRead,Write,BlockOnce,Stream, orRdwr. - At most one blocking handler.
BlockOnce,Stream, andRdwrare mutually exclusive. - Template nodes must have a
Listfunction. Names starting with{and ending with}require a dynamic child resolver. CreateandRemoveare directory-only.- Ownership inherits. Empty
UID/GIDvalues inherit from the parent node, all the way up to root (set from the system user atNewRoottime). - Mode defaults.
0inherits from parent; if still 0 at the root, directories default to0755, files to0444.
Aliases
A node may declare alternate names via Aliases []string. During path
walks (findChild), if no child matches by Name, aliases are checked.
This allows nodes to be addressed by immutable ID even though they are
listed by mutable display name.
sessionDir := Dir(s.Name(),
UID(s.Uname()), GID("agent"),
Alias(s.RunnableID()), // walk accepts UUID
// ...
)
Aliases are invisible — they do not appear in directory listings (ls).
They only affect path resolution during Twalk. This means:
ls session/→ shows display names (myproject,scratch)read session/a1b2c3d4-uuid/id→ resolves via alias, worksread session/myproject/id→ resolves via name, works
Session directories alias their immutable RunnableID(). Agent directories
alias their immutable ID(). Clients that need stable paths (long-lived
connections, bookmarks) should use the ID form.