execute: per-session FUSE mount for 9P identity

The execute server now starts ollie-9p-mount when OLLIE_SESSION_ID is
set, giving each agent session its own FUSE proxy that attaches to
olliesrv with Uname=session_id. Tools see the full namespace through
$OLLIE but all 9P operations carry the session's identity.

Close() tears down the mount on session destroy.
This commit is contained in:
Levi Neely 2026-05-19 14:10:09 +02:00
parent 5efed7040d
commit e20f9a7a06
2 changed files with 57 additions and 0 deletions

View File

@ -445,6 +445,14 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
// Close releases resources for this session, including its tmpdir.
func (s *agent) Close() {
s.log.Debug("Close() session=%q", s.sessionID)
if s.dispatcher != nil {
if srv, ok := s.dispatcher.GetServer("execute"); ok {
if c, ok := srv.(interface{ Close() }); ok {
s.log.Debug("Close() calling execute.Close()")
c.Close()
}
}
}
if s.sessionID != "" {
os.RemoveAll(filepath.Join(ollieTmpDir(), s.sessionID)) //nolint:errcheck
}

View File

@ -41,6 +41,10 @@ type Server struct {
// step dispatch. Set once at session init via SetLockDir; empty disables locking.
lockDir string
// mountProc is the ollie-9p-mount subprocess for this session's FUSE proxy.
mountProc *os.Process
mountPath string
// Strict rejects inline {code} steps; only {tool} steps are allowed.
Strict bool
@ -358,9 +362,54 @@ func (e *Server) SetEnv(key, value string) {
e.envExtra = make(map[string]string)
}
e.envExtra[key] = value
if key == "OLLIE_SESSION_ID" && value != "" {
e.startMount(value)
}
e.envMu.Unlock()
}
// startMount launches ollie-9p-mount for this session. Must be called with envMu held.
func (e *Server) startMount(sessionID string) {
bin, err := exec.LookPath("ollie-9p-mount")
if err != nil {
return // binary not installed; skip silently
}
mnt := filepath.Join(os.TempDir(), "ollie-mount-"+sessionID)
os.MkdirAll(mnt, 0755) //nolint:errcheck
cmd := exec.Command(bin, "-u", sessionID, mnt)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
return
}
// Reap in background so Wait works later.
go cmd.Wait() //nolint:errcheck
// Wait for mount to be ready.
for i := 0; i < 20; i++ {
time.Sleep(50 * time.Millisecond)
if _, err := os.Stat(filepath.Join(mnt, "s")); err == nil {
break
}
}
e.mountProc = cmd.Process
e.mountPath = mnt
e.envExtra["OLLIE"] = mnt
}
// Close tears down the per-session FUSE mount.
func (e *Server) Close() {
e.envMu.Lock()
proc := e.mountProc
mnt := e.mountPath
e.mountProc = nil
e.mountPath = ""
e.envMu.Unlock()
if proc != nil {
exec.Command("fusermount", "-u", mnt).Run() //nolint:errcheck
syscall.Kill(proc.Pid, syscall.SIGKILL) //nolint:errcheck
os.Remove(mnt) //nolint:errcheck
}
}
// executeElevated runs cmd outside the sandbox via x/elevate.
// Returns (output, error); a non-zero exit code is treated as an error.
func (e *Server) executeElevated(ctx context.Context, cmd, dir string, timeout int) (string, error) {