From 513a3d73d9c6b94015573d6bc77b648606c98423 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 04:38:17 +0000 Subject: [PATCH] Record failed native preparation phase --- .../internal/agent/codex/preparation.go | 26 ++++- .../agent/codex/preparation_close_test.go | 9 ++ .../agent/codex/preparation_log_test.go | 95 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 apps/daemon/internal/agent/codex/preparation_log_test.go diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 49d7b4265..379be82ee 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -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") } @@ -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 @@ -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) } diff --git a/apps/daemon/internal/agent/codex/preparation_close_test.go b/apps/daemon/internal/agent/codex/preparation_close_test.go index cb2abae2e..e03d1aea9 100644 --- a/apps/daemon/internal/agent/codex/preparation_close_test.go +++ b/apps/daemon/internal/agent/codex/preparation_close_test.go @@ -1,9 +1,12 @@ package codex import ( + "bytes" "context" + "log/slog" "os" "path/filepath" + "reflect" "testing" "time" ) @@ -11,6 +14,8 @@ import ( 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) @@ -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) { diff --git a/apps/daemon/internal/agent/codex/preparation_log_test.go b/apps/daemon/internal/agent/codex/preparation_log_test.go new file mode 100644 index 000000000..3a8d847c4 --- /dev/null +++ b/apps/daemon/internal/agent/codex/preparation_log_test.go @@ -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") + } + } + }) + } +}