From 861cb47d138ea11dd980b3bb03ab44b9e34858cf Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 12 Aug 2026 09:26:02 +0200 Subject: [PATCH] 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. --- cmd/toolsrv/internal/sandbox/sandbox.yaml | 2 +- cmd/toolsrv/internal/sandbox/sandbox.yaml~ | 151 +++++++++++++++++++++ cmd/toolsrv/internal/server/server.go | 8 +- virtfs/builder.go | 35 ++++- virtfs/decl.go | 8 +- 5 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 cmd/toolsrv/internal/sandbox/sandbox.yaml~ diff --git a/cmd/toolsrv/internal/sandbox/sandbox.yaml b/cmd/toolsrv/internal/sandbox/sandbox.yaml index e46a25d..7f7c0c3 100644 --- a/cmd/toolsrv/internal/sandbox/sandbox.yaml +++ b/cmd/toolsrv/internal/sandbox/sandbox.yaml @@ -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 diff --git a/cmd/toolsrv/internal/sandbox/sandbox.yaml~ b/cmd/toolsrv/internal/sandbox/sandbox.yaml~ new file mode 100644 index 0000000..a4b20db --- /dev/null +++ b/cmd/toolsrv/internal/sandbox/sandbox.yaml~ @@ -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 diff --git a/cmd/toolsrv/internal/server/server.go b/cmd/toolsrv/internal/server/server.go index f889641..074912f 100644 --- a/cmd/toolsrv/internal/server/server.go +++ b/cmd/toolsrv/internal/server/server.go @@ -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 } diff --git a/virtfs/builder.go b/virtfs/builder.go index 22d43a5..e788618 100644 --- a/virtfs/builder.go +++ b/virtfs/builder.go @@ -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 } diff --git a/virtfs/decl.go b/virtfs/decl.go index d311de2..2737990 100644 --- a/virtfs/decl.go +++ b/virtfs/decl.go @@ -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 }