ollie/doc/edsl.md

4.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
    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)
    Request   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, Request) 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, Request(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
Request(fn) func(HandlerCtx, []byte) ([]byte, error) d.Request
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

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   *elevate.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

  1. Directories cannot have leaf handlers. A node with Children or List must not set Read, Write, BlockOnce, Stream, or Request.
  2. At most one blocking handler. BlockOnce, Stream, and Request are mutually exclusive.
  3. Template nodes must have a List function. Names starting with { and ending with } require a dynamic child resolver.
  4. Create and Remove are directory-only.
  5. Ownership inherits. Empty UID/GID values inherit from the parent node, all the way up to root (set from the system user at NewRoot time).
  6. Mode defaults. 0 inherits from parent; if still 0 at the root, directories default to 0755, files to 0444.