p9: add x/ plugin namespace; wire elevation via x/elevate

Add PluginStore backed by scripts/x/, exposed read-only at x/ in the
9P filesystem. Remove elevator field and elevation.Detect(); update
execute.Decl() call sites to drop the now-removed elevator parameter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-18 20:50:27 +02:00
parent 8b233c295e
commit 680e2a49bd
4 changed files with 66 additions and 5 deletions

View File

@ -70,6 +70,8 @@ ollie/
<script> r/w: tool script content
u/ dir: utility scripts (read, backed by ~/.config/ollie/scripts/u/)
<script> exec: utility script; compositions of b/ primitives
x/ dir: plugins (read, backed by ~/.config/ollie/scripts/x/)
<plugin> exec: server-invoked plugin (e.g. elevation backends)
```
Session IDs are Unix nanosecond timestamps with a random suffix (e.g. `1744276689123456789-2b986c`), so `ls s/` sorted lexicographically gives creation order.
@ -88,6 +90,7 @@ Each 9P directory endpoint is backed by a named store implementing one of the st
| `/sk` | `SkillStore` | `Store` |
| `/t` | `ToolStore` | `Store` |
| `/u` | `UtilStore` | `Store` |
| `/x` | `PluginStore` | `Store` |
| `/s` | `SessionStore` | `Store` |
To swap a backing store (e.g. replace `/pl` with a vector database or `/m` with an object store), implement the appropriate interface and wire it in `New()`. The store interface requires only `Stat`, `List`, `Get`, `Put`, `Delete`, `Create`, and `Rename` — authentication, connection management, and credential rotation are internal concerns of the implementation.

View File

@ -269,7 +269,7 @@ func (s *BatchStore) executeJob(ctx context.Context, job *batchJob) (string, err
cfg, _ := config.Load(cfgPath)
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
"execute": execute.Decl(cwd, nil),
"execute": execute.Decl(cwd),
})
env := agent.BuildAgentEnv(cfg, newDisp(), cwd)

View File

@ -0,0 +1,30 @@
package p9
import (
"os"
"ollie/pkg/tools/execute"
)
// PluginStore is a read-only FlatDirStore backed by the scripts/x/ directory.
// Plugins are executables invoked by the server (e.g. elevation backends), not
// by the agent or user directly.
type PluginStore struct {
*FlatDirStore
}
func NewPluginStore() *PluginStore {
return &PluginStore{FlatDirStore: NewFlatDirStore(execute.PluginsPath(), 0555)}
}
func (s *PluginStore) Stat(name string) (os.FileInfo, error) {
return s.FlatDirStore.Stat(name)
}
func (s *PluginStore) List() ([]os.DirEntry, error) {
return s.FlatDirStore.List()
}
func (s *PluginStore) Get(name string) ([]byte, error) {
return s.FlatDirStore.Get(name)
}

View File

@ -31,6 +31,7 @@
// t/ (dir) tool scripts
// tmp/ (dir) tool dumping ground (r/w, backed by OLLIE_TMP_PATH)
// tr/ (dir) transcripts (ro; write to s/{id}/chat to save)
// x/ (dir) plugins (ro, backed by ~/.config/ollie/scripts/x/)
package p9
import (
@ -47,7 +48,6 @@ import (
"ollie/pkg/agent"
"ollie/pkg/backend"
"ollie/pkg/config"
"ollie/pkg/elevation"
olog "ollie/pkg/log"
"ollie/pkg/paths"
"ollie/pkg/tools"
@ -102,7 +102,6 @@ type connState struct {
// Server is the 9P server for ollie sessions.
type Server struct {
elevator elevation.Elevator
mu sync.RWMutex
sessions map[string]*session
conns []*connState
@ -114,6 +113,7 @@ type Server struct {
memStore Store
toolStore Store
utilStore Store
pluginStore Store
skillStore Store
sessionStore Store
batchStore *BatchStore
@ -131,7 +131,6 @@ func New() *Server {
os.MkdirAll(tmpDir, 0755) //nolint:errcheck
agentsDir := agent.DefaultAgentsDir()
s := &Server{
elevator: elevation.Detect(),
sessions: make(map[string]*session),
agentsDir: agentsDir,
sessionsDir: agent.DefaultSessionsDir(),
@ -141,6 +140,7 @@ func New() *Server {
memStore: NewFlatDirStore(memDir, 0644),
toolStore: NewToolStore(),
utilStore: NewUtilStore(),
pluginStore: NewPluginStore(),
skillStore: NewSkillStore(),
transcriptStore: NewFlatDirStore(transcriptDir, 0444),
tmpStore: NewFlatDirStore(tmpDir, 0600),
@ -332,6 +332,12 @@ func (s *Server) pathType(path string) string {
if _, err := s.utilStore.Stat(parts[1]); err == nil {
return "file"
}
case len(parts) == 1 && parts[0] == "x":
return "dir"
case len(parts) == 2 && parts[0] == "x":
if _, err := s.pluginStore.Stat(parts[1]); err == nil {
return "file"
}
case len(parts) == 1 && parts[0] == "tr":
return "dir"
case len(parts) == 2 && parts[0] == "tr":
@ -701,6 +707,16 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return s.readSlice(fc, content)
}
// Plugin files are served from the plugin store.
if strings.HasPrefix(path, "/x/") {
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
content, err := s.pluginStore.Get(pathBase(path))
if err != nil {
return errFcall(fc, err.Error())
}
return s.readSlice(fc, content)
}
// Tmp files are served from the tmp store.
if strings.HasPrefix(path, "/tmp/") {
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
@ -1165,7 +1181,7 @@ func (s *Server) createSession(args []string) error {
cfg, _ := config.Load(cfgPath) // nil cfg is handled by BuildAgentEnv
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
"execute": execute.Decl(cwd, s.elevator),
"execute": execute.Decl(cwd),
})
sessID := name
@ -1297,6 +1313,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
dirs = append(dirs, makeDir("tmp", "/tmp", true, plan9.DMDIR|0755))
dirs = append(dirs, makeDir("u", "/u", true, plan9.DMDIR|0755))
dirs = append(dirs, makeDir("x", "/x", true, plan9.DMDIR|0555))
dirs = append(dirs, makeDir("tr", "/tr", true, plan9.DMDIR|0555))
} else if path == "/a" {
entries, _ := s.agentStore.List()
@ -1383,6 +1400,11 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
for _, e := range entries {
dirs = append(dirs, makeDir(e.Name(), "/u/"+e.Name(), false, 0555))
}
} else if path == "/x" {
entries, _ := s.pluginStore.List()
for _, e := range entries {
dirs = append(dirs, makeDir(e.Name(), "/x/"+e.Name(), false, 0555))
}
} else if path == "/b" {
dirs = append(dirs, makeDir("job", "/b/job", false, 0555))
dirs = append(dirs, makeDir("q", "/b/q", false, 0555))
@ -1514,6 +1536,8 @@ func (s *Server) makeStat(path string) plan9.Dir {
mode = 0777
} else if strings.HasPrefix(path, "/u/") {
mode = 0555
} else if strings.HasPrefix(path, "/x/") {
mode = 0555
} else if path == "/b/job" || path == "/b/q" || path == "/b/sched" || path == "/b/cleanup" {
mode = 0555
} else {
@ -1601,6 +1625,10 @@ func (s *Server) makeStat(path string) plan9.Dir {
if content, err := s.utilStore.Get(base); err == nil {
dir.Length = uint64(len(content))
}
case strings.HasPrefix(path, "/x/"):
if content, err := s.pluginStore.Get(base); err == nil {
dir.Length = uint64(len(content))
}
case path == "/b/job" || path == "/b/q" || path == "/b/sched" || path == "/b/cleanup":
if content, err := os.ReadFile(paths.CfgDir() + "/scripts/b/" + base); err == nil {
dir.Length = uint64(len(content))