env: centralize daemon env in pkg/env; clean up session env propagation
- New pkg/env package owns EnsureDefaults, Set, Get, All, Format - BuildAgentEnv reverted to 3-arg signature (no initialEnv) - agentCore.setEnv removed; pushSessionEnv handles OLLIE_SESSION_ID only - execute.Server.SetEnv no longer calls os.Setenv (session-scoped only) - !cmd handler sets OLLIE_SESSION_ID explicitly in subprocess env - mcp/transport: seed cmd.Env from os.Environ() so MCP servers inherit PATH etc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2695567aa4
commit
44bff71a65
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Reference in New Issue