diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 8866d08..ea8a61d 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -243,14 +243,14 @@ type actionHandle struct { // AgentCoreConfig is the configuration for creating an agentCore. type AgentCoreConfig struct { - Backend backend.Backend - ModelName string // if non-empty, overrides backend's default model - AgentName string - AgentsDir string - SessionsDir string - SessionID string - CWD string // working directory for tool execution and system prompt - Session *Session + Backend backend.Backend + ModelName string // if non-empty, overrides backend's default model + AgentName string + AgentsDir string + SessionsDir string + SessionID string + CWD string // working directory for tool execution and system prompt + Session *Session Env AgentEnv NewDispatcher func() tools.Dispatcher } @@ -267,7 +267,7 @@ type agentCore struct { sessionID string dispatcher tools.Dispatcher newDispatcher func() tools.Dispatcher - cwd string + cwd string agentPrompt string // agent-specific prompt suffix; kept for system prompt rebuilds currentAction atomic.Pointer[actionHandle] fifo PromptFIFO @@ -277,6 +277,19 @@ type agentCore struct { reply string // assistant text from the most recently completed turn } +// pushSessionEnv injects session-scoped vars into the execute server's +// subprocess environment without touching the process-global env. +func (s *agentCore) pushSessionEnv() { + if s.dispatcher == nil || s.sessionID == "" { + return + } + if srv, ok := s.dispatcher.GetServer("execute"); ok { + if es, ok := srv.(tools.EnvSetter); ok { + es.SetEnv("OLLIE_SESSION_ID", s.sessionID) + } + } +} + var _ Core = (*agentCore)(nil) // compile-time interface check // NewAgentCore creates an agentCore from the given configuration. @@ -293,32 +306,12 @@ func NewAgentCore(cfg AgentCoreConfig) Core { } if cfg.SessionID != "" { os.MkdirAll("/tmp/ollie/"+cfg.SessionID, 0700) //nolint:errcheck - } - - // Inject session env into the execute server so OLLIE_SESSION_ID and OLLIE - // are available to tool scripts from the very first turn, not just after rename. - if cfg.Env.dispatcher != nil && cfg.SessionID != "" { - if srv, ok := cfg.Env.dispatcher.GetServer("execute"); ok { - if es, ok := srv.(tools.EnvSetter); ok { - es.SetEnv("OLLIE_SESSION_ID", cfg.SessionID) - ollie := os.Getenv("OLLIE") - if ollie == "" { - home, _ := os.UserHomeDir() - ollie = home + "/mnt/ollie" - } - es.SetEnv("OLLIE", ollie) - tmpPath := os.Getenv("OLLIE_TMP_PATH") - if tmpPath == "" { - home, _ := os.UserHomeDir() - tmpPath = home + "/.local/share/ollie/tmp" - } - os.MkdirAll(tmpPath, 0700) //nolint:errcheck - es.SetEnv("OLLIE_TMP_PATH", tmpPath) - } + if p := os.Getenv("OLLIE_TMP_PATH"); p != "" { + os.MkdirAll(p, 0700) //nolint:errcheck } } - return &agentCore{ + a := &agentCore{ session: cfg.Session, loopcfg: loopcfg, hooks: cfg.Env.Hooks, @@ -332,6 +325,8 @@ func NewAgentCore(cfg AgentCoreConfig) Core { newDispatcher: cfg.NewDispatcher, state: "idle", } + a.pushSessionEnv() + return a } // Close releases resources for this session, including its tmpdir. @@ -443,27 +438,7 @@ func (s *agentCore) SetSessionID(newID string) error { if _, err := os.Stat(oldTemp); err == nil { os.Rename(oldTemp, newTemp) //nolint:errcheck } - // Propagate to the execute server env. - if s.dispatcher != nil { - if srv, ok := s.dispatcher.GetServer("execute"); ok { - if es, ok := srv.(tools.EnvSetter); ok { - es.SetEnv("OLLIE_SESSION_ID", newID) - // Ensure OLLIE is always set, resolving the default if unset. - ollie := os.Getenv("OLLIE") - if ollie == "" { - home, _ := os.UserHomeDir() - ollie = home + "/mnt/ollie" - } - es.SetEnv("OLLIE", ollie) - tmpPath := os.Getenv("OLLIE_TMP_PATH") - if tmpPath == "" { - home, _ := os.UserHomeDir() - tmpPath = home + "/.local/share/ollie/tmp" - } - es.SetEnv("OLLIE_TMP_PATH", tmpPath) - } - } - } + s.pushSessionEnv() return nil } @@ -802,6 +777,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve } cmd := exec.Command("sh", "-c", cmdStr) cmd.Dir = s.CWD() + cmd.Env = append(os.Environ(), "OLLIE_SESSION_ID="+s.sessionID) o, err := cmd.CombinedOutput() if err != nil { handler(infoEvent("error: " + err.Error())) @@ -939,6 +915,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve s.agentName = name s.session = nil s.sessionID = NewSessionID() + s.pushSessionEnv() for _, msg := range env.Messages { handler(infoEvent(msg)) } diff --git a/pkg/env/env.go b/pkg/env/env.go new file mode 100644 index 0000000..69e5d11 --- /dev/null +++ b/pkg/env/env.go @@ -0,0 +1,71 @@ +// Package env manages the daemon-global environment for ollie. +// It owns the set of known OLLIE_* and SUPERPOWERD_* variables, provides +// defaults, and formats them for export to frontends via ollie/env. +package env + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// managed is the ordered list of env vars that ollie exposes to frontends. +var managed = []string{ + "OLLIE", + "OLLIE_TOOLS_PATH", + "OLLIE_SKILLS_PATH", + "OLLIE_PLAN_PATH", + "OLLIE_MEMORY_PATH", + "OLLIE_TMP_PATH", + "OLLIE_TRANSCRIPT_PATH", + "SUPERPOWERD_SESSION_TOKEN", + "SUPERPOWERD_SOCKET_DIR", +} + +// EnsureDefaults sets default values for any OLLIE_* vars not already present +// in the process environment. +func EnsureDefaults() { + home, _ := os.UserHomeDir() + defaults := map[string]string{ + "OLLIE": filepath.Join(home, "mnt", "ollie"), + "OLLIE_TOOLS_PATH": filepath.Join(home, ".config", "ollie", "tools"), + "OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"), + "OLLIE_PLAN_PATH": filepath.Join(home, ".config", "ollie", "planning"), + "OLLIE_MEMORY_PATH": filepath.Join(home, ".config", "ollie", "memory"), + "OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"), + "OLLIE_TRANSCRIPT_PATH": filepath.Join(home, ".config", "ollie", "transcript"), + } + for k, v := range defaults { + if os.Getenv(k) == "" { + os.Setenv(k, v) //nolint:errcheck + } + } +} + +// Set sets a variable in the process environment. +func Set(k, v string) { os.Setenv(k, v) } //nolint:errcheck + +// Get returns a variable from the process environment. +func Get(k string) string { return os.Getenv(k) } + +// All returns the full process environment as a map. +func All() map[string]string { + pairs := os.Environ() + m := make(map[string]string, len(pairs)) + for _, pair := range pairs { + k, v, _ := strings.Cut(pair, "=") + m[k] = v + } + return m +} + +// Format returns the full process environment as NAME=VALUE lines, suitable +// for serving as ollie/env. +func Format() []byte { + var sb strings.Builder + for _, pair := range os.Environ() { + fmt.Fprintf(&sb, "%s\n", pair) + } + return []byte(sb.String()) +} diff --git a/pkg/mcp/transport.go b/pkg/mcp/transport.go index 727c3f7..4c8c27a 100644 --- a/pkg/mcp/transport.go +++ b/pkg/mcp/transport.go @@ -21,8 +21,11 @@ func NewSTDIOTransport(command string, args []string, env map[string]string) *ST // Connect launches the subprocess and returns a connected Client. func (t *STDIOTransport) Connect() (*Client, error) { cmd := exec.Command(t.command, t.args...) - for k, v := range t.env { - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, os.ExpandEnv(v))) + if len(t.env) > 0 { + cmd.Env = os.Environ() + for k, v := range t.env { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, os.ExpandEnv(v))) + } } stdin, err := cmd.StdinPipe() diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index 027ec52..f025454 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -86,11 +86,11 @@ Examples: "properties": { "code": {"type": "string", "description": "Inline code to execute."}, "language": {"type": "string", "description": "Language interpreter (default: bash). Ignored when tool or parallel is set."}, - "tool": {"type": "string", "description": "Named tool script from the tools directory."}, + "tool": {"type": "string", "description": "Named tool script from the tools directory. Discover available tools: grep -iA2 'keyword' $OLLIE/t/idx"}, "args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."}, "parallel": { "type": "array", - "description": "Fan-out: steps run concurrently, outputs concatenated in submission order, result fed to next stage.", + "description": "Fan-out: steps run concurrently, outputs concatenated in submission order, result fed to next stage. All parallel steps should produce the same output schema so the concatenated result is coherent.", "items": { "type": "object", "properties": { @@ -162,11 +162,9 @@ func (e *Server) allowed(tool, detail string) bool { return e.Confirm(detail) } -// SetEnv adds a per-session environment variable to all subsequent commands. -// It also exports the variable to the process environment so it is inherited -// by sandboxed subprocesses that read os.Environ(). +// SetEnv adds a session-scoped environment variable injected into all +// subsequent subprocess invocations for this session. func (e *Server) SetEnv(key, value string) { - os.Setenv(key, value) //nolint:errcheck e.envMu.Lock() if e.envExtra == nil { e.envExtra = make(map[string]string)