Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/daemon/internal/agent/codex/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, i
caps.LocalEnvironment = proto.CapabilityFromBool(SupportsLocalEnvironment(version))
caps.MCPHTTPRequired = proto.CapabilityFromBool(SupportsNativeSessionRecovery(version))
runtime.Executor = NewExecutorFactory()
runtime.View = discoverView(version)
if caps.LocalEnvironment.IsSupported() {
runtime.WorkspaceReadPreparation = true
runtime.Preparation = func(ctx context.Context, req proto.PromptRequestPayload) (agent.Prepared, error) {
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/codex/declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestDeclaredCapabilityBaseline(t *testing.T) {

func TestUnavailableRuntimeHasNoExecutionFactories(t *testing.T) {
runtime := discoverWithCheck(t.Context(), agent.DiscoveryOptions{Stdout: io.Discard, Stderr: io.Discard}, Declaration.Info, func(context.Context, string) (string, error) { return "", errors.New("missing") })
if runtime.Info.Available || runtime.Executor != nil || runtime.Preparation != nil || runtime.Session == nil {
if runtime.Info.Available || runtime.Executor != nil || runtime.Preparation != nil || runtime.View != nil || runtime.Session == nil {
t.Fatalf("unavailable runtime: %+v", runtime)
}
}
8 changes: 1 addition & 7 deletions apps/daemon/internal/agent/codex/mcp_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package codex

import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
Expand Down Expand Up @@ -35,9 +33,6 @@ type mcpServerConfig struct {
// once per prompt after resetGeneratedConfig; native history stays in CODEX_HOME.
// The transport and enabled_tools fields mirror native McpServerConfig.
func writeCodexMCPConfig(codexHome string, servers map[string]mcpServerConfig) error {
if err := os.MkdirAll(codexHome, 0o700); err != nil {
return fmt.Errorf("codex: mkdir CODEX_HOME %s: %w", codexHome, err)
}
names := make([]string, 0, len(servers))
for name := range servers {
names = append(names, name)
Expand Down Expand Up @@ -113,8 +108,7 @@ func writeCodexMCPConfig(codexHome string, servers map[string]mcpServerConfig) e
b.WriteByte('\n')
}

path := filepath.Join(codexHome, "config.toml")
return appendConfigTOML(path, b.String())
return appendConfigTOML(codexHome, b.String())
}

func writeMCPHeaderMap(b *strings.Builder, field string, values map[string]string) {
Expand Down
16 changes: 12 additions & 4 deletions apps/daemon/internal/agent/codex/mcp_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"crypto/rand"
"errors"
"os"
"path/filepath"
"slices"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
Expand All @@ -22,6 +21,12 @@ func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConf
if bindings == nil {
return nil, nil, nil
}
return mcpServersFromBindings(bindings)
}

// mcpServersFromBindings renders resolved bindings, with each credential in a
// private environment variable.
func mcpServersFromBindings(bindings []agent.MCPBinding) (map[string]mcpServerConfig, []string, error) {
servers := make(map[string]mcpServerConfig, len(bindings))
var env []string
for _, binding := range bindings {
Expand Down Expand Up @@ -50,13 +55,16 @@ func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConf
}

func configureMCP(plan *SessionPlan, servers map[string]mcpServerConfig) error {
codexHome := nativeHomeFromPlan(*plan)
if !filepath.IsAbs(codexHome) {
codexHome := plan.home.Host
root, err := openNativeHome(codexHome)
if err != nil {
return errors.New("codex: public MCP requires a private native home")
}
// Native OAuth defaults to the global keyring. File mode confines lookup to
// this owned history directory; never delete existing credentials to admit it.
if _, err := os.Lstat(filepath.Join(codexHome, ".credentials.json")); !errors.Is(err, os.ErrNotExist) {
_, err = root.Lstat(".credentials.json")
root.Close()
if !errors.Is(err, os.ErrNotExist) {
return errors.New("codex: public MCP requires a native home without stored MCP credentials")
}
if err := writeCodexMCPConfig(codexHome, servers); err != nil {
Expand Down
44 changes: 33 additions & 11 deletions apps/daemon/internal/agent/codex/model_verbosity.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,42 @@ package codex

import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"os"
"path/filepath"
"path"
"slices"
"strings"
)

// catalogProbe runs `debug models` on the trusted install, outside any view.
type catalogProbe struct {
binary string
dir string
env []string
}

// Validate against the binary's active catalog and use that same snapshot for
// execution. A CLI override alone is silently ignored for unsupported models.
func prepareModelVerbosity(ctx context.Context, binary string, plan *SessionPlan) error {
return verifyModelVerbosity(ctx, catalogProbe{binary: binary, dir: plan.Cwd, env: append(os.Environ(), plan.Env...)}, plan)
}

func verifyModelVerbosity(ctx context.Context, probe catalogProbe, plan *SessionPlan) error {
args := []string{}
for _, kv := range plan.ExtraConfig {
args = append(args, "-c", kv[0]+"="+kv[1])
}
args = append(args, "debug", "models")
ctx, cancel := context.WithTimeout(ctx, rpcDefaultRequestTimeout)
defer cancel()
cmd, err := modelCatalogCommand(ctx, binary, args...)
cmd, err := modelCatalogCommand(ctx, probe.binary, args...)
if err != nil {
return err
}
cmd.Dir = plan.Cwd
cmd.Env = append(os.Environ(), plan.Env...)
cmd.Dir = probe.dir
cmd.Env = probe.env
catalog, err := cmd.Output()
// The launcher can exit before its children, ending the context watcher.
if cmd.Process != nil {
Expand All @@ -45,26 +57,36 @@ 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 := nativeHomeFromPlan(*plan)
if !filepath.IsAbs(codexHome) {
return fmt.Errorf("codex: missing managed home for model catalog")
codexHome := plan.home.Host
root, err := openNativeHome(codexHome)
if err != nil {
return fmt.Errorf("codex: missing managed home for model catalog: %w", err)
}
file, err := os.CreateTemp(codexHome, "model-catalog-*.json")
defer root.Close()
name := "model-catalog-" + rand.Text() + ".json"
file, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
_, writeErr := file.Write(catalog)
closeErr := file.Close()
if writeErr != nil || closeErr != nil {
_ = os.Remove(file.Name())
_ = root.Remove(name)
if writeErr != nil {
return writeErr
}
return closeErr
}
cleanup := plan.Cleanup
plan.Cleanup = func() { _ = os.Remove(file.Name()); cleanup() }
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_catalog_json", strconv(file.Name())})
plan.Cleanup = func() {
if root, err := openNativeHome(codexHome); err == nil {
_ = root.Remove(name)
root.Close()
}
cleanup()
}
// Codex reads the catalog at its own path for the same file.
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_catalog_json", strconv(path.Join(plan.home.View, name))})
return nil
}

Expand Down
62 changes: 45 additions & 17 deletions apps/daemon/internal/agent/codex/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sort"
"strings"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths"
harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/codex"
)
Expand All @@ -21,9 +22,9 @@ type SessionPlan struct {
// the Session's private CODEX_HOME for environment:none.
Cwd string

// Env is the full environment slice (KEY=value) to layer onto
// os.Environ() before spawning. Includes CODEX_HOME, plus any
// caller-provided OPENAI_API_KEY / CODEX_API_KEY / proxy vars.
// Env is the environment slice (KEY=value) the plan adds. A local
// codex layers it onto os.Environ(); in an agent-host view it is the
// complete environment. Includes CODEX_HOME.
Env []string

// ExtraConfig is a list of `-c key=value` overrides applied at the
Expand All @@ -39,6 +40,9 @@ type SessionPlan struct {
// Non-nil for declared service or Environment MCP, including private references.
mcpServers map[string]mcpServerConfig

// home is CODEX_HOME as the daemon writes it and as codex sees it.
home agent.ViewDir

// Model is the slug to request on thread/start. Empty inherits the
// codex.config.toml default.
Model string
Expand Down Expand Up @@ -95,6 +99,15 @@ type SessionPlan struct {
//
// Daemon-managed Codex sessions bypass approvals and the engine sandbox.
func BuildSessionPlan(runID, agentStateKey string, opts map[string]any) (SessionPlan, error) {
return buildSessionPlan(opts, func() (agent.ViewDir, error) {
home, err := allocCodexHome(agentStateKey)
return agent.ViewDir{Host: home, View: home}, err
})
}

// buildSessionPlan derives the plan with CODEX_HOME from allocHome, which runs
// only after the options validate.
func buildSessionPlan(opts map[string]any, allocHome func() (agent.ViewDir, error)) (SessionPlan, error) {
cleanup := func() {}
plan := SessionPlan{
CollaborationMode: CollaborationModeDefault,
Expand Down Expand Up @@ -147,14 +160,16 @@ func BuildSessionPlan(runID, agentStateKey string, opts map[string]any) (Session
return plan, err
}

codexHome, err := allocCodexHome(agentStateKey)
home, err := allocHome()
if err != nil {
return plan, err
}
codexHome := home.Host
if err := resetGeneratedConfig(codexHome); err != nil {
return plan, err
}
env = append(env, "CODEX_HOME="+codexHome)
env = append(env, "CODEX_HOME="+home.View)
plan.home = home

// MCP servers come pre-rendered from server/internal/connector/agentdaemon
// (capabilityAdditions.MCPServers, rendered via render.TargetCodex)
Expand Down Expand Up @@ -233,22 +248,35 @@ 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
}
// openNativeHome opens CODEX_HOME from its parent. Every read and write in the
// home goes through this Root: the Session user owns a view home, and a link
// it leaves there resolves only inside the parent, never outside it.
func openNativeHome(codexHome string) (*os.Root, error) {
if !filepath.IsAbs(codexHome) {
return nil, errors.New("codex: missing private native home")
}
parent, err := os.OpenRoot(filepath.Dir(codexHome))
if err != nil {
return nil, fmt.Errorf("codex: open native home: %w", err)
}
defer parent.Close()
root, err := parent.OpenRoot(filepath.Base(codexHome))
if err != nil {
return nil, fmt.Errorf("codex: open native home: %w", err)
}
return home
return root, nil
}

// resetGeneratedConfig removes config.toml; a link in its place is removed,
// never followed.
func resetGeneratedConfig(codexHome string) error {
path := filepath.Join(codexHome, "config.toml")
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("codex: remove generated config %s: %w", path, err)
root, err := openNativeHome(codexHome)
if err != nil {
return err
}
defer root.Close()
if err := root.Remove("config.toml"); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("codex: remove generated config: %w", err)
}
return nil
}
Expand Down
13 changes: 11 additions & 2 deletions apps/daemon/internal/agent/codex/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg
req.AgentStateKey = effectiveAgentStateKey(req)

req.AgentOptions = executionOptions(req)
plan, skillRoots, err := prepareSessionPlan(parent, req, cfg)
var plan SessionPlan
var skillRoots []string
if cfg.view != nil {
plan, err = prepareViewPlan(parent, req, cfg)
} else {
plan, skillRoots, err = prepareSessionPlan(parent, req, cfg)
}
if err != nil {
return nil, err
}
Expand All @@ -80,14 +86,17 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg
LogTag: "codex-preparation",
Logger: cfg.logger,
}
if cfg.view != nil {
rpcCfg.Binary, rpcCfg.Env, rpcCfg.Launch = cfg.view.binary, plan.Env, cfg.view.Launch
}
for _, kv := range plan.ExtraConfig {
rpcCfg.ExtraArgs = append(rpcCfg.ExtraArgs, "-c", kv[0]+"="+kv[1])
}

rpc := NewJSONRPCClient(rpcCfg)

s := &Session{
nativeHome: nativeHomeFromPlan(plan),
nativeHome: plan.home,
functions: functions,
observeMessages: req.ObserveMessages,

Expand Down
23 changes: 12 additions & 11 deletions apps/daemon/internal/agent/codex/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package codex
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
Expand Down Expand Up @@ -60,9 +59,6 @@ type providerConfig struct {
// The provider block is rewritten on every prompt; manual edits to
// scratch CODEX_HOME files are lost on the next spawn.
func writeCodexProviderConfig(codexHome string, cfg providerConfig) error {
if err := os.MkdirAll(codexHome, 0o700); err != nil {
return fmt.Errorf("codex: mkdir CODEX_HOME %s: %w", codexHome, err)
}
if strings.TrimSpace(cfg.BaseURL) == "" {
return fmt.Errorf("codex: provider base_url is required")
}
Expand Down Expand Up @@ -148,8 +144,7 @@ func writeCodexProviderConfig(codexHome string, cfg providerConfig) error {

b.WriteByte('\n')

path := filepath.Join(codexHome, "config.toml")
return appendConfigTOML(path, b.String())
return appendConfigTOML(codexHome, b.String())
}

// appendConfigTOML appends body to <codexHome>/config.toml, creating it
Expand All @@ -159,15 +154,21 @@ func writeCodexProviderConfig(codexHome string, cfg providerConfig) error {
//
// File is opened O_APPEND so concurrent writers in the same prompt
// (today: at most one of each) don't race. 0o600 perms because the
// file carries the API bearer token in plaintext.
func appendConfigTOML(path string, body string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
// file carries the API bearer token in plaintext. The file opens inside the
// native home Root, so a link at config.toml cannot lead the write outside.
func appendConfigTOML(codexHome string, body string) error {
root, err := openNativeHome(codexHome)
if err != nil {
return err
}
defer root.Close()
f, err := root.OpenFile("config.toml", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("codex: open %s: %w", path, err)
return fmt.Errorf("codex: open config.toml: %w", err)
}
defer f.Close()
if _, err := f.WriteString(body); err != nil {
return fmt.Errorf("codex: append %s: %w", path, err)
return fmt.Errorf("codex: append config.toml: %w", err)
}
return nil
}
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/codex/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func SupportsNativeSessionRecovery(version string) bool {
}

func (s *Session) recoverRoot(plan SessionPlan) (string, error) {
home := nativeHomeFromPlan(plan)
home := plan.home.View
if !filepath.IsAbs(home) || !filepath.IsAbs(plan.Cwd) {
return "", errors.New("codex: recovery requires private native history")
}
Expand Down
Loading
Loading