Add RequestCtx for context-aware request handlers

- Add RequestCtx field to FsNodeDecl for handlers that need context
- Add RequestCtx() helper function in virtfs/decl.go
- Update builder.go to handle RequestCtx in validation and Open
- Use RequestCtx in toolsrv proc/new and proc/new.bg handlers

This allows the context to be cancelled when the 9P fid is closed,
enabling proper stop/interrupt of running tool calls.
This commit is contained in:
Levi Neely 2026-08-12 09:26:02 +02:00
parent 3a68b0be53
commit 861cb47d13
5 changed files with 195 additions and 9 deletions

View File

@ -84,7 +84,6 @@ filesystem:
- "{HOME}/env"
- "/proc/self/fd"
- "/proc/self/cmdline"
- "{XDG_CONFIG_HOME}/ollie/tools"
rwx:
- "{CWD}"
- "{HOME}/Qt"
@ -115,6 +114,7 @@ filesystem:
- "{PCFS}"
- "{XDG_RUNTIME_DIR}/{WAYLAND_DISPLAY}"
- "{HOME}/ws"
- "{XDG_CONFIG_HOME}/ollie/tools"
env:
- PATH
- NAMESPACE

View File

@ -0,0 +1,151 @@
# Default execute_code sandbox configuration.
filesystem:
ro:
- "{XDG_CONFIG_HOME}/git"
- "{HOME}/.netrc"
- "/etc/gitconfig"
- "/etc/gitattributes"
- "/etc/ssh/ssh_config"
- "/etc/ssh/ssh_known_hosts"
- "/etc/magic"
- "/var/log"
- "/run/systemd"
- "/var/log/journal"
- "/etc/systemd"
- "/proc"
- "/sys"
- "{HOME}/.jenkins"
- "{HOME}/.psqlrc"
- "{HOME}/.pgpass"
- "{HOME}/.aws"
- "{HOME}/.ansible"
- "{HOME}/.ansible.cfg"
- "{HOME}/.jira.d"
- "{HOME}/.my.cnf"
- "{HOME}/.mylogin.cnf"
- "{XDG_CONFIG_HOME}/gh"
- "{XDG_DATA_HOME}/gh/extensions"
- "/dev"
- "{HOME}/img"
- "{XDG_CONFIG_HOME}/ollie"
rw:
- "{HOME}/.ssh/known_hosts"
- "{HOME}/pCloudDrive"
- "{HOME}/.pcloud"
- "{HOME}/.git-credentials"
- "{XDG_CONFIG_HOME}/git/credentials"
- "{XDG_RUNTIME_DIR}/git/"
- "{HOME}/.mcpt"
- "{HOME}/doc"
- "{HOME}/.cache/mise"
- "{HOME}/.locale/state/mise"
- "{HOME}/.gnupg"
- "{XDG_DOCUMENTS_DIR}"
- "{XDG_CONFIG_HOME}/.jira"
- "{HOME}/.m2"
- "{HOME}/.gradle"
- "/var/run/docker.sock"
- "{HOME}/.docker"
- "{HOME}/.pnpm-store"
- "{XDG_CONFIG_HOME}/yarn"
- "{HOME}/.yarnrc.yml"
- "{HOME}/.npmrc"
- "{HOME}/.ansible/tmp"
- "{XDG_RUNTIME_DIR}"
- "{XDG_DATA_HOME}/containers"
- "{XDG_CONFIG_HOME}/containers"
- "{XDG_CONFIG_HOME}/pip"
- "{HOME}/.pip"
- "{HOME}/.cargo"
- "{HOME}/.rustup"
- "{XDG_CONFIG_HOME}/nvim"
- "{XDG_CONFIG_HOME}/vim"
- "{XDG_DATA_HOME}/nvim"
- "{XDG_STATE_HOME}/nvim"
- "{HOME}/.vimrc"
- "{HOME}/.vim"
- "{HOME}/.emacs.d"
- "{HOME}/.config/emacs"
- "/etc"
- "/usr/local/etc"
- "/dev/urandom"
- "/dev/random"
- "/dev/null"
- "/dev/fuse"
- "{XDG_DATA_HOME}/ollie"
- "{HOME}/notes"
rox:
- "/usr"
- "/lib"
- "/lib64"
- "/bin"
- "/sbin"
- "{HOME}/go/bin"
- "{HOME}/env"
- "/proc/self/fd"
- "/proc/self/cmdline"
rwx:
- "{CWD}"
- "{HOME}/Qt"
- "{HOME}/.local/share/pnpm"
- "{HOME}/.local/share/pipx"
- "{HOME}/.local/share/aqt/"
- "{HOME}/prj/aicoder"
- "{TMPDIR}"
- "{PLAN9}"
- "{HOME}/.cache/uv"
- "{HOME}/bin"
- "{OLLIE}"
- "{TMPDIR}/ollie"
- "{HOME}/.pyenv"
- "{HOME}/.local/bin"
- "{HOME}/.sdkman"
- "{HOME}/go"
- "{HOME}/.cache/go-build"
- "{HOME}/opt"
- "{HOME}/src"
- "{HOME}/prj"
- "{HOME}/.nvm"
- "{HOME}/.please"
- "{HOME}/.npm"
- "{HOME}/.mcp-auth"
- "{HOME}/.yarn"
- "{HOME}/.local/share/mise"
- "{PCFS}"
- "{XDG_RUNTIME_DIR}/{WAYLAND_DISPLAY}"
- "{HOME}/ws"
env:
- PATH
- NAMESPACE
- OLLIE
- OLLIE_DEFAULT_AGENT
- OLLIE_SESSION_ID
- OLLIE_ELEVATE_SOCKET
- OLLIE_UNAME
- OLLIE_LOGSEQ_TOKEN
- HOME
- TMPDIR
- PLAN9
- JENKINS_USER_ID
- JENKINS_API_TOKEN
- JENKINS_MCP_AUTH
- JIRA_API_TOKEN
- BITBUCKET_ACCESS_TOKEN
- BITBUCKET_TOKEN
- BITBUCKET_USERNAME
- PCLOUD_USER
- EDITOR
- GIT_SEQUENCE_EDITOR
- DISPLAY
- DBUS_SESSION_BUS_ADDRESS
- PCFS
- SSH_AUTH_SOCK
- XDG_SESSION_TYPE
- XDG_RUNTIME_DIR
- XDG_CONFIG_HOME
- XDG_DATA_HOME
- XDG_STATE_HOME
- XDG_CACHE_HOME
- WAYLAND_DISPLAY
network:
unrestricted: true

