agent: remove $OLLIE mount dependency from core

- Add ListHandlers map to AgentCoreConfig/agent struct; /skills and /tools
  dispatch through it instead of reading local paths directly
- Add ReadPlanStep to AgentCoreConfig; wired by 9p/ directly from sess.plan
- Export NextUncheckedStep([]byte) as pure helper
- Fix prependOlliePath to use OLLIE_CFG_PATH/scripts/x/ instead of $OLLIE/x/
- Add OLLIE_CFG_PATH to EnsureDefaults and managed env var list
This commit is contained in:
ollie 2026-06-16 12:30:36 +02:00
parent 72ae801e3f
commit 8e7dd60a8b
4 changed files with 31 additions and 32 deletions

View File

@ -25,20 +25,10 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
cmd := parts[0]
args := parts[1:]
listMountDir := func(subdir string) {
mount := os.Getenv("OLLIE")
if mount == "" {
home, _ := os.UserHomeDir()
mount = home + "/mnt/ollie"
}
entries, err := os.ReadDir(mount + "/" + subdir)
if err != nil {
s.emit(infoEvent(fmt.Sprintf("%s: %v", cmd, err)))
return
}
for _, e := range entries {
if !e.IsDir() {
s.emit(infoEvent(" " + e.Name()))
listFromHandler := func(name string) {
if h := s.listHandlers[name]; h != nil {
for _, item := range h() {
s.emit(infoEvent(" " + item))
}
}
}
@ -423,8 +413,8 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
s.emit(infoEvent("cwd: " + dir))
},
"/skills": func(args []string) { listMountDir("sk") },
"/tools": func(args []string) { listMountDir("t") },
"/skills": func(args []string) { listFromHandler("skills") },
"/tools": func(args []string) { listFromHandler("tools") },
"/sp": func(args []string) {
s.emit(infoEvent(s.runtime.Preamble))

View File

@ -235,6 +235,13 @@ type AgentCoreConfig struct {
// MaxSteps overrides the agent JSON maxSteps when non-zero.
// 0 means use the value from the agent config (or unlimited if absent).
MaxSteps int
// ReadPlanStep, if non-nil, is called to get the next unchecked plan step.
// When provided, it is used directly instead of reading from the filesystem.
ReadPlanStep func() string
// ListHandlers provides harness-specific list implementations for slash
// commands such as /skills, /tools, /agents. Keyed by command name without
// the leading slash. Commands with no entry emit nothing.
ListHandlers map[string]func() []string
}
// agent is the Core implementation. It owns all agent and session state
@ -252,6 +259,7 @@ type agent struct {
newDispatcher func() tools.Dispatcher
newBackend func(string) (backend.Backend, error)
readPlanStep func() string
listHandlers map[string]func() []string
turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
cwd string
startupMessages []string
@ -342,6 +350,11 @@ func readNextUnchecked(path string) string {
if err != nil || len(data) == 0 {
return ""
}
return NextUncheckedStep(data)
}
// NextUncheckedStep returns the first unchecked step from plan bytes.
func NextUncheckedStep(data []byte) string {
for _, line := range strings.Split(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- [ ]") {
@ -385,12 +398,9 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
var readPlanStep func() string
if cfg.SessionID != "" {
os.MkdirAll(filepath.Join(ollieTmpDir(), cfg.SessionID), 0700) //nolint:errcheck
if mount := os.Getenv("OLLIE"); mount != "" {
planPath := filepath.Join(mount, "s", cfg.SessionID, "plan")
readPlanStep = func() string {
return readNextUnchecked(planPath)
}
}
}
if cfg.ReadPlanStep != nil {
readPlanStep = cfg.ReadPlanStep
}
log := cfg.Log
@ -413,6 +423,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
newDispatcher: cfg.NewDispatcher,
newBackend: cfg.NewBackend,
readPlanStep: readPlanStep,
listHandlers: cfg.ListHandlers,
state: "idle",
bus: pubsub.NewBus(),
}

2
pkg/env/env.go vendored
View File

@ -16,6 +16,7 @@ import (
// managed is the ordered list of env vars that ollie exposes to frontends.
var managed = []string{
"OLLIE",
"OLLIE_CFG_PATH",
"OLLIE_TOOLS_PATH",
"OLLIE_AGENTS_PATH",
"OLLIE_SKILLS_PATH",
@ -39,6 +40,7 @@ func EnsureDefaults() {
}
defaults := map[string]string{
"OLLIE": filepath.Join(home, "mnt", "ollie"),
"OLLIE_CFG_PATH": filepath.Join(home, ".config", "ollie"),
"OLLIE_TOOLS_PATH": filepath.Join(home, ".config", "ollie", "tools"),
"OLLIE_AGENTS_PATH": filepath.Join(home, ".config", "ollie", "agents"),
"OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"),

View File

@ -643,11 +643,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
filtered = append(filtered, ev)
}
}
olliePath := e.envExtra["OLLIE"]
if olliePath == "" {
olliePath = os.Getenv("OLLIE")
}
cmd.Env = prependOlliePath(filtered, olliePath)
cmd.Env = prependOlliePath(filtered, paths.CfgDir())
cmd.Env = append(cmd.Env, "OLLIE_TOOLS_PATH="+ToolsPath())
for k, v := range e.envExtra {
cmd.Env = append(cmd.Env, k+"="+v)
@ -682,13 +678,13 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
return string(output), nil
}
// prependOlliePath returns env with $OLLIE/x/ prepended to PATH so wrapper
// scripts placed there (e.g. ~/.config/ollie/scripts/x/) shadow system binaries.
func prependOlliePath(env []string, ollie string) []string {
if ollie == "" {
// prependOlliePath returns env with $OLLIE_CFG_PATH/scripts/x/ prepended to PATH so wrapper
// scripts placed there shadow system binaries.
func prependOlliePath(env []string, cfgDir string) []string {
if cfgDir == "" {
return env
}
xdir := filepath.Join(ollie, "x")
xdir := filepath.Join(cfgDir, "scripts", "x")
result := make([]string, 0, len(env))
for _, e := range env {
if strings.HasPrefix(e, "PATH=") {