diff --git a/apps/daemon/internal/agent/codex/mcp_http.go b/apps/daemon/internal/agent/codex/mcp_http.go index badc5796..bdeb2af1 100644 --- a/apps/daemon/internal/agent/codex/mcp_http.go +++ b/apps/daemon/internal/agent/codex/mcp_http.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "slices" - "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -51,13 +50,8 @@ func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConf } func configureMCP(plan *SessionPlan, servers map[string]mcpServerConfig) error { - var codexHome string - for _, entry := range plan.Env { - if value, ok := strings.CutPrefix(entry, "CODEX_HOME="); ok { - codexHome = value - } - } - if codexHome == "" || !filepath.IsAbs(codexHome) { + codexHome := nativeHomeFromPlan(*plan) + if !filepath.IsAbs(codexHome) { return errors.New("codex: public MCP requires a private native home") } // Native OAuth defaults to the global keyring. File mode confines lookup to @@ -68,9 +62,6 @@ func configureMCP(plan *SessionPlan, servers map[string]mcpServerConfig) error { if err := writeCodexMCPConfig(codexHome, servers); err != nil { return errors.New("codex: cannot write public MCP configuration") } - if plan.Cwd == "" { - plan.Cwd = codexHome - } for _, feature := range []string{"plugins", "apps"} { plan.EnableFeatures = slices.DeleteFunc(plan.EnableFeatures, func(value string) bool { return value == feature }) if !slices.Contains(plan.DisableFeatures, feature) { diff --git a/apps/daemon/internal/agent/codex/mcp_http_test.go b/apps/daemon/internal/agent/codex/mcp_http_test.go index f8ad4fea..7720bee3 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_test.go @@ -42,7 +42,7 @@ func TestPublicMCPHTTPPlanOwnsConfigurationAndPreservesHistory(t *testing.T) { t.Fatal(err) } if plan.Cwd != home { - t.Fatal("empty cwd did not resolve to the private home") + t.Fatal("environment:none cwd is not the private home") } config, err := os.ReadFile(filepath.Join(home, "config.toml")) if err != nil { diff --git a/apps/daemon/internal/agent/codex/model_verbosity.go b/apps/daemon/internal/agent/codex/model_verbosity.go index db17e64c..6c9d4f28 100644 --- a/apps/daemon/internal/agent/codex/model_verbosity.go +++ b/apps/daemon/internal/agent/codex/model_verbosity.go @@ -45,12 +45,7 @@ func prepareModelVerbosity(ctx context.Context, binary string, plan *SessionPlan // Protocol medium means the default text amount, which needs no native override. plan.ExtraConfig = slices.DeleteFunc(plan.ExtraConfig, func(kv [2]string) bool { return kv[0] == "model_verbosity" }) } - codexHome := "" - for _, entry := range plan.Env { - if value, ok := strings.CutPrefix(entry, "CODEX_HOME="); ok { - codexHome = value - } - } + codexHome := nativeHomeFromPlan(*plan) if !filepath.IsAbs(codexHome) { return fmt.Errorf("codex: missing managed home for model catalog") } diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 46b280b5..54defb0c 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -17,8 +17,8 @@ import ( // the daemon's PromptRequestPayload. type SessionPlan struct { // Cwd is the working directory passed to codex and the spawned - // app-server: the bound workspace root for an Environment request. For - // environment:none it is empty, or CODEX_HOME when MCP is configured. + // app-server: the bound workspace root for an Environment request and + // the Session's private CODEX_HOME for environment:none. Cwd string // Env is the full environment slice (KEY=value) to layer onto @@ -233,6 +233,18 @@ func allocCodexHome(agentStateKey string) (string, error) { return dir, nil } +// nativeHomeFromPlan returns the CODEX_HOME codex receives: os/exec keeps the +// last duplicate entry, and BuildSessionPlan appends the allocated home last. +func nativeHomeFromPlan(plan SessionPlan) string { + var home string + for _, entry := range plan.Env { + if value, ok := strings.CutPrefix(entry, "CODEX_HOME="); ok { + home = value + } + } + return home +} + func resetGeneratedConfig(codexHome string) error { path := filepath.Join(codexHome, "config.toml") if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { diff --git a/apps/daemon/internal/agent/codex/recovery.go b/apps/daemon/internal/agent/codex/recovery.go index 49ef3d30..1f787cf2 100644 --- a/apps/daemon/internal/agent/codex/recovery.go +++ b/apps/daemon/internal/agent/codex/recovery.go @@ -13,12 +13,7 @@ func SupportsNativeSessionRecovery(version string) bool { } func (s *Session) recoverRoot(plan SessionPlan) (string, error) { - var home string - for _, value := range plan.Env { - if strings.HasPrefix(value, "CODEX_HOME=") { - home = strings.TrimPrefix(value, "CODEX_HOME=") - } - } + home := nativeHomeFromPlan(plan) if !filepath.IsAbs(home) || !filepath.IsAbs(plan.Cwd) { return "", errors.New("codex: recovery requires private native history") } diff --git a/apps/daemon/internal/agent/codex/recovery_test.go b/apps/daemon/internal/agent/codex/recovery_test.go index e880e8df..da32ad87 100644 --- a/apps/daemon/internal/agent/codex/recovery_test.go +++ b/apps/daemon/internal/agent/codex/recovery_test.go @@ -142,43 +142,57 @@ func TestRequiredHistoryResolution(t *testing.T) { } func TestPreparedRecoveryCannotStartWithoutExistingHistory(t *testing.T) { - req, cfg, root := preparationFixture(t) - req.RequireExistingNativeSession = true - // Recovery searches history for the bound workspace root. - workspace := filepath.Join(root, "workspace") - if err := os.Mkdir(workspace, 0o700); err != nil { - t.Fatal(err) - } - req.DisableExecutionEnvironment = false - req.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}, WorkspaceRoot: workspace} - p, err := newPreparation(t.Context(), req, cfg) - if err != nil { - t.Fatal(err) - } - defer p.Close() - assertPreparationOnly(t, root) - out := make(chan proto.Envelope, 16) - session, err := p.Start(t.Context(), "recovery-run", proto.TextInput("continue"), out) - if err != nil { - t.Fatal(err) - } - defer session.Cancel(context.Background()) - select { - case <-p.session.waitDone: - case <-time.After(4 * time.Second): - t.Fatal("recovery did not terminate") - } - found := false - for _, frame := range preparationFrames(t, root) { - if frame.Method == "thread/list" { - found = true - } - if frame.Method == "thread/start" || frame.Method == "turn/start" { - t.Fatal("missing history started work", frame.Method) - } - } - if !found { - t.Fatal("prepared start lost recovery requirement") + // Recovery searches history for the Session's working directory: the private + // home for environment:none and the bound workspace root otherwise. + for _, environment := range []string{"none", "local"} { + t.Run(environment, func(t *testing.T) { + req, cfg, root := preparationFixture(t) + req.RequireExistingNativeSession = true + cwd, err := allocCodexHome(req.AgentStateKey) + if err != nil { + t.Fatal(err) + } + if environment == "local" { + cwd = filepath.Join(root, "workspace") + if err := os.Mkdir(cwd, 0o700); err != nil { + t.Fatal(err) + } + req.DisableExecutionEnvironment = false + req.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}, WorkspaceRoot: cwd} + } + p, err := newPreparation(t.Context(), req, cfg) + if err != nil { + t.Fatal(err) + } + defer p.Close() + if p.plan.Cwd != cwd { + t.Fatalf("cwd = %q, want %q", p.plan.Cwd, cwd) + } + assertPreparationOnly(t, root) + out := make(chan proto.Envelope, 16) + session, err := p.Start(t.Context(), "recovery-run", proto.TextInput("continue"), out) + if err != nil { + t.Fatal(err) + } + defer session.Cancel(context.Background()) + select { + case <-p.session.waitDone: + case <-time.After(4 * time.Second): + t.Fatal("recovery did not terminate") + } + found := false + for _, frame := range preparationFrames(t, root) { + if frame.Method == "thread/list" { + found = true + } + if frame.Method == "thread/start" || frame.Method == "turn/start" { + t.Fatal("missing history started work", frame.Method) + } + } + if !found { + t.Fatal("prepared start lost recovery requirement") + } + }) } } diff --git a/apps/daemon/internal/agent/codex/session_plan.go b/apps/daemon/internal/agent/codex/session_plan.go index ded2f314..0fde0a6e 100644 --- a/apps/daemon/internal/agent/codex/session_plan.go +++ b/apps/daemon/internal/agent/codex/session_plan.go @@ -35,6 +35,9 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg plan.Sandbox = "danger-full-access" plan.Permissions = "" plan.ApprovalPolicy = AskForApproval{String: "never"} + } else if req.DisableExecutionEnvironment { + // environment:none has no workspace; the Session's private home is its cwd. + plan.Cwd = nativeHomeFromPlan(plan) } if req.LocalEnvironment != nil { diff --git a/apps/daemon/internal/agent/codex/subagent_profile.go b/apps/daemon/internal/agent/codex/subagent_profile.go index 8342e553..6bf395c6 100644 --- a/apps/daemon/internal/agent/codex/subagent_profile.go +++ b/apps/daemon/internal/agent/codex/subagent_profile.go @@ -6,7 +6,6 @@ import ( "errors" "slices" stdstrconv "strconv" - "strings" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -72,12 +71,3 @@ func verifySubagentObservationProfile(ctx context.Context, rpc *JSONRPCClient, c } return nil } - -func nativeHomeFromPlan(plan SessionPlan) string { - for _, value := range plan.Env { - if strings.HasPrefix(value, "CODEX_HOME=") { - return strings.TrimPrefix(value, "CODEX_HOME=") - } - } - return "" -}