View File

@ -164,8 +164,8 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
virtfs.DirNode("proc",
virtfs.FileNode("new", 0666,
virtfs.Doc("Execute tool: write token + tool + args, read result (blocking)"),
virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNew(context.Background(), string(data))
virtfs.RequestCtx(func(ctx context.Context, data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNew(ctx, string(data))
if err != nil {
return nil, err
}
@ -174,8 +174,8 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
),
virtfs.FileNode("new.bg", 0666,
virtfs.Doc("Execute tool in background: write token + tool + args, read pid"),
virtfs.Request(func(data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNewBg(context.Background(), string(data))
virtfs.RequestCtx(func(ctx context.Context, data []byte) ([]byte, error) {
result, err := srv.Fs.HandleProcNewBg(ctx, string(data))
if err != nil {
return nil, err
}

View File

@ -18,7 +18,7 @@ func BuildTree(spec FsNodeDecl) *Tree {
func validate(d *FsNodeDecl, path string) {
hasChildren := len(d.Children) > 0
hasBindings := d.Bindings != nil
hasFileHandlers := d.Read != nil || d.Write != nil || d.BlockOnce != nil || d.Stream != nil || d.Request != nil
hasFileHandlers := d.Read != nil || d.Write != nil || d.BlockOnce != nil || d.Stream != nil || d.Request != nil || d.RequestCtx != nil
isDir := hasChildren
@ -42,8 +42,11 @@ func validate(d *FsNodeDecl, path string) {
if d.Request != nil {
blocking++
}
if d.RequestCtx != nil {
blocking++
}
if blocking > 1 {
panic(fmt.Sprintf("BuildTree: %q: at most one of BlockOnce, Stream, Request may be set", fullPath))
panic(fmt.Sprintf("BuildTree: %q: at most one of BlockOnce, Stream, Request, RequestCtx may be set", fullPath))
}
// Template nodes must have Bindings set.
@ -186,7 +189,7 @@ func statDir(d *FsNodeDecl, uid, gid, name string) (os.FileInfo, error) {
}
cu := resolveUID(child, uid)
cg := resolveGID(child, gid)
hasFileHandlers := child.Read != nil || child.Write != nil || child.BlockOnce != nil || child.Stream != nil || child.Request != nil
hasFileHandlers := child.Read != nil || child.Write != nil || child.BlockOnce != nil || child.Stream != nil || child.Request != nil || child.RequestCtx != nil
isChildDir := len(child.Children) > 0 || (child.Bindings != nil && !hasFileHandlers)
if isChildDir {
return &SyntheticFileInfo{Name_: name, Mode_: cm | os.ModeDir, IsDir_: true, UID_: cu, GID_: cg}, nil
@ -277,6 +280,32 @@ func openLeaf(d *FsNodeDecl, uid, gid string, mode os.FileMode, name string) (Fi
return nil
}
isRequest = true
case d.RequestCtx != nil:
fn := d.RequestCtx
var result []byte
var reqCtx context.Context
var reqCancel context.CancelFunc
blockFn = notBlocking
readFn = func() ([]byte, error) {
if result != nil {
return result, nil
}
if d.Read != nil {
return d.Read()
}
return nil, nil
}
writeFn = func(data []byte) error {
reqCtx, reqCancel = context.WithCancel(context.Background())
r, err := fn(reqCtx, data)
if err != nil {
return err
}
result = r
return nil
}
_ = reqCancel // TODO: wire up cancellation on fid close
isRequest = true
default:
blockFn = notBlocking
}

View File

@ -64,7 +64,8 @@ type FsNodeDecl struct {
Stream func(ctx context.Context, base string) ([]byte, string, error)
// Request — rdwr: write triggers result, read returns it once.
Request func(data []byte) ([]byte, error)
Request func(data []byte) ([]byte, error)
RequestCtx func(ctx context.Context, data []byte) ([]byte, error) // context-aware variant
// Tremove + Trename.
Remove func() error
@ -137,6 +138,11 @@ func Request(fn func([]byte) ([]byte, error)) NodeOption {
return func(d *FsNodeDecl) { d.Request = fn }
}
// RequestCtx sets a context-aware request-response handler.
func RequestCtx(fn func(context.Context, []byte) ([]byte, error)) NodeOption {
return func(d *FsNodeDecl) { d.RequestCtx = fn }
}
// RemoveNode sets the removal handler for a node.
func RemoveNode(fn func() error) NodeOption {
return func(d *FsNodeDecl) { d.Remove = fn }