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
26 changes: 25 additions & 1 deletion apps/daemon/internal/agent/codex/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"errors"
"fmt"
"sync"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
obslog "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log"
)

func newExecutor(parent context.Context, req agent.PrepareRequest, cfg sessionConfig) (*Executor, error) {
func newExecutor(parent context.Context, req agent.PrepareRequest, cfg sessionConfig) (_ *Executor, err error) {
if req.WorkspaceReadOnly {
return nil, errors.New("codex: workspace reads use the local Runtime interface")
}
Expand All @@ -27,6 +28,23 @@ func newExecutor(parent context.Context, req agent.PrepareRequest, cfg sessionCo
if err != nil {
return nil, err
}
started := time.Now()
phase := "view_plan"
defer func() {
if err == nil {
return
}
// Native errors may contain credentials. Record only the failed phase
// and owner context; preparation cleanup cancels its child context.
contextOutcome := "active"
switch {
case errors.Is(parent.Err(), context.DeadlineExceeded):
contextOutcome = "deadline_exceeded"
case errors.Is(parent.Err(), context.Canceled):
contextOutcome = "cancelled"
}
cfg.logger.Info("codex preparation failed", "session_id", req.Assignment.SessionID, "phase", phase, "duration_ms", time.Since(started).Milliseconds(), "context_outcome", contextOutcome)
}()
plan, err := prepareViewPlan(parent, req, cfg)
if err != nil {
return nil, err
Expand Down Expand Up @@ -67,31 +85,37 @@ func newExecutor(parent context.Context, req agent.PrepareRequest, cfg sessionCo
ClientInfo: InitializeClientInfo{Name: "oac-daemon", Version: "0.0.0"},
Capabilities: &InitializeCapabilities{ExperimentalAPI: true},
}
phase = "initialize"
if _, err := rpc.Start(cancelCtx, initParams); err != nil {
return e.preparationFailed(fmt.Errorf("codex: rpc start: %w", err))
}
if req.ExecutionControls != nil && req.ExecutionControls.DisableProgrammaticToolCalling {
phase = "programmatic_tools_configuration"
if err := verifyProgrammaticToolsDisabled(cancelCtx, rpc); err != nil {
return e.preparationFailed(err)
}
}
if req.DisableExecutionEnvironment {
phase = "execution_environment_configuration"
if err := verifyNoExecutionEnvironment(cancelCtx, rpc); err != nil {
return e.preparationFailed(err)
}
}
if s.observeSubagentIdentities {
phase = "subagent_observation_configuration"
if err := verifySubagentObservationProfile(cancelCtx, rpc, plan.Cwd); err != nil {
return e.preparationFailed(err)
}
}

if plan.mcpServers != nil {
phase = "mcp_configuration"
if err := verifyMCPConfig(cancelCtx, rpc, plan); err != nil {
return e.preparationFailed(err)
}
}
if len(req.Skills) > 0 {
phase = "skill_registration"
if err := registerSkills(cancelCtx, rpc, plan.Cwd, req.Skills); err != nil {
return e.preparationFailed(err)
}
Expand Down
9 changes: 9 additions & 0 deletions apps/daemon/internal/agent/codex/preparation_close_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package codex

import (
"bytes"
"context"
"log/slog"
"os"
"path/filepath"
"reflect"
"testing"
"time"
)

func TestPreparationCancellationDuringReadiness(t *testing.T) {
req, cfg, root := preparationFixture(t)
t.Setenv("OAC_TEST_PREPARATION_BLOCK", "1")
var logs bytes.Buffer
cfg.logger = slog.New(slog.NewJSONHandler(&logs, nil))
owner, cancel := context.WithCancel(t.Context())
defer cancel()
result := make(chan error, 1)
Expand All @@ -35,6 +40,10 @@ func TestPreparationCancellationDuringReadiness(t *testing.T) {
t.Fatal("failed readiness leaked model catalog")
}
assertPreparationOnly(t, root)
want := []string{"execution_environment_configuration"}
if got := preparationLogStages(t, &logs, "execution_environment_configuration", "cancelled"); !reflect.DeepEqual(got, want) {
t.Fatalf("stages %v, want %v", got, want)
}
}

func TestReadOnlyPreparationRejectedBeforeNativeSetup(t *testing.T) {
Expand Down
95 changes: 95 additions & 0 deletions apps/daemon/internal/agent/codex/preparation_log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package codex

import (
"bytes"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities"
)

func preparationLogStages(t *testing.T, buf *bytes.Buffer, failed, outcome string) []string {
t.Helper()
var stages []string
for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
var entry map[string]any
if err := json.Unmarshal([]byte(line), &entry); err != nil {
t.Fatal(err)
}
if entry["msg"] != "codex preparation failed" {
continue
}
for key := range entry {
switch key {
case "time", "level", "msg", "session_id", "phase", "duration_ms", "context_outcome":
default:
t.Fatalf("unexpected preparation log field %q", key)
}
}
stage, ok := entry["phase"].(string)
if !ok || stage != failed || entry["duration_ms"].(float64) < 0 {
t.Fatalf("incorrect preparation outcome: %v", entry)
}
want := "active"
if stage == failed {
want = outcome
}
if entry["context_outcome"] != want {
t.Fatalf("incorrect context outcome: %v", entry)
}
stages = append(stages, stage)
}
return stages
}

func TestPreparationLogsLocateNativeFailureWithoutPayloads(t *testing.T) {
for _, reject := range []bool{false, true} {
t.Run(map[bool]string{false: "success", true: "mcp-rejected"}[reject], func(t *testing.T) {
req, cfg, root := preparationFixture(t)
req.Assignment.SessionID = "00000000-0000-4000-8000-000000000001"
req.Skills = []agentcapabilities.InstalledSkill{{InstallationRoot: root, RelativeRoot: "PRIVATE_SKILL_PATH"}}
var buf bytes.Buffer
cfg.logger = slog.New(slog.NewJSONHandler(&buf, nil))
if reject {
if err := os.WriteFile(os.Getenv("OAC_TEST_PREPARATION_MCP_CONFIG"), []byte(`{"config":{"mcp_servers":{"PRIVATE_SECRET":{"url":"https://private.invalid/private-token"}}}}`), 0o600); err != nil {
t.Fatal(err)
}
}
e, err := testExecutor(t, "complete", req, cfg)
if (err != nil) != reject || (e == nil) != reject {
t.Fatal("preparation result changed", err)
}
if e != nil {
if err := e.Close(t.Context()); err != nil {
t.Fatal(err)
}
}
assertPreparationOnly(t, root)
if len(preparedCatalogs(t, root)) != 0 {
t.Fatal("preparation cleanup leaked catalog")
}
failed := ""
var want []string
if reject {
failed = "mcp_configuration"
if !strings.Contains(buf.String(), `"session_id":"`+req.Assignment.SessionID+`"`) {
t.Fatal("missing assignment Session correlation")
}
want = []string{"mcp_configuration"}
}
if got := preparationLogStages(t, &buf, failed, "active"); !reflect.DeepEqual(got, want) {
t.Fatalf("stages %v, want %v", got, want)
}
for _, private := range []string{"PRIVATE_SECRET", "PRIVATE_SKILL_PATH", "private-token", filepath.Join(root, "home"), "fixture-key"} {
if strings.Contains(buf.String(), private) {
t.Fatal("private preparation data in log")
}
}
})
}
}
Loading