diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go index e4549a8b7..2e042f292 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/google/uuid" ) @@ -38,9 +39,9 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) { if err != nil { t.Fatal(err) } + provider := &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://api.minimax.cn/anthropic", APIKey: strings.TrimSpace(string(key))} config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=https://api.minimax.cn/anthropic", "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M3", }} readiness, err := CheckRuntime(context.Background(), config) @@ -65,7 +66,7 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."} running, err := NewFactory(config)(ctx, request, out) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_test.go index 70586f8bd..8672981b1 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_test.go @@ -163,7 +163,7 @@ func cancellationConfig(root, mode string) Config { } func cancellationRequest() proto.PromptRequestPayload { - return proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} } func runCancellationHelper(request startRequest, mode string, scanner *bufio.Scanner, emit func(bridgeEvent)) { diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index 149ef8f65..aee9000a1 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -127,7 +127,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { } if !ready { factory, _ := registry.Resolve("claude_sdk") - if _, err := factory(t.Context(), proto.PromptRequestPayload{}, nil); err == nil || !strings.Contains(err.Error(), "runtime is unavailable") { + if _, err := factory(t.Context(), proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()}, nil); err == nil || !strings.Contains(err.Error(), "runtime is unavailable") { t.Fatal(err) } } diff --git a/apps/daemon/internal/agent/claudesdk/error_classification_test.go b/apps/daemon/internal/agent/claudesdk/error_classification_test.go index 53440e2bb..3fbd8f8a8 100644 --- a/apps/daemon/internal/agent/claudesdk/error_classification_test.go +++ b/apps/daemon/internal/agent/claudesdk/error_classification_test.go @@ -21,7 +21,7 @@ func TestClassifiedBridgeFailurePreservesTerminalEvidence(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=classified-" + mode, "GORACE=atexit_sleep_ms=0"}} - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index b2f861097..4e10c4b7a 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -17,7 +17,7 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), AgentSessionID: "native-session", Model: "native-model", SystemPrompt: "Keep these exact instructions.\nDo not replace them."} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), AgentSessionID: "native-session", Model: "native-model", SystemPrompt: "Keep these exact instructions.\nDo not replace them."} ordinary, _, err := prepare(config, request) if err != nil { t.Fatal(err) @@ -49,7 +49,7 @@ func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, Model: "native-model"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, Model: "native-model"} _, err := NewFactory(config)(t.Context(), request, make(chan proto.Envelope, 1)) if err == nil || !strings.Contains(err.Error(), "execution controls require") { t.Fatal("unsupported controls did not fail at admission", err) @@ -66,7 +66,7 @@ func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Input"), MCPHTTPServers: &servers} + request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Input"), MCPHTTPServers: &servers, Model: "fixture", ModelProvider: fixtureProvider()} _, err := NewFactory(config)(t.Context(), request, make(chan proto.Envelope, 1)) if err == nil || !strings.Contains(err.Error(), "service-origin MCP requires a service execution host") { t.Fatal("MCP reached an unsupported environment", err) @@ -81,7 +81,7 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} schema := json.RawMessage(`{"type":"object","properties":{"n":{"const":9007199254740992}}}`) - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), ObserveMessages: true, DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), ObserveMessages: true, DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}} start, _, err := prepare(config, request) if err != nil { t.Fatal(err) @@ -104,7 +104,7 @@ func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), DisableSubagents: true, ToolSearch: true, Model: "model", FunctionTools: []proto.FunctionTool{ + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), DisableSubagents: true, ToolSearch: true, Model: "model", FunctionTools: []proto.FunctionTool{ {Name: "lookup", Description: "Lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"const":"original"}}}`), DeferLoading: true}, {Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"type":"object"}`)}, }} diff --git a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go index 1b5dab8f4..63ca1e998 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go @@ -17,6 +17,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/google/uuid" ) @@ -41,8 +42,9 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { t.Fatal("cannot read selected credential file") } t.Setenv("OAC_RUNTIME_HOME", proof) + provider := &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: endpoint, APIKey: strings.TrimSpace(string(key))} config := Config{Node: os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_NODE"), Entrypoint: entry, StateDir: filepath.Join(proof, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, }} if proxy := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_HTTP_PROXY"); proxy != "" { @@ -80,7 +82,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { _ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600) } defer persist() - request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: model, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."} + request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."} factory := NewExecutorFactory(config) prepared := time.Now() owner, err := factory(ctx, request) diff --git a/apps/daemon/internal/agent/claudesdk/executor_test.go b/apps/daemon/internal/agent/claudesdk/executor_test.go index ee6de5fef..c1cecc7fb 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_test.go @@ -27,7 +27,7 @@ func persistentConfig(t *testing.T, mode string) (Config, proto.PromptRequestPay if err := os.WriteFile(entry, []byte("version-one"), 0600); err != nil { t.Fatal(err) } - return Config{Node: os.Args[0], Entrypoint: entry, StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_EXECUTOR_HELPER=1", "SDK_EXECUTOR_DIR=" + root, "SDK_EXECUTOR_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}}, proto.PromptRequestPayload{DisableExecutionEnvironment: true, Model: "fixture"} + return Config{Node: os.Args[0], Entrypoint: entry, StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_EXECUTOR_HELPER=1", "SDK_EXECUTOR_DIR=" + root, "SDK_EXECUTOR_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}}, proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, Model: "fixture"} } func runPersistentExecutorHelper() { diff --git a/apps/daemon/internal/agent/claudesdk/functions_test.go b/apps/daemon/internal/agent/claudesdk/functions_test.go index a142b0139..4e44dcc08 100644 --- a/apps/daemon/internal/agent/claudesdk/functions_test.go +++ b/apps/daemon/internal/agent/claudesdk/functions_test.go @@ -22,7 +22,7 @@ func TestFunctionFactoryNativeReceipts(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", ObserveToolObservations: true, Model: "fake-model", SystemPrompt: "instructions", FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", ObserveToolObservations: true, Model: "fake-model", SystemPrompt: "instructions", FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/harness_config_test.go b/apps/daemon/internal/agent/claudesdk/harness_config_test.go index 62442e304..b2f969773 100644 --- a/apps/daemon/internal/agent/claudesdk/harness_config_test.go +++ b/apps/daemon/internal/agent/claudesdk/harness_config_test.go @@ -13,7 +13,7 @@ func TestHarnessConfigReachesBridge(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} - req := proto.PromptRequestPayload{Model: "fixture", HarnessConfig: proto.HarnessConfig(`{"effort":"high","thinking":{"type":"enabled","budgetTokens":1024}}`)} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "fixture", HarnessConfig: proto.HarnessConfig(`{"effort":"high","thinking":{"type":"enabled","budgetTokens":1024}}`)} start, _, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go index 704c20af6..d14865c0e 100644 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/live_linux_test.go @@ -22,6 +22,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/google/uuid" ) @@ -90,9 +91,9 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { proxy.ServeHTTP(w, req) })) defer forwarder.Close() + provider := &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: forwarder.URL, APIKey: strings.TrimSpace(string(key))} config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + forwarder.URL, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M3", }} @@ -137,7 +138,7 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { requestStart := len(requests) mu.Unlock() out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", SystemPrompt: "Answer briefly and preserve the exact verification value in the conversation. Use no tools."} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Answer briefly and preserve the exact verification value in the conversation. Use no tools."} if success != nil { request.ObserveToolObservations = true request.SystemPrompt = "Call lookup exactly once as requested, then report both result parts and any prior verification value. Never retry a failed tool." diff --git a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go index fe5828e48..60b9a8304 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go @@ -22,7 +22,7 @@ func TestMCPBearerUsesFreshOwnedEnvironmentReferences(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "second", ServerURL: "https://second.example/mcp", BearerToken: &tokens[1]}, {ConnectionOrigin: "service", ServerLabel: "anonymous", ServerURL: "http://anonymous.example/mcp"}, } - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} seen := map[string]bool{} for range 2 { start, env, err := prepare(config, req) @@ -66,7 +66,7 @@ func TestMCPBearerRejectsInvalidCredentialBeforeStateCreation(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} if _, _, err := prepare(config, req); err == nil || err.Error() != "claudesdk: unsupported HTTPS MCP bearer credential" { t.Fatal("invalid bearer accepted or unsafe error returned") } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index 0d451ee91..f2d259979 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -16,7 +16,7 @@ func TestHTTPMCPDeclaration(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}} - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} tools := []string{"echo"} switch mode { case "selected": diff --git a/apps/daemon/internal/agent/claudesdk/messages_test.go b/apps/daemon/internal/agent/claudesdk/messages_test.go index 19736ffd4..5481adbd7 100644 --- a/apps/daemon/internal/agent/claudesdk/messages_test.go +++ b/apps/daemon/internal/agent/claudesdk/messages_test.go @@ -20,7 +20,7 @@ func TestMessageObservations(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), ObserveMessages: mode != "messages-unrequested", AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), ObserveMessages: mode != "messages-unrequested", AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 9735ee71a..d8532b944 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -164,14 +164,8 @@ func prepareOptions(req proto.PromptRequestPayload, mcp bool) (startRequest, []s return startRequest{}, nil, err } start.Model, start.SystemPrompt = modelConfiguration.Model, req.SystemPrompt - var provider []string - if selected := modelConfiguration.Provider; selected != nil { - // The key renders as ANTHROPIC_API_KEY, which Claude Code sends as the - // X-Api-Key header that the anthropic protocol declares. - provider = []string{"ANTHROPIC_BASE_URL=" + selected.BaseURL, "ANTHROPIC_API_KEY=" + selected.APIKey} - } - if strings.TrimSpace(start.Model) == "" { - return fail("model is required") - } - return start, provider, nil + // The key renders as ANTHROPIC_API_KEY, which Claude Code sends as the + // X-Api-Key header that the anthropic protocol declares. + selected := modelConfiguration.Provider + return start, []string{"ANTHROPIC_BASE_URL=" + selected.BaseURL, "ANTHROPIC_API_KEY=" + selected.APIKey}, nil } diff --git a/apps/daemon/internal/agent/claudesdk/options_test.go b/apps/daemon/internal/agent/claudesdk/options_test.go index 19bd3be50..9fd82449e 100644 --- a/apps/daemon/internal/agent/claudesdk/options_test.go +++ b/apps/daemon/internal/agent/claudesdk/options_test.go @@ -1,21 +1,29 @@ package claudesdk import ( + "errors" + "os" "path/filepath" "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) -func TestModelIsRequired(t *testing.T) { +// A request needs a model and a provider: device credentials never stand in. +func TestModelAndProviderAreRequired(t *testing.T) { + t.Setenv("ANTHROPIC_API_KEY", "device-key") for _, tc := range []struct { name string req proto.PromptRequestPayload want string + err error }{ - {"no system prompt", proto.PromptRequestPayload{Model: "test-model"}, ""}, - {"system prompt", proto.PromptRequestPayload{Model: "test-model", SystemPrompt: "instructions"}, "instructions"}, - {"no model", proto.PromptRequestPayload{SystemPrompt: "instructions"}, ""}, + {"no system prompt", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "test-model"}, "", nil}, + {"system prompt", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "test-model", SystemPrompt: "instructions"}, "instructions", nil}, + {"no model", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), SystemPrompt: "instructions"}, "", harnessconfig.ErrModel}, + {"no provider", proto.PromptRequestPayload{Model: "test-model"}, "", harnessconfig.ErrModelProvider}, } { t.Run(tc.name, func(t *testing.T) { root := t.TempDir() @@ -23,9 +31,17 @@ func TestModelIsRequired(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} tc.req.RunID, tc.req.Input = "run", proto.TextInput("hello") start, _, err := prepare(config, tc.req) - if (err != nil) != (tc.req.Model == "") || start.SystemPrompt != tc.want { + if !errors.Is(err, tc.err) || (err == nil) != (tc.err == nil) || start.SystemPrompt != tc.want { t.Fatalf("system prompt %q, error %v", start.SystemPrompt, err) } + if _, statErr := os.Stat(config.StateDir); tc.err != nil && !os.IsNotExist(statErr) { + t.Fatal("rejected request created native state") + } }) } } + +// fixtureProvider is the provider every Claude request carries. +func fixtureProvider() *modelprovider.Provider { + return &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://model.example", APIKey: "fixture-key"} +} diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index fbd58f088..dbfbde766 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -66,7 +66,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { if frozen.Model != "fixture" || frozen.Resume != "native-session" || frozen.Workspace == nil || len(frozen.Input) != 0 { t.Fatal("configuration-only request was not retained") } - config.Env[0] = "ANTHROPIC_AUTH_TOKEN=changed" + config.Env[0] = "HTTPS_PROXY=http://changed.example" config.Workspace.Directory = "/changed" config.Workspace.Directory = "/changed" req.Model = "changed" diff --git a/apps/daemon/internal/agent/claudesdk/provider.go b/apps/daemon/internal/agent/claudesdk/provider.go index 19c6f567d..64d31cb4b 100644 --- a/apps/daemon/internal/agent/claudesdk/provider.go +++ b/apps/daemon/internal/agent/claudesdk/provider.go @@ -2,14 +2,13 @@ package claudesdk import "strings" +// withProvider replaces every model credential and endpoint in env with the +// Session's provider. func withProvider(env, provider []string) []string { - if provider == nil { - return env - } out := make([]string, 0, len(env)+len(provider)) for _, entry := range env { key, _, _ := strings.Cut(entry, "=") - if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" { + if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" && key != "CLAUDE_CODE_OAUTH_TOKEN" { out = append(out, entry) } } diff --git a/apps/daemon/internal/agent/claudesdk/readiness_test.go b/apps/daemon/internal/agent/claudesdk/readiness_test.go index 1e6c633b5..e322ed385 100644 --- a/apps/daemon/internal/agent/claudesdk/readiness_test.go +++ b/apps/daemon/internal/agent/claudesdk/readiness_test.go @@ -23,7 +23,7 @@ func TestRequiredMCPNeedsQualifiedRuntime(t *testing.T) { config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state"), Env: []string{ "GO_CLAUDE_READINESS_HELPER=1", "READINESS_MODE=ready-http-mcp", "GORACE=atexit_sleep_ms=0", }} - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", Required: true}}} if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" { t.Fatalf("unqualified runtime executed required MCP: %v", err) @@ -44,7 +44,7 @@ func TestHTTPMCPRejectsOldPackagedRuntime(t *testing.T) { if !info.SupportsHTTPMCP() { t.Fatal("runtime feature not recognized") } - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}}} if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || !strings.Contains(err.Error(), "packaged runtime does not support HTTP MCP") { t.Fatalf("old runtime was not rejected before execution: %v", err) @@ -81,7 +81,7 @@ func TestMCPBearerRejectsAnonymousOnlyRuntimeWithoutProbeSecrets(t *testing.T) { "GO_CLAUDE_READINESS_HELPER=1", "READINESS_MODE=ready-http-mcp", "GORACE=atexit_sleep_ms=0", }} token := "private-fixture-token" - req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}}} if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support authenticated HTTP MCP" { t.Fatalf("old runtime executed authenticated request or readiness received its secret: %v", err) diff --git a/apps/daemon/internal/agent/claudesdk/restrictions_test.go b/apps/daemon/internal/agent/claudesdk/restrictions_test.go index c52f85390..cb080ae77 100644 --- a/apps/daemon/internal/agent/claudesdk/restrictions_test.go +++ b/apps/daemon/internal/agent/claudesdk/restrictions_test.go @@ -25,7 +25,7 @@ func TestTextFactoryAcceptsRestrictiveCapabilities(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=success", "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{RunID: "restricted-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", DisableExecutionEnvironment: test.environment, DisableSubagents: test.subagents, ExecutionControls: test.controls, Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "restricted-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", DisableExecutionEnvironment: test.environment, DisableSubagents: test.subagents, ExecutionControls: test.controls, Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/session_test.go b/apps/daemon/internal/agent/claudesdk/session_test.go index dbc0d35a2..679f156c3 100644 --- a/apps/daemon/internal/agent/claudesdk/session_test.go +++ b/apps/daemon/internal/agent/claudesdk/session_test.go @@ -22,7 +22,7 @@ func TestTextFactoryCompletionAndFailures(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) @@ -79,7 +79,7 @@ func TestTextFactoryRejectsUnsupportedInput(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), Model: "fake"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), Model: "fake"} switch kind { case "execution-controls": request.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "low"} diff --git a/apps/daemon/internal/agent/claudesdk/steering_test.go b/apps/daemon/internal/agent/claudesdk/steering_test.go index 292536ca9..4157aeaab 100644 --- a/apps/daemon/internal/agent/claudesdk/steering_test.go +++ b/apps/daemon/internal/agent/claudesdk/steering_test.go @@ -27,7 +27,7 @@ func TestSteeringReceiptsAndLifecycle(t *testing.T) { if mode == "phased" { config.Env[1] = "SDK_HELPER_MODE=steering-timeout" } - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/usage_test.go b/apps/daemon/internal/agent/claudesdk/usage_test.go index 392159bfb..299078c5e 100644 --- a/apps/daemon/internal/agent/claudesdk/usage_test.go +++ b/apps/daemon/internal/agent/claudesdk/usage_test.go @@ -23,7 +23,7 @@ func TestUsageTransportPreservesSnapshotOnFailureAndDone(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=usage-" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{RunID: "usage-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "usage-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index 5476f2188..34a41588c 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -135,7 +135,7 @@ var nativeFlags = []string{"DISABLE_TELEMETRY=1", "DISABLE_ERROR_REPORTING=1", " func workspaceEnvName(name string) bool { switch name { - case "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", + case "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY": return true diff --git a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go index dfaac5162..810bbd095 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go @@ -19,8 +19,8 @@ func TestWorkspaceLaunchAndReadinessInheritsUserEnvironment(t *testing.T) { // not a native sandbox or provider acceptance test. script := `#!/bin/sh test "$OAC_TEST_PARENT_SECRET" = must-not-inherit || exit 21 -test -z "${ANTHROPIC_API_KEY+x}" || exit 22 -test "$ANTHROPIC_AUTH_TOKEN" = selected-provider-fixture || exit 23 +test "${ANTHROPIC_API_KEY-}" != unselected || exit 22 +test -z "${ANTHROPIC_AUTH_TOKEN+x}" && test "$HTTPS_PROXY" = http://proxy.example || exit 23 test "$TMPDIR" != "$CLAUDE_CONFIG_DIR/tmp" || exit 24 case "$1" in */runtime_check.js) diff --git a/apps/daemon/internal/agent/claudesdk/workspace_test.go b/apps/daemon/internal/agent/claudesdk/workspace_test.go index 57e53e35d..28d978fca 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_test.go @@ -23,7 +23,7 @@ func workspaceFixture(t *testing.T) Config { } } config := Config{Node: filepath.Join(root, "bin", "node"), Entrypoint: filepath.Join(root, "runtime", "dist", "main.js"), StateDir: filepath.Join(root, "state"), - Env: []string{"ANTHROPIC_AUTH_TOKEN=selected-provider-fixture", "ANTHROPIC_BASE_URL=https://example.invalid"}, + Env: []string{"HTTPS_PROXY=http://proxy.example"}, Workspace: &WorkspaceConfig{Directory: filepath.Join(root, "workspace"), HomeDir: filepath.Join(root, "home"), ScratchDir: filepath.Join(root, "scratch")}} for _, name := range []string{config.Node, config.Entrypoint, filepath.Join(filepath.Dir(config.Entrypoint), "runtime_check.js")} { @@ -35,7 +35,7 @@ func workspaceFixture(t *testing.T) Config { } func workspaceRequest() proto.PromptRequestPayload { - return proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableSubagents: true, + return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableSubagents: true, Model: "fixture"} } @@ -43,6 +43,7 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { config := workspaceFixture(t) t.Setenv("OAC_TEST_PARENT_SECRET", "parent-only") t.Setenv("ANTHROPIC_API_KEY", "unselected-provider") + config.Env = append(config.Env, "ANTHROPIC_BASE_URL=https://unselected.example") start, env, err := prepare(config, workspaceRequest()) if err != nil { t.Fatal(err) @@ -51,7 +52,7 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { t.Fatal("trusted binding was not retained") } raw, _ := json.Marshal(start) - if strings.Contains(string(raw), "selected-provider-fixture") || strings.Contains(string(raw), "parent-only") { + if strings.Contains(string(raw), "fixture-key") || strings.Contains(string(raw), "parent-only") { t.Fatal("secret value entered the private request") } values := map[string]string{} @@ -62,11 +63,12 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { if values["OAC_TEST_PARENT_SECRET"] != "parent-only" { t.Fatal("lost user environment") } - if _, ok := values["ANTHROPIC_API_KEY"]; ok { - t.Fatal("inherited unselected provider credential") + _, token := values["ANTHROPIC_AUTH_TOKEN"] + if token || values["ANTHROPIC_API_KEY"] != "fixture-key" || values["ANTHROPIC_BASE_URL"] != "https://model.example" { + t.Fatal("environment credentials replaced the Session provider") } if values["HOME"] != config.Workspace.HomeDir || values["CLAUDE_CONFIG_DIR"] != config.StateDir || - values["TMPDIR"] != config.Workspace.ScratchDir || values["ANTHROPIC_AUTH_TOKEN"] != "selected-provider-fixture" { + values["TMPDIR"] != config.Workspace.ScratchDir || values["HTTPS_PROXY"] != "http://proxy.example" { t.Fatal("explicit runtime environment was not preserved") } entries, err := os.ReadDir(config.StateDir) @@ -108,9 +110,9 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) { case "ambient-setting": config.Env = append(config.Env, "NODE_OPTIONS=--require=untrusted") case "duplicate-env": - config.Env = append(config.Env, "ANTHROPIC_AUTH_TOKEN=second") + config.Env = append(config.Env, "HTTPS_PROXY=http://second.example") case "bad-env": - config.Env = append(config.Env, "ANTHROPIC_API_KEY=bad\x00value") + config.Env = append(config.Env, "NO_PROXY=bad\x00value") } if _, _, err := prepare(config, req); err == nil { diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index 5019d617e..ee97e44ed 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -8,17 +8,17 @@ import ( func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state"}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state"}) if err != nil { t.Fatal(err) } plan.Cleanup() - if len(plan.ExtraConfig) != 0 { + if len(plan.ExtraConfig) != 1 || plan.ExtraConfig[0][0] != "model_provider" { t.Fatal("native settings without ExecutionControls", plan.ExtraConfig) } for _, search := range []string{"disabled", "cached", "live"} { for _, verbosity := range []string{"low", "medium", "high"} { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{WebSearch: search, TextVerbosity: verbosity}}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{WebSearch: search, TextVerbosity: verbosity}}) if err != nil { t.Fatal(err) } @@ -45,7 +45,7 @@ func TestExecutionControlsRejectIncompleteOrInvalidValues(t *testing.T) { {}, {WebSearch: "disabled"}, {TextVerbosity: "medium"}, {WebSearch: "invalid", TextVerbosity: "medium"}, {WebSearch: "disabled", TextVerbosity: "invalid"}, } { - if plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", ExecutionControls: &controls}); err == nil { + if plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &controls}); err == nil { plan.Cleanup() t.Fatal("invalid controls accepted", controls) } diff --git a/apps/daemon/internal/agent/codex/harness_config_test.go b/apps/daemon/internal/agent/codex/harness_config_test.go index 85dbadd5d..ea06e435d 100644 --- a/apps/daemon/internal/agent/codex/harness_config_test.go +++ b/apps/daemon/internal/agent/codex/harness_config_test.go @@ -28,7 +28,7 @@ func TestHarnessConfigAppliedWithoutChangingProvider(t *testing.T) { } func TestHarnessConfigConflictFailsBeforePreparation(t *testing.T) { - _, err := BuildSessionPlan(proto.PromptRequestPayload{RunID: "run", HarnessConfig: proto.HarnessConfig(`{"model_provider":"bypass"}`)}) + _, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run", HarnessConfig: proto.HarnessConfig(`{"model_provider":"bypass"}`)}) if err != harnessconfig.ErrHarnessConfig { t.Fatalf("configuration must fail before filesystem preparation: %v", err) } diff --git a/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go b/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go index ca3f561a7..8cd66c0d9 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go @@ -19,7 +19,7 @@ func TestMCPHTTPBearerPlanSeparatesServersAndProcesses(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "second", ServerURL: "https://second.example/mcp", BearerToken: &tokens[1]}, {ConnectionOrigin: "service", ServerLabel: "public", ServerURL: "http://public.example/mcp"}, } - req := proto.PromptRequestPayload{AgentStateKey: "retained-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "retained-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} seen := map[string]bool{} for range 2 { plan, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()) @@ -57,7 +57,7 @@ func TestMCPHTTPBearerRejectsInvalidTokensWithoutPersistence(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) for _, token := range []string{"", "=", " has-space", "has-space ", "has space", "line\r\ninjection", "nul\x00byte", "opaque中文", "middle=padding", "punctuation:invalid"} { servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{AgentStateKey: "invalid-bearer", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "invalid-bearer", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} if _, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil || err.Error() != "invalid HTTPS MCP bearer credential" { t.Fatal("invalid bearer value accepted or unsafe error returned") } @@ -80,7 +80,7 @@ func TestMCPHTTPBearerDoesNotReachModelCatalogProbe(t *testing.T) { } token := "synthetic-catalog-secret" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{AgentStateKey: "catalog", DisableExecutionEnvironment: true, MCPHTTPServers: &servers, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "catalog", DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture-model", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} cfg := defaultSessionConfig() cfg.codexBinary = binary diff --git a/apps/daemon/internal/agent/codex/mcp_http_test.go b/apps/daemon/internal/agent/codex/mcp_http_test.go index cde8f8f98..9a9a7732d 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_test.go @@ -20,7 +20,7 @@ func TestPublicMCPHTTPPlanOwnsConfigurationAndPreservesHistory(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "docs.server", ServerURL: "https://docs.example/mcp", AllowedTools: &tools, Required: true}, {ConnectionOrigin: "service", ServerLabel: "blocked", ServerURL: "http://127.0.0.1:12345/mcp", AllowedTools: &denyAll}, } - req := proto.PromptRequestPayload{AgentStateKey: "public-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "public-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} plan, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()) if err != nil { t.Fatal(err) @@ -99,7 +99,7 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { if err := os.WriteFile(path, stored, 0o600); err != nil { t.Fatal(err) } - req := proto.PromptRequestPayload{AgentStateKey: "credentials", DisableExecutionEnvironment: true, MCPHTTPServers: &valid} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "credentials", DisableExecutionEnvironment: true, MCPHTTPServers: &valid} if _, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil { t.Fatal("existing MCP credentials accepted") } diff --git a/apps/daemon/internal/agent/codex/message_input_test.go b/apps/daemon/internal/agent/codex/message_input_test.go index 5efa14a46..6e0238c5b 100644 --- a/apps/daemon/internal/agent/codex/message_input_test.go +++ b/apps/daemon/internal/agent/codex/message_input_test.go @@ -1,8 +1,9 @@ package codex import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func TestNativeInputRetainsImageOrderAndMessageSeparator(t *testing.T) { diff --git a/apps/daemon/internal/agent/codex/model_verbosity_test.go b/apps/daemon/internal/agent/codex/model_verbosity_test.go index e5cac5c86..3e1905255 100644 --- a/apps/daemon/internal/agent/codex/model_verbosity_test.go +++ b/apps/daemon/internal/agent/codex/model_verbosity_test.go @@ -37,7 +37,7 @@ func TestPrepareModelVerbosity(t *testing.T) { if err := os.WriteFile(binary, []byte("#!/bin/sh\nprintf '%s' '"+catalog+"'\n"), 0700); err != nil { t.Fatal(err) } - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", Model: "known-model", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "high"}}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "state", Model: "known-model", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "high"}}) if err != nil { t.Fatal(err) } @@ -80,7 +80,7 @@ func TestPrepareDefaultModelVerbosity(t *testing.T) { for _, model := range []string{"supported", "unsupported", "unknown-provider-model"} { for _, level := range []string{"low", "medium", "high"} { t.Run(model+"/"+level, func(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", Model: model, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: level}}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "state", Model: model, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: level}}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 132779246..b86683063 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -122,24 +122,18 @@ func buildSessionPlan(req proto.PromptRequestPayload, allocHome func() (agent.Vi } plan.home = home plan.Env = []string{"DISABLE_TELEMETRY=1", "CODEX_HOME=" + home.View} - if prepared.Provider != nil { - if err := writeCodexProviderConfig(home.Host, nativeProvider(*prepared.Provider)); err != nil { - return plan, err - } - plan.ModelProvider = oacProviderSlug + if err := writeCodexProviderConfig(home.Host, nativeProvider(prepared.Provider)); err != nil { + return plan, err } + plan.ModelProvider = oacProviderSlug if effort, ok := prepared.HarnessConfig["model_reasoning_effort"].(string); ok { plan.ModelReasoningEffort = effort plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_reasoning_effort", strconv(effort)}) } - if plan.ModelProvider != "" { - // Pin model_provider at the CLI layer so codex skips its builtin - // "openai" provider — without this the [model_providers.oac] - // block we wrote into config.toml would be loaded but never - // selected (the default model_provider is "openai"). - plan.ExtraConfig = append(plan.ExtraConfig, - [2]string{"model_provider", strconv(plan.ModelProvider)}) - } + // Pin model_provider at the CLI layer so codex skips its builtin "openai" + // provider; otherwise the [model_providers.oac] block in config.toml would + // be loaded but never selected. + plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_provider", strconv(plan.ModelProvider)}) return plan, nil } diff --git a/apps/daemon/internal/agent/codex/options_test.go b/apps/daemon/internal/agent/codex/options_test.go index a10975b3f..b0263b13e 100644 --- a/apps/daemon/internal/agent/codex/options_test.go +++ b/apps/daemon/internal/agent/codex/options_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "conv-1/agent-1/codex"}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv-1/agent-1/codex"}) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } @@ -25,7 +25,7 @@ func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { } func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "conv-1/agent-1/codex"}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv-1/agent-1/codex"}) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } @@ -50,11 +50,11 @@ func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { func TestBuildSessionPlan_StableCodexHomeByStateKey(t *testing.T) { stateKey := "conv-stable/agent-stable/codex" - planA, err := BuildSessionPlan(proto.PromptRequestPayload{RunID: "run-a", AgentStateKey: stateKey}) + planA, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run-a", AgentStateKey: stateKey}) if err != nil { t.Fatalf("BuildSessionPlan A: %v", err) } - planB, err := BuildSessionPlan(proto.PromptRequestPayload{RunID: "run-b", AgentStateKey: stateKey}) + planB, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run-b", AgentStateKey: stateKey}) if err != nil { t.Fatalf("BuildSessionPlan B: %v", err) } @@ -74,7 +74,7 @@ func codexHomeFromEnv(env []string) string { func TestBuildSessionPlan_CarriesModelAndSystemPrompt(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "conv/agent/codex", Model: "MiniMax-M3", SystemPrompt: "current reference"}) + plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "conv/agent/codex", Model: "MiniMax-M3", SystemPrompt: "current reference"}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/permission_profile_test.go b/apps/daemon/internal/agent/codex/permission_profile_test.go index dc250c7be..eed3437f1 100644 --- a/apps/daemon/internal/agent/codex/permission_profile_test.go +++ b/apps/daemon/internal/agent/codex/permission_profile_test.go @@ -11,7 +11,7 @@ import ( func TestRuntimeUsesHostPermissions(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) { - req := proto.PromptRequestPayload{AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()}} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()}} plan, _, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) if err != nil { t.Fatal(err) @@ -51,7 +51,7 @@ func TestSelfHostedToolEnvironmentCannotRedirectNativeHistory(t *testing.T) { for key, value := range map[string]string{"OAC_RUNTIME_ENVIRONMENT_ID": "b3d154b8-543b-4248-97b1-665f9f418d52", "OAC_RUNTIME_SESSION_ID": "33e02e0d-6fc8-4904-9d7a-4b61b9094ae0", "OAC_RUNTIME_WORKSPACE": workspace, "OAC_RUNTIME_CAPABILITY_DIRECTORY": filepath.Join(root, "capabilities"), "OAC_RUNTIME_NETWORK_ACCESS": "enabled", "OAC_RUNTIME_TOOL_ENV_FILE": config} { t.Setenv(key, value) } - req := proto.PromptRequestPayload{AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} + req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} plan, _, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/codex/preparation_helpers_test.go b/apps/daemon/internal/agent/codex/preparation_helpers_test.go index 19ef9367a..f21fe49e3 100644 --- a/apps/daemon/internal/agent/codex/preparation_helpers_test.go +++ b/apps/daemon/internal/agent/codex/preparation_helpers_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) type preparationFrame struct { @@ -44,6 +45,7 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig AgentKind: "codex", AgentStateKey: "prepared-session", ReleaseOnCompletion: true, StrictResume: true, Model: "fixture-model", + ModelProvider: fixtureProvider(), ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, DisableExecutionEnvironment: true, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"value":{"type":"integer"}}}`)}}, @@ -51,6 +53,10 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig return req, cfg, root } +func fixtureProvider() *modelprovider.Provider { + return &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://model.example/v1", APIKey: "fixture-key"} +} + func preparationFrames(t *testing.T, root string) []preparationFrame { t.Helper() data, err := os.ReadFile(filepath.Join(root, "frames.jsonl")) diff --git a/apps/daemon/internal/agent/codex/preparation_router_test.go b/apps/daemon/internal/agent/codex/preparation_router_test.go index 7f7ccb53e..62d31d24c 100644 --- a/apps/daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/daemon/internal/agent/codex/preparation_router_test.go @@ -1,13 +1,8 @@ package codex -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/google/uuid" "os" "path/filepath" "testing" @@ -15,8 +10,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" + harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/codex" + "github.com/google/uuid" ) type preparationWireSender chan proto.Envelope @@ -59,7 +58,7 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { req.DisableExecutionEnvironment = false req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfiguration.Configuration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary Factory must not run") }) prepared := make(chan *Prepared, 1) diff --git a/apps/daemon/internal/agent/codex/provider_config_test.go b/apps/daemon/internal/agent/codex/provider_config_test.go index 620a1fb4c..650db06bd 100644 --- a/apps/daemon/internal/agent/codex/provider_config_test.go +++ b/apps/daemon/internal/agent/codex/provider_config_test.go @@ -170,22 +170,6 @@ func TestBuildSessionPlan_PinsModelProviderWhenProviderSet(t *testing.T) { } } -func TestBuildSessionPlan_NoProviderLeavesBuiltinDefault(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{RunID: "run-y", AgentStateKey: "conv-1/agent-1/codex"}) - if err != nil { - t.Fatalf("BuildSessionPlan: %v", err) - } - defer plan.Cleanup() - if plan.ModelProvider != "" { - t.Fatalf("plan.ModelProvider = %q, want empty when no provider configured", plan.ModelProvider) - } - for _, kv := range plan.ExtraConfig { - if kv[0] == "model_provider" { - t.Fatalf("model_provider override leaked into ExtraConfig: %+v", plan.ExtraConfig) - } - } -} - func mustReadFile(t *testing.T, path string) string { t.Helper() b, err := os.ReadFile(path) diff --git a/apps/daemon/internal/agent/codex/session_knowledge_test.go b/apps/daemon/internal/agent/codex/session_knowledge_test.go index 1edc883b4..a43425e31 100644 --- a/apps/daemon/internal/agent/codex/session_knowledge_test.go +++ b/apps/daemon/internal/agent/codex/session_knowledge_test.go @@ -3,9 +3,10 @@ package codex import ( "context" "encoding/json" - "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "testing" "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" ) func TestResumeRefreshesReferenceContext(t *testing.T) { diff --git a/apps/daemon/internal/agent/codex/session_policy_test.go b/apps/daemon/internal/agent/codex/session_policy_test.go index a96b5c5e5..fcf9a0486 100644 --- a/apps/daemon/internal/agent/codex/session_policy_test.go +++ b/apps/daemon/internal/agent/codex/session_policy_test.go @@ -16,7 +16,7 @@ func TestThreadRequestsApplyDeploymentPolicy(t *testing.T) { for _, method := range []string{"thread/start", "thread/resume"} { t.Run(method, func(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, _, err := prepareSessionPlan(context.Background(), proto.PromptRequestPayload{AgentStateKey: "conv/agent/codex", DisableSubagents: true}, sessionConfig{}) + plan, _, err := prepareSessionPlan(context.Background(), proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv/agent/codex", DisableSubagents: true}, sessionConfig{}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/view.go b/apps/daemon/internal/agent/codex/view.go index 0cd9ea312..ddd14d75d 100644 --- a/apps/daemon/internal/agent/codex/view.go +++ b/apps/daemon/internal/agent/codex/view.go @@ -180,7 +180,8 @@ func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg se // mount. No trust entry is written, so the project stays untrusted. plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"allow_login_shell", "false"}, [2]string{"project_root_markers", "[]"}) if req.ExecutionControls != nil { - if err := viewModelVerbosity(ctx, cfg.codexBinary, &plan, req.ModelProvider); err != nil { + // buildSessionPlan admitted the request's provider. + if err := viewModelVerbosity(ctx, cfg.codexBinary, &plan, *req.ModelProvider); err != nil { plan.Cleanup() return SessionPlan{}, err } @@ -218,7 +219,7 @@ func viewHome(home agent.ViewDir) (agent.ViewDir, error) { // viewModelVerbosity reads the catalog from the trusted install on this host, // with a scratch CODEX_HOME that holds only the Session's provider. -func viewModelVerbosity(ctx context.Context, binary string, plan *SessionPlan, provider *modelprovider.Provider) error { +func viewModelVerbosity(ctx context.Context, binary string, plan *SessionPlan, provider modelprovider.Provider) error { scratch, err := os.MkdirTemp("", "oac-codex-catalog-") if err != nil { return err @@ -230,10 +231,8 @@ func viewModelVerbosity(ctx context.Context, binary string, plan *SessionPlan, p return err } } - if provider != nil { - if err := writeCodexProviderConfig(home, nativeProvider(*provider)); err != nil { - return err - } + if err := writeCodexProviderConfig(home, nativeProvider(provider)); err != nil { + return err } probe := catalogProbe{binary: binary, dir: home, env: []string{"HOME=" + home, "CODEX_HOME=" + home, "TMPDIR=" + tmp, "DISABLE_TELEMETRY=1"}} return verifyModelVerbosity(ctx, probe, plan) diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index 2fc4e7aff..b7fb7180c 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -49,6 +49,7 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { for _, req := range []proto.PromptRequestPayload{ {Model: "fixture", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example", APIKey: "private-sentinel"}}, {ModelProvider: responses}, + {Model: "fixture"}, {HarnessConfig: proto.HarnessConfig(`{"unknown":"private-sentinel"}`)}, } { before := calls diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 13448f800..04309d913 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -272,7 +272,7 @@ type ViewSession struct { // is the gateway with the placeholder key, and MCP arrives only in session.MCP // and without credentials. func checkViewHandoff(req proto.PromptRequestPayload, prepared harnessconfig.PreparedConfiguration, session ViewSession) error { - if provider := prepared.Provider; provider == nil || provider.APIKey != modelprovider.Placeholder || !isGatewayURL(provider.BaseURL, false) { + if provider := prepared.Provider; provider.APIKey != modelprovider.Placeholder || !isGatewayURL(provider.BaseURL, false) { return fmt.Errorf("%w: the model provider is not the Session's gateway", ErrViewHandoff) } if req.MCPHTTPServers != nil || (req.LocalEnvironment != nil && len(req.LocalEnvironment.MCP) > 0) { diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index a2b6e148a..3717a159f 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -82,15 +82,12 @@ func prepareOptionsWithTools(req proto.PromptRequestPayload, tools *workspaceToo } // validateOptions checks the request before any native effect and returns its -// model configuration, which must name a model and a model provider. +// model configuration. func validateOptions(req proto.PromptRequestPayload) (harnessconfig.PreparedConfiguration, error) { prepared, err := harnessconfiguration.Configuration().Prepare(req) if err != nil { return prepared, err } - if prepared.Model == "" || prepared.Provider == nil { - return prepared, fmt.Errorf("mcode: model and model provider are required") - } if req.StrictResume { if err := validateExecutionRequest(req); err != nil { return prepared, err @@ -114,7 +111,7 @@ func writeNativeConfig(req proto.PromptRequestPayload, prepared harnessconfig.Pr return err } config := map[string]any{"logLevel": "error", "skills": map[string]any{"external": map[string]any{"enabled": false}}} - config["custom_provider"] = map[string]any{"oac": modelProviderConfig(*prepared.Provider, prepared.Model)} + config["custom_provider"] = map[string]any{"oac": modelProviderConfig(prepared.Provider, prepared.Model)} if req.StrictResume { configureTextExecution(config) if !req.DisableSubagents { diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index c41b6269a..9614a7f7e 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -1,7 +1,5 @@ package agent_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -34,13 +32,13 @@ func (stubSession) SubmitPromptForUserChoice(context.Context, string, proto.Prom func TestRegistryResolveReturnsRegisteredFactory(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("cc")) f, err := reg.Resolve("fake_alpha") if err != nil { t.Fatalf("Resolve: %v", err) } - sess, err := f(context.Background(), proto.PromptRequestPayload{AgentKind: "fake_alpha"}, nil) + sess, err := f(context.Background(), prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"}), nil) if err != nil { t.Fatalf("factory: %v", err) } @@ -52,7 +50,7 @@ func TestRegistryResolveReturnsRegisteredFactory(t *testing.T) { func TestRegistryResolveUnknownKindReturnsTypedError(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("cc")) _, err := reg.Resolve("fake_beta") if !errors.Is(err, agent.ErrUnsupportedKind) { @@ -62,14 +60,14 @@ func TestRegistryResolveUnknownKindReturnsTypedError(t *testing.T) { func TestRegistryRegisterOverwrites(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v1")) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v2")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("v1")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("v2")) f, err := reg.Resolve("k") if err != nil { t.Fatalf("Resolve: %v", err) } - sess, _ := f(context.Background(), proto.PromptRequestPayload{}, nil) + sess, _ := f(context.Background(), prototest.WithModel(proto.PromptRequestPayload{}), nil) if got := sess.(stubSession).marker; got != "v2" { t.Errorf("overwrite: marker = %q, want v2", got) } @@ -77,8 +75,8 @@ func TestRegistryRegisterOverwrites(t *testing.T) { func TestRegistryKindsReportsRegistered(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_beta", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("oc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_beta", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("oc")) got := reg.Kinds() slices.Sort(got) @@ -94,7 +92,7 @@ func TestRegistryRegisterPanicsOnEmptyKind(t *testing.T) { t.Fatal("Register(\"\", ...) did not panic") } }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("x")) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("x")) } func TestRegistryRegisterPanicsOnNilFactory(t *testing.T) { @@ -103,7 +101,7 @@ func TestRegistryRegisterPanicsOnNilFactory(t *testing.T) { t.Fatal("Register(kind, nil) did not panic") } }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, nil) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), nil) } func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { @@ -115,7 +113,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ Streaming: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}, stubFactory("oc")) + }, prototest.ModelConfiguration(), stubFactory("oc")) reg.RegisterKind(proto.SupportedAgentKind{ Kind: "fake_alpha", Available: true, @@ -126,7 +124,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}, stubFactory("cc")) + }, prototest.ModelConfiguration(), stubFactory("cc")) got := reg.SupportedAgentKinds() if len(got) != 2 { @@ -145,7 +143,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("native")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("native")) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("legacy factory implied reusable execution") } @@ -155,10 +153,10 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := factory(t.Context(), proto.PromptRequestPayload{}); !errors.Is(err, expected) { + if _, err := factory(t.Context(), prototest.WithModel(proto.PromptRequestPayload{})); !errors.Is(err, expected) { t.Fatal(err) } - registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("replacement")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), stubFactory("replacement")) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("replacing a kind retained its old executor capability") } @@ -169,7 +167,7 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { for i := 0; i < reflect.TypeOf(valid).NumField(); i++ { t.Run(reflect.TypeOf(valid).Field(i).Name, func(t *testing.T) { registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid}, harnessconfig.Configuration{}, stubFactory("original")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid}, prototest.ModelConfiguration(), stubFactory("original")) missing := valid reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) func() { @@ -178,13 +176,13 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { t.Error("incomplete declaration registered") } }() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, harnessconfig.Configuration{}, stubFactory("replacement")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, prototest.ModelConfiguration(), stubFactory("replacement")) }() factory, err := registry.Resolve("fixture") if err != nil { t.Fatal(err) } - session, err := factory(t.Context(), proto.PromptRequestPayload{}, nil) + session, err := factory(t.Context(), prototest.WithModel(proto.PromptRequestPayload{}), nil) if err != nil || session.(stubSession).marker != "original" { t.Fatal("failed declaration changed registry") } diff --git a/apps/daemon/internal/cli/connect_cleanup_test.go b/apps/daemon/internal/cli/connect_cleanup_test.go index e8ef2d21e..24cf58f4c 100644 --- a/apps/daemon/internal/cli/connect_cleanup_test.go +++ b/apps/daemon/internal/cli/connect_cleanup_test.go @@ -1,7 +1,5 @@ package cli -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -116,7 +114,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { owner := &cleanupExecutor{retry: make(chan struct{}), confirm: make(chan struct{})} registry := agent.NewRegistry() registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("unexpected legacy factory") }) var factories atomic.Int32 @@ -148,7 +146,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { } defer peer.Close() env, err := proto.NewEnvelope(proto.TypeExecutionPrepare, "prepare", proto.ExecutionPreparePayload{SessionID: "cleanup", - Configuration: proto.PromptRequestPayload{AgentKind: "cleanup", AgentStateKey: "agents-api-cleanup", StrictResume: true, DisableExecutionEnvironment: true}}) + Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "cleanup", AgentStateKey: "agents-api-cleanup", StrictResume: true, DisableExecutionEnvironment: true})}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/dispatch/cancellation_test.go b/apps/daemon/internal/dispatch/cancellation_test.go index f4273d5ef..4c5d32e69 100644 --- a/apps/daemon/internal/dispatch/cancellation_test.go +++ b/apps/daemon/internal/dispatch/cancellation_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -29,11 +27,11 @@ func TestCompletionWaitsForNativeWriterRelease(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) sess := &cancelReceiptSession{entered: make(chan struct{}), release: make(chan struct{})} - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess.fakeSession = &fakeSession{out: out, closeOutOnCancel: true} return sess, nil }) - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "release", proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "stable", ReleaseOnCompletion: true})); err != nil { + if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "release", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "stable", ReleaseOnCompletion: true}))); err != nil { t.Fatal(err) } sess.out <- mustEnv(t, proto.TypeDone, "release", proto.DonePayload{Content: "Finished"}) @@ -76,11 +74,11 @@ func TestCancellationReceiptFollowsAdapterOutcome(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) sess := &cancelReceiptSession{entered: make(chan struct{}), release: make(chan struct{}), outcome: test.outcome, err: test.err} - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess.fakeSession = &fakeSession{out: out, closeOutOnCancel: true} return sess, nil }) - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "codex"})); err != nil { + if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex"}))); err != nil { t.Fatal(err) } done := make(chan error, 1) @@ -125,7 +123,7 @@ func TestCancellationReceiptFollowsAdapterOutcome(t *testing.T) { func TestLegacyCancellationDoesNotEmitNewFrames(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "legacy", proto.PromptRequestPayload{AgentKind: "fake_alpha"})); err != nil { + if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "legacy", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"}))); err != nil { t.Fatal(err) } sess := <-h.gotSess diff --git a/apps/daemon/internal/dispatch/capability_admission_test.go b/apps/daemon/internal/dispatch/capability_admission_test.go index 0a9fde120..e6975aef5 100644 --- a/apps/daemon/internal/dispatch/capability_admission_test.go +++ b/apps/daemon/internal/dispatch/capability_admission_test.go @@ -9,7 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) func TestSteeringUsesAdmittedDeclarationAndDoesNotReplayUnsupportedImplementation(t *testing.T) { @@ -25,13 +24,13 @@ func TestSteeringUsesAdmittedDeclarationAndDoesNotReplayUnsupportedImplementatio }}, nil } info := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilityFromBool(supported)})} - h.reg.RegisterKind(info, harnessconfig.Configuration{}, factory) - if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "fixture"})); err != nil { + h.reg.RegisterKind(info, prototest.ModelConfiguration(), factory) + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fixture"}))); err != nil { t.Fatal(err) } // A new registration cannot rewrite the already admitted owner's contract. info.Capabilities.Steering = proto.CapabilityFromBool(!supported) - h.reg.RegisterKind(info, harnessconfig.Configuration{}, factory) + h.reg.RegisterKind(info, prototest.ModelConfiguration(), factory) for range 2 { if err := handleSteeringAndWait(t, h, mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "input", Input: proto.TextInput("hello")})); err != nil { t.Fatal(err) @@ -64,11 +63,11 @@ func TestInteractionDeclarationPrecedesResponderMethods(t *testing.T) { defer h.router.Shutdown(context.Background()) var session *fakeSession info := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Permissions: proto.CapabilityFromBool(supported)})} - h.reg.RegisterKind(info, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(info, prototest.ModelConfiguration(), func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true, submitErr: agent.ErrUnsupportedOperation, askErr: agent.ErrUnsupportedOperation} return session, nil }) - if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "fixture"})); err != nil { + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fixture"}))); err != nil { t.Fatal(err) } event := mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "interaction", Tool: "fixture"}) diff --git a/apps/daemon/internal/dispatch/environment_test.go b/apps/daemon/internal/dispatch/environment_test.go index 0e72f9020..1fe4a940e 100644 --- a/apps/daemon/internal/dispatch/environment_test.go +++ b/apps/daemon/internal/dispatch/environment_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -16,11 +14,11 @@ func TestNoEnvironmentRejectsOtherEngineBeforeFactory(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) called := false - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) - err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "none", proto.PromptRequestPayload{AgentKind: "fake_alpha", DisableExecutionEnvironment: true})) + err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "none", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha", DisableExecutionEnvironment: true}))) if err == nil || called { t.Fatal("unsupported engine was started", err) } @@ -35,11 +33,11 @@ func TestNoEnvironmentUsesAvailableCapability(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) called := false - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, errors.New("controlled factory stop") }) - _ = h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "sdk", proto.PromptRequestPayload{AgentKind: "claude_sdk", DisableExecutionEnvironment: true})) + _ = h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "sdk", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "claude_sdk", DisableExecutionEnvironment: true}))) if called != available { t.Fatalf("factory called=%t, available=%t", called, available) } @@ -54,7 +52,7 @@ func TestLocalEnvironmentRequiresAvailableCapability(t *testing.T) { called := false h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: mode != "unavailable", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(mode != "unsupported")})}, - harnessconfig.Configuration{}, func(_ context.Context, req proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { + prototest.ModelConfiguration(), func(_ context.Context, req proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { called = true if req.LocalEnvironment == nil || req.LocalEnvironment.ID != preparationEnvironmentID { t.Error("local descriptor lost before factory") diff --git a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go index b99a1ef84..30aef2660 100644 --- a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -127,7 +125,7 @@ func TestExecutorCancellationReachesNativeBeforeDurableReceiptJoin(t *testing.T) sender := &receiptCancelSender{recSender: &recSender{}, entered: make(chan struct{}), release: make(chan struct{})} owner := &receiptCancelExecutor{turn: make(chan *receiptCancelTurn, 2), cancelFails: mode == "cancel_failure" || mode == "close_failure_retry", closeFailsFirst: mode == "close_failure_retry", closeEntered: make(chan struct{}), closeRelease: make(chan struct{})} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("legacy factory forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) diff --git a/apps/daemon/internal/dispatch/executor_handoff_test.go b/apps/daemon/internal/dispatch/executor_handoff_test.go index b0fc793fa..9f3b233c2 100644 --- a/apps/daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/daemon/internal/dispatch/executor_handoff_test.go @@ -1,7 +1,5 @@ package dispatch -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -94,7 +92,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { owner := &terminalHandoffExecutor{turns: make(chan *terminalHandoffTurn, 3)} registry := agent.NewRegistry() registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("legacy path forbidden") }) var creates atomic.Int32 @@ -150,7 +148,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } } } - request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "handoff", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true}} + request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "handoff", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true})} admit := func(id string) proto.PreparationStatusPayload { t.Helper() handle(proto.TypeExecutionPrepare, id, request) diff --git a/apps/daemon/internal/dispatch/executor_test.go b/apps/daemon/internal/dispatch/executor_test.go index 7167ac02c..74274af6d 100644 --- a/apps/daemon/internal/dispatch/executor_test.go +++ b/apps/daemon/internal/dispatch/executor_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -70,13 +68,13 @@ func (t *reusableTurn) AwaitSettlement(ctx context.Context) (agent.TurnSettlemen } func executorRequest() proto.ExecutionPreparePayload { - return proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "reusable", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true}} + return proto.ExecutionPreparePayload{SessionID: "session", Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "reusable", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true})} } func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) (*dispatch.Router, *recSender, *atomic.Int32) { t.Helper() calls := &atomic.Int32{} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary factory is forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { @@ -220,7 +218,7 @@ func TestExecutorPreInputFailureConfirmsCloseBeforeRetrySignal(t *testing.T) { func poolRouter(t *testing.T, factory agent.ExecutorFactory) (*dispatch.Router, *recSender) { t.Helper() registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("unexpected legacy factory") }) registry.RegisterExecutor("reusable", factory) diff --git a/apps/daemon/internal/dispatch/functions_native_test.go b/apps/daemon/internal/dispatch/functions_native_test.go index 63fd1c462..a17d0f7a7 100644 --- a/apps/daemon/internal/dispatch/functions_native_test.go +++ b/apps/daemon/internal/dispatch/functions_native_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "encoding/json" @@ -111,7 +109,7 @@ func TestNativeFunctionBridge(t *testing.T) { })) defer model.Close() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, codex.Factory) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration(), codex.Factory) sender := make(nativeFunctionSender, 256) router, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender}) if err != nil { diff --git a/apps/daemon/internal/dispatch/functions_test.go b/apps/daemon/internal/dispatch/functions_test.go index 59be84409..663d7a3c0 100644 --- a/apps/daemon/internal/dispatch/functions_test.go +++ b/apps/daemon/internal/dispatch/functions_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "bytes" "context" @@ -38,7 +36,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { reg := agent.NewRegistry() sender := &recSender{} sessions := map[string]*functionSession{} - reg.RegisterKind(proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { s := &functionSession{fakeSession: &fakeSession{out: out, ctx: ctx, closeOutOnCancel: true}} sessions[p.RunID] = s return s, nil @@ -49,7 +47,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { } defer router.Shutdown(context.Background()) for _, id := range []string{"one", "two"} { - env, _ := proto.NewEnvelope(proto.TypePromptRequest, id, proto.PromptRequestPayload{AgentKind: "function-test", Input: proto.TextInput("lookup"), FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}}) + env, _ := proto.NewEnvelope(proto.TypePromptRequest, id, prototest.WithModel(proto.PromptRequestPayload{AgentKind: "function-test", Input: proto.TextInput("lookup"), FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}})) if err := router.Handle(t.Context(), env); err != nil { t.Fatal(err) } @@ -138,14 +136,14 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { func TestFunctionToolsRequireAdvertisedSupport(t *testing.T) { reg := agent.NewRegistry() called := false - reg.RegisterKind(proto.SupportedAgentKind{Kind: "unsupported", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "unsupported", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) sender := &recSender{} router, _ := dispatch.New(dispatch.Config{Registry: reg, Sender: sender}) defer router.Shutdown(context.Background()) - env, _ := proto.NewEnvelope(proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "unsupported", FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}}) + env, _ := proto.NewEnvelope(proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "unsupported", FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}})) if err := router.Handle(t.Context(), env); err == nil || called { t.Fatal("unsupported engine silently ignored tools", err) } @@ -166,14 +164,14 @@ func functionResultContent(text string) []proto.InputContent { func TestDiscoveryCannotReachAnEagerOnlyAdapter(t *testing.T) { reg := agent.NewRegistry() called := false - reg.RegisterKind(proto.SupportedAgentKind{Kind: "eager-only", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "eager-only", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) router, _ := dispatch.New(dispatch.Config{Registry: reg, Sender: &recSender{}}) defer router.Shutdown(context.Background()) for _, search := range []bool{false, true} { - env, _ := proto.NewEnvelope(proto.TypePromptRequest, "discovery", proto.PromptRequestPayload{AgentKind: "eager-only", ToolSearch: search, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`), DeferLoading: true}}}) + env, _ := proto.NewEnvelope(proto.TypePromptRequest, "discovery", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "eager-only", ToolSearch: search, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`), DeferLoading: true}}})) if err := router.Handle(t.Context(), env); err == nil || called { t.Fatal("deferred definitions reached an eager-only adapter", err) } diff --git a/apps/daemon/internal/dispatch/mcp_http_test.go b/apps/daemon/internal/dispatch/mcp_http_test.go index 45ae2fe80..e809961a5 100644 --- a/apps/daemon/internal/dispatch/mcp_http_test.go +++ b/apps/daemon/internal/dispatch/mcp_http_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "encoding/json" @@ -22,7 +20,7 @@ func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) { defer h.router.Shutdown(context.Background()) token := "synthetic-private-token" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{AgentKind: "codex", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", DisableExecutionEnvironment: true, MCPHTTPServers: &servers}) caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported}) switch mode { case "claude", "claude old peer", "claude local": @@ -56,7 +54,7 @@ func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) { } called := false h.reg.RegisterKind(proto.SupportedAgentKind{Kind: req.AgentKind, Available: mode != "unavailable", Capabilities: caps}, - harnessconfig.Configuration{}, func(_ context.Context, got proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { + prototest.ModelConfiguration(), func(_ context.Context, got proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { called = true if mode == "required" && !(*got.MCPHTTPServers)[0].Required { t.Error("required initialization lost before adapter") @@ -105,7 +103,7 @@ func TestLocalMCPOriginAndCapabilityAdmission(t *testing.T) { req.Configuration.MCPHTTPServers = nil } entered := make(chan struct{}, 1) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { t.Error("ordinary factory called") return nil, errors.New("unexpected") }) diff --git a/apps/daemon/internal/dispatch/native_file_results_test.go b/apps/daemon/internal/dispatch/native_file_results_test.go index dcc8b83bf..2cd90fe56 100644 --- a/apps/daemon/internal/dispatch/native_file_results_test.go +++ b/apps/daemon/internal/dispatch/native_file_results_test.go @@ -2,11 +2,12 @@ package dispatch import ( "errors" + "io/fs" + "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" - "io/fs" - "testing" ) func TestNativeDirectoryFailureMapping(t *testing.T) { diff --git a/apps/daemon/internal/dispatch/optional_interactions_test.go b/apps/daemon/internal/dispatch/optional_interactions_test.go index dc63be1eb..84f73984b 100644 --- a/apps/daemon/internal/dispatch/optional_interactions_test.go +++ b/apps/daemon/internal/dispatch/optional_interactions_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "testing" @@ -20,12 +18,12 @@ func TestOptionalInteractionResponders(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var output chan<- proto.Envelope - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "minimal", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "minimal", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration(), func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { output = out s := &fakeSession{out: out, closeOutOnCancel: true} return lifecycleOnly{Session: s}, nil }) - if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "minimal"})); err != nil { + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "minimal"}))); err != nil { t.Fatal(err) } event := mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "interaction", Tool: "fixture"}) diff --git a/apps/daemon/internal/dispatch/preparation_cleanup_test.go b/apps/daemon/internal/dispatch/preparation_cleanup_test.go index 20a45212b..7ac57ee41 100644 --- a/apps/daemon/internal/dispatch/preparation_cleanup_test.go +++ b/apps/daemon/internal/dispatch/preparation_cleanup_test.go @@ -222,5 +222,6 @@ func TestPublishedPreparedRunRetainsRetryAfterHandleRetirement(t *testing.T) { func readOnlyPreparationRequest() proto.ExecutionPreparePayload { req := preparationRequest() req.Configuration.WorkspaceReadOnly = true + req.Configuration.Model, req.Configuration.ModelProvider = "", nil return req } diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index e7b7b904f..62ac78bfa 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -113,13 +111,13 @@ func localPreparationHarness(t *testing.T) *harness { } func preparationRequest() proto.ExecutionPreparePayload { - return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: proto.PromptRequestPayload{AgentKind: "prepared", AgentStateKey: "agents-api-" + preparationSessionID, StrictResume: true, ReleaseOnCompletion: true, LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, NetworkAccess: "enabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}}} + return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "prepared", AgentStateKey: "agents-api-" + preparationSessionID, StrictResume: true, ReleaseOnCompletion: true, LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, NetworkAccess: "enabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}})} } func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary Factory must not be used for preparation") }) reg.RegisterPreparation("prepared", true, factory) diff --git a/apps/daemon/internal/dispatch/receipt_order_test.go b/apps/daemon/internal/dispatch/receipt_order_test.go index 8fb22b4c4..4f43bcc62 100644 --- a/apps/daemon/internal/dispatch/receipt_order_test.go +++ b/apps/daemon/internal/dispatch/receipt_order_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -52,7 +50,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { registry := agent.NewRegistry() var session *fakeSession var calls atomic.Int32 - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload) error { calls.Add(1) @@ -75,7 +73,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { t.Fatal(err) } } - handle(proto.TypePromptRequest, proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "stable", ReleaseOnCompletion: true}) + handle(proto.TypePromptRequest, prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "stable", ReleaseOnCompletion: true})) input := proto.PromptSteerPayload{InputID: "input-1", Input: proto.TextInput("original")} handle(proto.TypePromptSteer, input) <-sender.entered @@ -125,7 +123,7 @@ func TestShutdownReleasesSteeringWorkerAndCompletionBarrier(t *testing.T) { registry := agent.NewRegistry() entered, exited := make(chan struct{}), make(chan struct{}) var session *fakeSession - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(ctx context.Context, _ proto.PromptSteerPayload) error { close(entered) @@ -142,7 +140,7 @@ func TestShutdownReleasesSteeringWorkerAndCompletionBarrier(t *testing.T) { t.Fatal(err) } defer router.Shutdown(context.Background()) - if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "shutdown", proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true})); err != nil { + if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "shutdown", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true}))); err != nil { t.Fatal(err) } if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptSteer, "shutdown", proto.PromptSteerPayload{InputID: "one", Input: proto.TextInput("text")})); err != nil { diff --git a/apps/daemon/internal/dispatch/receipt_shutdown_test.go b/apps/daemon/internal/dispatch/receipt_shutdown_test.go index 4455f59ac..882aeb994 100644 --- a/apps/daemon/internal/dispatch/receipt_shutdown_test.go +++ b/apps/daemon/internal/dispatch/receipt_shutdown_test.go @@ -1,16 +1,15 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" + "testing" + "time" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "testing" - "time" ) type shutdownAllSendsBlockSender struct { @@ -47,7 +46,7 @@ func TestShutdownCancelsCompletionErrorSend(t *testing.T) { sender := &shutdownAllSendsBlockSender{entered: make(chan struct{}), terminal: make(chan context.Context, 1), rescue: make(chan struct{})} registry := agent.NewRegistry() var session *fakeSession - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload) error { return nil }}, nil }) @@ -55,7 +54,7 @@ func TestShutdownCancelsCompletionErrorSend(t *testing.T) { if err != nil { t.Fatal(err) } - if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "shutdown-terminal", proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true})); err != nil { + if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "shutdown-terminal", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true}))); err != nil { t.Fatal(err) } if err = router.Handle(context.Background(), mustEnv(t, proto.TypePromptSteer, "shutdown-terminal", proto.PromptSteerPayload{InputID: "one", Input: proto.TextInput("text")})); err != nil { diff --git a/apps/daemon/internal/dispatch/router_test.go b/apps/daemon/internal/dispatch/router_test.go index 20a1a9400..9b05035a8 100644 --- a/apps/daemon/internal/dispatch/router_test.go +++ b/apps/daemon/internal/dispatch/router_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -156,7 +154,7 @@ func newHarnessWithIdleTimeout(t *testing.T, idleTimeout time.Duration) *harness gotReq: make(chan proto.PromptRequestPayload, 16), gotSess: make(chan *fakeSession, 16), } - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Permissions: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Permissions: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess := &fakeSession{out: out, ctx: ctx} h.gotReq <- req h.gotSess <- sess @@ -174,9 +172,9 @@ func TestCompletedSessionCancelsAfterIdleTimeout(t *testing.T) { h := newHarnessWithIdleTimeout(t, 40*time.Millisecond) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_idle", proto.PromptRequestPayload{ + env := mustEnv(t, proto.TypePromptRequest, "run_idle", prototest.WithModel(proto.PromptRequestPayload{ AgentKind: "fake_alpha", ConversationID: "conv-idle", AgentStateKey: "conv-idle/agent/fake_alpha", - }) + })) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("Handle prompt_request: %v", err) } @@ -197,9 +195,9 @@ func TestNewPromptResetsCompletedSessionIdleTimeout(t *testing.T) { defer h.router.Shutdown(context.Background()) stateKey := "conv-renew/agent/fake_alpha" - first := mustEnv(t, proto.TypePromptRequest, "run_first", proto.PromptRequestPayload{ + first := mustEnv(t, proto.TypePromptRequest, "run_first", prototest.WithModel(proto.PromptRequestPayload{ AgentKind: "fake_alpha", ConversationID: "conv-renew", AgentStateKey: stateKey, - }) + })) if err := h.router.Handle(context.Background(), first); err != nil { t.Fatalf("Handle first prompt: %v", err) } @@ -208,9 +206,9 @@ func TestNewPromptResetsCompletedSessionIdleTimeout(t *testing.T) { waitFor(t, func() bool { return h.router.ActiveRuns() == 0 }, "first run cleanup") time.Sleep(50 * time.Millisecond) - second := mustEnv(t, proto.TypePromptRequest, "run_second", proto.PromptRequestPayload{ + second := mustEnv(t, proto.TypePromptRequest, "run_second", prototest.WithModel(proto.PromptRequestPayload{ AgentKind: "fake_alpha", ConversationID: "conv-renew", AgentStateKey: stateKey, - }) + })) if err := h.router.Handle(context.Background(), second); err != nil { t.Fatalf("Handle second prompt: %v", err) } @@ -241,9 +239,9 @@ func TestHandlePromptRequestInvokesFactoryAndForwardsOutput(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_1", proto.PromptRequestPayload{ + env := mustEnv(t, proto.TypePromptRequest, "run_1", prototest.WithModel(proto.PromptRequestPayload{ AgentKind: "fake_alpha", Input: proto.TextInput("hi"), ConversationID: "c1", - }) + })) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("Handle prompt_request: %v", err) } @@ -269,7 +267,7 @@ func TestHandlePromptRequestRejectsDuplicateRunID(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_dup", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_dup", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("first Handle: %v", err) } @@ -293,7 +291,7 @@ func TestHandlePromptRequestUnsupportedKindEmitsErrorDone(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_x", proto.PromptRequestPayload{AgentKind: "fake_beta"}) + env := mustEnv(t, proto.TypePromptRequest, "run_x", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_beta"})) err := h.router.Handle(context.Background(), env) if !errors.Is(err, agent.ErrUnsupportedKind) { t.Errorf("Handle unsupported = %v, want ErrUnsupportedKind", err) @@ -309,7 +307,7 @@ func TestHandlePromptRequestMissingRunIDIsError(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err == nil { t.Fatal("expected error on missing run id") } @@ -319,7 +317,7 @@ func TestHandlePromptCancelInvokesSessionCancel(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_2", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_2", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("prompt_request: %v", err) } @@ -348,7 +346,7 @@ func TestPermissionRequestIsIndexedAndDecisionRoutes(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_p", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_p", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("prompt_request: %v", err) } @@ -393,7 +391,7 @@ func TestPermissionCancelDeindexes(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_p2", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_p2", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) _ = h.router.Handle(context.Background(), env) <-h.gotReq sess := <-h.gotSess @@ -429,7 +427,7 @@ func TestPromptForUserChoiceDecisionRoutesToSession(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_ask", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_ask", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("prompt_request: %v", err) } @@ -486,7 +484,7 @@ func TestPromptForUserChoiceDecisionClearsIndexOnAgentUnknown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_ask_u", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_ask_u", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("prompt_request: %v", err) } @@ -524,7 +522,7 @@ func TestPromptForUserChoiceDecisionKeepsIndexOnTransientAgentError(t *testing.T h := newHarness(t) defer h.router.Shutdown(context.Background()) - env := mustEnv(t, proto.TypePromptRequest, "run_ask_retry", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) + env := mustEnv(t, proto.TypePromptRequest, "run_ask_retry", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"})) if err := h.router.Handle(context.Background(), env); err != nil { t.Fatalf("prompt_request: %v", err) } @@ -607,7 +605,7 @@ func TestHandleAfterShutdownReturnsErrRouterClosed(t *testing.T) { if err := h.router.Shutdown(context.Background()); err != nil { t.Fatalf("Shutdown: %v", err) } - err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "r", proto.PromptRequestPayload{AgentKind: "fake_alpha"})) + err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "r", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"}))) if !errors.Is(err, dispatch.ErrRouterClosed) { t.Errorf("post-shutdown Handle = %v, want ErrRouterClosed", err) } @@ -616,7 +614,7 @@ func TestHandleAfterShutdownReturnsErrRouterClosed(t *testing.T) { func TestShutdownWaitsForPumpDrain(t *testing.T) { h := newHarness(t) - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "rs", proto.PromptRequestPayload{AgentKind: "fake_alpha"})); err != nil { + if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptRequest, "rs", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"}))); err != nil { t.Fatalf("prompt_request: %v", err) } <-h.gotReq diff --git a/apps/daemon/internal/dispatch/steering_lifetime_test.go b/apps/daemon/internal/dispatch/steering_lifetime_test.go index c51b87b87..b6879868c 100644 --- a/apps/daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/daemon/internal/dispatch/steering_lifetime_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "sync/atomic" @@ -28,7 +26,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { var session *fakeSession var calls atomic.Int32 release := make(chan struct{}) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &durableSteeringSession{steeringSession: &steeringSession{fakeSession: session}, phased: func(ctx context.Context, input proto.PromptSteerPayload, written func()) error { calls.Add(1) @@ -42,7 +40,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { }}, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "durable", proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "durable", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true}))); err != nil { t.Fatal(err) } input := proto.PromptSteerPayload{InputID: "extra", Input: proto.TextInput("additional"), DurableReceipt: true} @@ -82,7 +80,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) exited := make(chan struct{}) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &durableSteeringSession{steeringSession: &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}}, phased: func(ctx context.Context, _ proto.PromptSteerPayload, written func()) error { defer close(exited) if phase == "written" { @@ -93,7 +91,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { }}, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: true}))); err != nil { t.Fatal(err) } if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "one", Input: proto.TextInput("text"), DurableReceipt: true})); err != nil { @@ -131,7 +129,7 @@ func TestDurableSteeringRequiresOptInAndAdapter(t *testing.T) { t.Run(map[bool]string{false: "old-adapter", true: "retained-run"}[supported], func(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { s := &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { t.Error("unexpected legacy call"); return nil }} if !supported { return s, nil @@ -142,7 +140,7 @@ func TestDurableSteeringRequiresOptInAndAdapter(t *testing.T) { }}, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: !supported})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex", ReleaseOnCompletion: !supported}))); err != nil { t.Fatal(err) } if err := handleSteeringAndWait(t, h, mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "one", Input: proto.TextInput("text"), DurableReceipt: true})); err != nil { diff --git a/apps/daemon/internal/dispatch/steering_test.go b/apps/daemon/internal/dispatch/steering_test.go index c989afd7c..1ac57dfec 100644 --- a/apps/daemon/internal/dispatch/steering_test.go +++ b/apps/daemon/internal/dispatch/steering_test.go @@ -1,7 +1,5 @@ package dispatch_test -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -33,7 +31,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls, starts := 0, 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { starts++ return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, @@ -53,7 +51,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { }, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", proto.PromptRequestPayload{AgentKind: "codex"})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex"}))); err != nil { t.Fatal(err) } input := proto.PromptSteerPayload{InputID: "input-1", Input: proto.TextInput("additional text")} @@ -94,7 +92,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { @@ -107,7 +105,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { }, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", proto.PromptRequestPayload{AgentKind: "codex"})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex"}))); err != nil { t.Fatal(err) } input := proto.PromptSteerPayload{InputID: "input-1", Input: proto.TextInput("extra")} @@ -139,7 +137,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { if ack := lastSteeringAck(t, h.sender, "run-1", "input-1"); ack.ErrorCode != "run_inactive" { t.Fatalf("inactive: %+v", ack) } - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-2", proto.PromptRequestPayload{AgentKind: "fake_alpha"})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-2", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "fake_alpha"}))); err != nil { t.Fatal(err) } session := <-h.gotSess @@ -172,7 +170,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { defer h.router.Shutdown(context.Background()) entered, release := make(chan struct{}), make(chan struct{}) defer close(release) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { @@ -184,7 +182,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { }) ctx := context.Background() for _, run := range []struct{ id, engine string }{{"run-1", "codex"}, {"run-2", "fake_alpha"}} { - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, run.id, proto.PromptRequestPayload{AgentKind: run.engine})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, run.id, prototest.WithModel(proto.PromptRequestPayload{AgentKind: run.engine}))); err != nil { t.Fatal(err) } } @@ -235,7 +233,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration(), func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { @@ -245,7 +243,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { }, nil }) ctx := context.Background() - if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", proto.PromptRequestPayload{AgentKind: "codex"})); err != nil { + if err := h.router.Handle(ctx, mustEnv(t, proto.TypePromptRequest, "run-1", prototest.WithModel(proto.PromptRequestPayload{AgentKind: "codex"}))); err != nil { t.Fatal(err) } for i := range 257 { diff --git a/apps/daemon/internal/wireconformance/wire_test.go b/apps/daemon/internal/wireconformance/wire_test.go index 328737c23..4d8dacb00 100644 --- a/apps/daemon/internal/wireconformance/wire_test.go +++ b/apps/daemon/internal/wireconformance/wire_test.go @@ -22,7 +22,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) const wait = 3 * time.Second @@ -158,7 +157,7 @@ func connectRuntime(t *testing.T, peer *corePeer, setupErr error) *runtimeSide { rt := &runtimeSide{conn: conn, executor: &controlledExecutor{turn: make(chan *controlledTurn, 1)}, stopped: make(chan struct{})} kinds := agent.NewRegistry() kinds.RegisterKind(proto.SupportedAgentKind{Kind: prototest.HarnessKind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + prototest.ModelConfiguration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("prepared execution must not use prompt_request") }) kinds.RegisterExecutor(prototest.HarnessKind, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 8ecc4dbaf..e63a63ed1 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -1,8 +1,9 @@ // This synthetic harness exercises the production router without a native model. -// It is test-only, has no workspace/tools, and is never in the built-in catalog. +// It is test-only and has no workspace or tools. Core admits only built-in +// catalog Harnesses, so it registers as the mcode kind with that declaration. package main -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" +import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" import ( "context" @@ -161,12 +162,12 @@ func (s *session) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, er func run() error { registry := agent.NewRegistry() h := &harness{history: map[string]string{}} - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture_harness", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + registry.RegisterKind(proto.SupportedAgentKind{Kind: "mcode", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, - })}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + })}, mcode.Configuration(), func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("fixture execution requires an Executor") }) - registry.RegisterExecutor("fixture_harness", h.prepare) + registry.RegisterExecutor("mcode", h.prepare) sink := &sender{encoder: json.NewEncoder(os.Stdout)} router, err := dispatch.New(dispatch.Config{Registry: registry, Sender: sink, Log: slog.New(slog.NewTextHandler(io.Discard, nil))}) if err != nil { diff --git a/apps/web/src/features/system/DefaultModelsSection.tsx b/apps/web/src/features/system/DefaultModelsSection.tsx index faa302c8d..21b2f1969 100644 --- a/apps/web/src/features/system/DefaultModelsSection.tsx +++ b/apps/web/src/features/system/DefaultModelsSection.tsx @@ -22,9 +22,8 @@ function message(error: unknown): string { /** * System owns each harness's deployment default model configuration. - * New Core-hosted Sessions and Sessions without an environment inherit it - * when no explicit Session or Agent model configuration takes precedence. - * Self-hosted Sessions bring their own configuration. Whether a harness is enabled, and + * New Sessions inherit it when no explicit Session or Agent model + * configuration takes precedence. Whether a harness is enabled, and * which is the default, is Core's startup configuration and only shown here. * A write replaces the whole model configuration and needs the API key every time; the * key lives only in the open form's state, never in the query cache, storage diff --git a/apps/web/src/i18n/locales/en/keys.ts b/apps/web/src/i18n/locales/en/keys.ts index e61ba4802..b77f6eaf1 100644 --- a/apps/web/src/i18n/locales/en/keys.ts +++ b/apps/web/src/i18n/locales/en/keys.ts @@ -109,7 +109,7 @@ export const keys = { failed: "The API address couldn't be read.", localOnly: "For access from other machines, set OAC_PUBLIC_URL to an address they can reach.", noAddress: "Core has no public API address yet. Set OAC_PUBLIC_URL.", - model: "Replace {{model}} with a model name your model provider serves, or remove the model field to use this deployment's default model configuration. Running an Agent needs a model provider: pass one in each request, save one on the Agent, or rely on the deployment default. Self-hosted Sessions never use the deployment default.", + model: "Replace {{model}} with a model name your model provider serves, or remove the model field to use this deployment's default model configuration. Running an Agent needs a model provider: pass one in each request, save one on the Agent, or rely on the deployment default.", keyPlaceholder: "", projectKey: "Set OPENAI_API_KEY to an API key issued for this project. A key is shown only once, when it is issued; if it's lost, issue a new one.", projectHelp: "Samples for applications that call the Agents API with this project's API keys. The console never sends these requests.", diff --git a/apps/web/src/i18n/locales/en/system.ts b/apps/web/src/i18n/locales/en/system.ts index 01539ccb9..b04734eff 100644 --- a/apps/web/src/i18n/locales/en/system.ts +++ b/apps/web/src/i18n/locales/en/system.ts @@ -38,7 +38,7 @@ export const system = { }, models: { title: "Default model configuration", - help: "Default model settings for new Core-hosted Sessions and Sessions without an environment. Explicit application settings take priority. Self-hosted Sessions supply their own configuration; existing Sessions keep their saved settings.", + help: "Default model settings for new Sessions that bring no model provider of their own. Explicit application settings take priority; existing Sessions keep their saved settings.", loadFailed: "Default model configurations could not be loaded.", refreshFailed: "Refresh failed; showing the last loaded default model configurations.", none: "Core reports no harnesses.", diff --git a/apps/web/src/i18n/locales/zh-CN/keys.ts b/apps/web/src/i18n/locales/zh-CN/keys.ts index dd38c5907..4ce657e98 100644 --- a/apps/web/src/i18n/locales/zh-CN/keys.ts +++ b/apps/web/src/i18n/locales/zh-CN/keys.ts @@ -111,7 +111,7 @@ export const keys: TranslationShape = { failed: "无法读取 API 地址。", localOnly: "从其他机器调用前,请先把 OAC_PUBLIC_URL 设为它们能访问的地址。", noAddress: "Core 尚未配置公开 API 地址,请设置 OAC_PUBLIC_URL。", - model: "把 {{model}} 换成模型服务提供的模型名;也可以删掉 model 字段,使用本部署的默认模型配置。运行 Agent 需要模型服务:在每个请求里传入、保存在 Agent 上,或使用部署默认值。自托管 Session 不使用部署默认值。", + model: "把 {{model}} 换成模型服务提供的模型名;也可以删掉 model 字段,使用本部署的默认模型配置。运行 Agent 需要模型服务:在每个请求里传入、保存在 Agent 上,或使用部署默认值。", keyPlaceholder: "<项目 API key>", projectKey: "把 OPENAI_API_KEY 设为这个项目签发的 API key。key 只在签发时显示一次;丢失后请签发新 key。", projectHelp: "应用用这个项目的 API key 调用 Agents API 的示例。控制台不会发送这些请求。", diff --git a/apps/web/src/i18n/locales/zh-CN/system.ts b/apps/web/src/i18n/locales/zh-CN/system.ts index 072fd56c5..fb2eba401 100644 --- a/apps/web/src/i18n/locales/zh-CN/system.ts +++ b/apps/web/src/i18n/locales/zh-CN/system.ts @@ -40,7 +40,7 @@ export const system: TranslationShape = { }, models: { title: "默认模型配置", - help: "新建的 Core 托管会话和无环境会话使用这些默认设置,应用显式提供的设置优先。自托管会话需自行提供配置;已有会话保留原配置。", + help: "未自带模型服务的新建会话使用这些默认设置,应用显式提供的设置优先;已有会话保留原配置。", loadFailed: "无法加载默认模型配置。", refreshFailed: "刷新失败,显示的是上次加载的默认模型配置。", none: "Core 没有报告任何执行框架。", diff --git a/contracts/agents-api/environment-executor-credentials.md b/contracts/agents-api/environment-executor-credentials.md index fad59755c..175922386 100644 --- a/contracts/agents-api/environment-executor-credentials.md +++ b/contracts/agents-api/environment-executor-credentials.md @@ -95,4 +95,4 @@ A machine reconnects only with its bound `key_id`, rotated to a new secret that ## Model provider -A `self_hosted` Session carries its own model provider; deployment defaults never apply. [Model execution](./model-execution.md) owns the delivery rules. A saved Agent's provider key is delivered to the executor of every `self_hosted` Session created with that Agent in the Project, so anyone who can create `self_hosted` Sessions in the Project and run an executor can read it. +[Model execution](./model-execution.md) owns provider resolution and delivery. A saved Agent's provider key is delivered to the executor of every `self_hosted` Session created with that Agent in the Project, and a deployment default's key to the executor of every `self_hosted` Session that falls back to it, so anyone who can create such a Session and run an executor can read that key. diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index 23820a684..d951c4243 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -71,7 +71,6 @@ The application owns the machine. It creates the Session with a clean absolute ` - `remote_url` is the daemon WebSocket URL derived from Core's public URL, never from request headers or a daemon address. It names Core's private daemon transport. - Enrollment binds the exact Session, Environment, device and executor key. It creates no allocation and cannot move a Session to another device. - The Session's workspace must equal the `/workspace` alias or the exact canonical directory the Runtime is bound to. Naming a path grants no access to it. -- The Session carries its own model provider; deployment defaults never apply ([model execution](./model-execution.md#saved-defaults-and-precedence)). - Session reads, lists and events return the `self_hosted` output with the Environment ID, workspace and capability directories, never private configuration. `capability_directories` lists the caller's selections; the Runtime's installation locations stay private. - Compute, workspace and files stay the application's. Deleting the Session or revoking the credential denies further access but does not stop native processes; the machine owner stops and cleans up. - The workspace and native history must survive a daemon restart. Losing them never authorizes silent replacement or replay. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 03ebf4a52..cff644dab 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -168,7 +168,7 @@ Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySu The admission mapping is explicit. `Steering` controls non-durable `Steerer` input. `DurableInputReceipts` controls `DurableSteerer` input and also requires the Turn settlement contract; neither implies the other, and Core's public text profile requires both. `Permissions` qualifies permission and user-choice responses together and requires both native response paths. Workspace declarations describe the authorized resource owner, including the common Runtime workspace implementation. Runtime registration does not grant Core qualification; the service profile does. -The runnable test-only example [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) registers a text-only synthetic Harness. It shows a Session-owned Executor, fresh Turns, durable steering, cancellation and history binding, and is never shipped. +The runnable test-only example [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) registers a text-only synthetic Harness under the `mcode` kind, because Core admits only [catalog](./harness-catalog.md) Harnesses. It shows a Session-owned Executor, fresh Turns, durable steering, cancellation and history binding, and is never shipped. ## Add the engine to Core @@ -205,7 +205,7 @@ Run the `engine` and `execution` tests for omission, policy, combination and err [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) owns the shared configuration declaration and pure preparation contract. Each adapter supplies one `Configuration`, in `internal/harnessconfig/`, to Core's composition and to the Runtime's `RegisterKind`. The direct factory, preparation and Executor paths all validate through that declaration before native side effects, and Registry wrappers keep the declaration with the factory. The wire object is `proto.HarnessConfig`. [Model execution](./model-execution.md#native-model-parameters) lists each Harness's accepted fields. -A supplied `model` must be a nonempty string, and an explicit `model_provider` requires it. The native-owned connection path may omit both; explicit null is invalid. An explicitly empty declaration accepts no provider or nonempty native parameters and advertises no provider support. Unknown protocol formats and duplicate protocol declarations fail at registration. +Every request names a nonempty `model` and a `model_provider`; preparation rejects a request without either, including an explicit null or empty value, before any native side effect. An empty declaration therefore accepts no request. Unknown protocol formats and duplicate protocol declarations fail at registration. The declaration's ordered `protocols` list is the only source of accepted protocols and the default (the first entry); it also feeds Core's configuration-support descriptor, and Core and Runtime reject unsupported combinations through it. Adapters connect through native configuration and the [credential gateway](./model-execution.md#credential-gateway); they never introduce their own model API proxy or protocol converter, a second model capability registry, or capabilities inferred from model names. Claude's private bridge receives compiled native options and performs structural checks only, not a second copy of the declaration's rules. diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 71a9deb39..ca27f0193 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -24,7 +24,7 @@ The Session's `environment` and Environment Templates select preparation, not Ha A saved Agent is editable configuration, not a bound runtime. Create or update it with `model`, optional `x_agents_core.harness` and an optional complete `x_agents_core.model_provider`. Responses return the safe provider fields and the output-only `api_key_configured` flag, never `api_key`, ciphertext or a reusable credential reference. Agent JSON stores only the safe view; the secret bundle has its own encrypted row, bound to the Project and Agent with a distinct encryption purpose, and is written in the same transaction. A model-only edit needs no key. -Session creation resolves each explicit model or Harness override before saved defaults; without a selected Harness, the deployment default applies. Saved Agents require a model. An inline `openai_hosted` or `none` Session may omit its model to use the deployment model of the resolved Harness; `self_hosted` never uses deployment model settings. Core never infers a model from its name. +Session creation resolves each explicit model or Harness override before saved defaults; without a selected Harness, the deployment default applies. Saved Agents require a model. An inline Session may omit its model to use the deployment model of the resolved Harness. Core never infers a model from its name. Provider precedence is: a complete Session bundle, then a complete saved bundle, then the deployment default for the resolved Harness. Core never merges a replacement endpoint with an inherited key; a model-only override reuses the whole inherited bundle. Each Harness connects only through its native protocols: @@ -36,15 +36,7 @@ Provider precedence is: a complete Session bundle, then a complete saved bundle, MiniMax Code requires positive context and output limits. Core validates the resolved combination before writing the Session. Core and Runtime read the same ordered `protocols` declaration in `internal/harnessconfig`. Nothing converts between protocols, including inside a Harness. Unsupported saved configurations and Session snapshots fail when used; they are never rewritten, aliased or migrated. -Which sources apply depends on who owns the compute that receives the key: - -| Environment | Session or saved-Agent bundle | Deployment default | No bundle resolved | -| --- | --- | --- | --- | -| `openai_hosted` | Accepted | Applied | 400 `model_provider_required` | -| `self_hosted` | Accepted | Never applied | 400 `model_provider_required` | -| `none` | Rejected with 400 | Applied when configured | Accepted; the device's own environment supplies the model | - -The deployment default holds the operator's key, so it applies only to operator-run Environments: Core-managed sandboxes and operator-registered `none` devices. A `self_hosted` executor belongs to the application, which supplies its own bundle. Hosted and self-hosted Runtimes carry no model configuration of their own, so a Session there without a bundle is rejected before any write, with param `x_agents_core.model_provider` and a message saying what to configure. +Every source applies to every Environment type, because the key reaches only the [credential gateway](#credential-gateway) on the agent host, never the Harness or the sandbox. For `self_hosted`, that agent host is the application's executor, so a deployment default there hands the operator's key to that executor's gateway. Every Session must resolve a bundle: without one, creation is rejected before any write with 400 `model_provider_required`, param `x_agents_core.model_provider` and a message saying what to configure. A saved Agent may omit its bundle and leave it to the Session or the deployment default. | Operation | Omitted | Explicit null | | --- | --- | --- | @@ -59,7 +51,7 @@ An empty Session execution extension is invalid. An explicit null provider reque Core reads the Agent configuration and encrypted bundle from one database snapshot; an explicit complete Session override needs no decryption of the saved bundle. The Session's own encrypted snapshot is written atomically with the Session and its Environment. Existing Sessions never consult the Agent again: edits, key replacement, deletion, suspension and restarts cannot change their model, Harness or provider. A missing or wrong encryption key fails closed; keep the same [credential key](../../docs/configuration.md#installation-directory) across restarts. There is no Turn-level override. -New hosted requests, and requests that omit the inline model, record caller intent before resolving mutable defaults. Other inline requests, such as `none`, keep the resolved-request retry rule; that hash leaves out the deployment default, so changing the default does not change their retry identity. A matching creation retry recovers the committed Session before resolving the Agent or provider again and enqueues no further input. Streaming is outside the retry identity. The [TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) shows saved Agents and deployment defaults. +Every creation request records caller intent before resolving saved Agents, templates, credentials or deployment defaults, so changing or removing them does not change its retry identity. A matching creation retry recovers the committed Session before resolving the Agent or provider again and enqueues no further input. Streaming is outside the retry identity. The [TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) shows saved Agents and deployment defaults. ## Session override @@ -80,7 +72,7 @@ New hosted requests, and requests that omit the inline model, record caller inte ``` - `protocol` names the upstream API (`anthropic`, `responses` or `chat_completions`), not an engine. The selected Harness must support it natively. -- `base_url` uses HTTPS with a valid host, without credentials, query or fragment. For `anthropic` it excludes the version path, because the Harness appends `/v1/messages`, so a value ending in `/v1` or `/v1/` is rejected. +- `base_url` uses HTTPS with a valid host, without credentials, query or fragment. A loopback host means the agent host's network namespace, for every Environment type. For `anthropic` it excludes the version path, because the Harness appends `/v1/messages`, so a value ending in `/v1` or `/v1/` is rejected. - `api_key` is nonempty, at most 16 KiB and contains no NUL, CR or LF. - `context_window` and `max_output_tokens` are optional nonnegative integers, with output no larger than context; both must be positive for MiniMax Code. Use the real model's limits. - `agent.model` is the exact provider model ID; a supplied value always replaces the deployment model. @@ -88,7 +80,7 @@ New hosted requests, and requests that omit the inline model, record caller inte Unsupported protocol, Harness or Environment combinations are rejected before the Session is created. Provider availability is checked during execution, not by a probe. -The resolved provider configuration is frozen and encrypted in the Session creation transaction, with its own encryption purpose and Project and Session binding. Creation retries include it in their request hash, so a changed key or endpoint under the same Idempotency-Key conflicts; a key enters any stored hash only as a fingerprint keyed by the deployment credential key. No public Session, Agent, Environment, event or ordinary configuration contains the key. The top-level extension is write-only and cannot be updated. +The resolved provider configuration is frozen and encrypted in the Session creation transaction, with its own encryption purpose and Project and Session binding. Creation retries include a Session's own bundle in their request hash, so a changed key or endpoint under the same Idempotency-Key conflicts; a key enters any stored hash only as a fingerprint keyed by the deployment credential key. No public Session, Agent, Environment, event or ordinary configuration contains the key. The top-level extension is write-only and cannot be updated. At dispatch, Core sends the snapshot as one confidential provider bundle over the daemon connection bound to the Session, and the adapter points the Harness at the [credential gateway](#credential-gateway) through native configuration. Core never falls back to other credentials when a snapshot is missing or cannot be decrypted. For `self_hosted`, the receiving daemon is the executor enrolled for the Session's own Environment with a current executor credential of the Session creator's principal; rotation or revocation closes the socket before further dispatch. The gateway holds the key in memory for the Session, and the key never enters the Harness's environment, configuration or home, or the sandbox. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index 64c942b2e..12e691d56 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -2987,18 +2987,17 @@ paths: consumes: - application/json description: 'The optional Core model_provider bundle resolves from the Session - override, saved Agent defaults, then, for openai_hosted and none, the deployment - default of the resolved harness; self_hosted never uses the deployment default - and none accepts only it. openai_hosted and self_hosted Sessions that resolve - no bundle return 400 model_provider_required with param x_agents_core.model_provider - before any write. Core encrypts and freezes the resolved bundle; later Agent - or deployment default edits and same-key retries cannot change it. Keys are - never returned. Supports inline configuration or a tenant-owned saved agent_id - with per-Session field replacements. Execution supports model/instructions, - text verbosity, non-deferred function tools, adapter-qualified multi_agent - with persisted Subagent reads, implicit reasoning, service tier auto and environment - type none, subject to the configured engine. Codex additionally supports HTTP - MCP with service origin (omitted or null on HTTP transport is saved as service), + override, saved Agent defaults, then the deployment default of the resolved + harness, for every environment type. A Session that resolves no bundle returns + 400 model_provider_required with param x_agents_core.model_provider before + any write. Core encrypts and freezes the resolved bundle; later Agent or deployment + default edits and same-key retries cannot change it. Keys are never returned. + Supports inline configuration or a tenant-owned saved agent_id with per-Session + field replacements. Execution supports model/instructions, text verbosity, + non-deferred function tools, adapter-qualified multi_agent with persisted + Subagent reads, implicit reasoning, service tier auto and environment type + none, subject to the configured engine. Codex additionally supports HTTP MCP + with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects @@ -3056,14 +3055,11 @@ paths: actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key - rotation. Saved-Agent retries and inline requests using Vault attachments - or credential references retain caller intent independently of later resource - changes; new hosted inline requests also freeze caller intent before deployment - defaults resolve; unrelated non-hosted inline retries preserve resolved/default - equivalences, and their resolved hash leaves out any deployment default. Provider + rotation. Every creation records caller intent before saved Agents, templates, + Vault credentials or deployment defaults resolve, so a same-intent retry returns + the committed Session independently of later changes to those resources. Provider keys enter retry hashes only as fingerprints keyed by the credential key. - Unknown historical creators reject retries; known creators without recorded - intent retain resolved-snapshot retry rules. These conflict policies are local + Unknown historical creators reject retries. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none, Core-managed @@ -3072,48 +3068,46 @@ paths: Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text - initial input remains unsupported. Basic Codex and Claude SDK openai_hosted - creation requires an explicitly configured managed provider. The Claude workspace - profile supports non-deferred function tools with text or successful inline - PNG/JPEG results alongside native workspace tools; explicit environment-origin - HTTP MCP uses the common Runtime path. MiniMax accepts public environment-origin - HTTP MCP only with null or omitted allowed_tools and required=false; even - an empty non-null allowlist rejects. Idle Sessions provision automatically; - initial provisioning has no caller connection action. Network defaults to - enabled; disabled and restricted policies reject before compute allocation - because the current Runtime cannot enforce them. The x_agents_core.environment - extension accepts common preparation fields for either hosted or self-hosted - placement: environment_template_id, files, env, packages, setup_commands, - skills, plugins and capability_directories. Duplicate fields in environment - and the extension reject. Confidential env, npm/Python packages and ordered - setup commands use the same Environment-owned initialization lifecycle; compute - allocation does not own preparation. Unknown side effects are not replayed - after disconnect or restart. System dependencies must be preinstalled in the - sandbox image or template, or on the host machine; packages.system is rejected. - Initial inline and tenant-owned file_id files freeze encrypted bytes before - provisioning, then install through the common Core lifecycle before native - execution or live Files access. With a template reference, omitted/null files, - env, packages and setup_commands inherit. Non-null files and command lists - replace; env overlays by key; each package manager inherits on omission/null - and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned - environment_template_id references inherit omitted/null network and allow - only narrowing overrides. Inline hosted network:null retains the enabled default; - updating a Template with network:null resets its saved policy to enabled. - Core freezes effective configuration; template updates/deletion do not alter - Session snapshots or same-intent creation retries. Inline or tenant-owned - skill_reference Skills share initialization. Templates preserve default/latest/explicit - selectors; Session creation freezes concrete metadata and encrypted content - atomically. Skill, Plugin and capability-directory list omission/null inherit; - a non-null list replaces, including empty-list clearing. Omitted/null Skill - version selectors resolve the default version. Source deletion/default updates - cannot change committed Session Skill contents. Deferred function discovery - uses type-only tool_search and per-function defer_loading in the qualified - single-agent Claude function profile on none or a managed/user-owned workspace, - including qualified inline image messages and text results. Explicit web_search - mode disabled and programmatic_tool_calling enabled false use frozen common - Runtime controls. Enabled forms, including those saved on an Agent, remain - unqualified and reject before any write unless the Session replaces tools. - Omitted programmatic configuration preserves native behavior, a documented + initial input remains unsupported. The Claude workspace profile supports non-deferred + function tools with text or successful inline PNG/JPEG results alongside native + workspace tools; explicit environment-origin HTTP MCP uses the common Runtime + path. MiniMax accepts public environment-origin HTTP MCP only with null or + omitted allowed_tools and required=false; even an empty non-null allowlist + rejects. Idle Sessions provision automatically; initial provisioning has no + caller connection action. Network defaults to enabled; disabled and restricted + policies reject before compute allocation because the current Runtime cannot + enforce them. The x_agents_core.environment extension accepts common preparation + fields for either hosted or self-hosted placement: environment_template_id, + files, env, packages, setup_commands, skills, plugins and capability_directories. + Duplicate fields in environment and the extension reject. Confidential env, + npm/Python packages and ordered setup commands use the same Environment-owned + initialization lifecycle; compute allocation does not own preparation. Unknown + side effects are not replayed after disconnect or restart. System dependencies + must be preinstalled in the sandbox image or template, or on the host machine; + packages.system is rejected. Initial inline and tenant-owned file_id files + freeze encrypted bytes before provisioning, then install through the common + Core lifecycle before native execution or live Files access. With a template + reference, omitted/null files, env, packages and setup_commands inherit. Non-null + files and command lists replace; env overlays by key; each package manager + inherits on omission/null and otherwise replaces its list. Empty lists clear + their selected field. Tenant-owned environment_template_id references inherit + omitted/null network and allow only narrowing overrides. Inline hosted network:null + retains the enabled default; updating a Template with network:null resets + its saved policy to enabled. Core freezes effective configuration; template + updates/deletion do not alter Session snapshots or same-intent creation retries. + Inline or tenant-owned skill_reference Skills share initialization. Templates + preserve default/latest/explicit selectors; Session creation freezes concrete + metadata and encrypted content atomically. Skill, Plugin and capability-directory + list omission/null inherit; a non-null list replaces, including empty-list + clearing. Omitted/null Skill version selectors resolve the default version. + Source deletion/default updates cannot change committed Session Skill contents. + Deferred function discovery uses type-only tool_search and per-function defer_loading + in the qualified single-agent Claude function profile on none or a managed/user-owned + workspace, including qualified inline image messages and text results. Explicit + web_search mode disabled and programmatic_tool_calling enabled false use frozen + common Runtime controls. Enabled forms, including those saved on an Agent, + remain unqualified and reject before any write unless the Session replaces + tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage.' parameters: diff --git a/contracts/agents-api/v1/model_provider_admission.go b/contracts/agents-api/v1/model_provider_admission.go index fa91a1cc2..d5ac0d495 100644 --- a/contracts/agents-api/v1/model_provider_admission.go +++ b/contracts/agents-api/v1/model_provider_admission.go @@ -22,34 +22,6 @@ const ( ModelProviderSourceDeployment = "deployment" ) -// ModelProviderAllowed reports whether a provider bundle from source may be -// frozen into a Session placed in environment. Caller bundles (Session or saved -// Agent) run on Core-managed or caller-owned compute. The deployment default -// holds the operator's key, so it stays on operator compute: openai_hosted and -// operator-registered none devices. An unknown source is allowed only where -// every source is. -func ModelProviderAllowed(environment, source string) bool { - caller := source == ModelProviderSourceSession || source == ModelProviderSourceAgent - deployment := source == ModelProviderSourceDeployment - switch environment { - case "openai_hosted": - return true - case "self_hosted": - return caller - case "none": - return deployment - default: - return false - } -} - -// ModelProviderRequired reports whether a Session in environment cannot run -// without a frozen provider bundle. Hosted and self-hosted Runtimes carry no -// model configuration of their own; a none device may use its own environment. -func ModelProviderRequired(environment string) bool { - return environment == "openai_hosted" || environment == "self_hosted" -} - func validModelProviderBaseURL(base string) bool { u, err := url.Parse(base) return err == nil && u.Scheme == providerScheme && validModelProviderHost(u) && u.User == nil && u.RawQuery == "" && u.Fragment == "" && !strings.ContainsAny(base, "\x00\r\n") diff --git a/contracts/agents-api/wire-semantics.md b/contracts/agents-api/wire-semantics.md index 75d9136f2..44efd725f 100644 --- a/contracts/agents-api/wire-semantics.md +++ b/contracts/agents-api/wire-semantics.md @@ -223,7 +223,7 @@ Send an `Idempotency-Key` of 1–128 bytes that is not only whitespace; a longer | Same key, different request | 409 `idempotency_conflict` | | Same key after the Session was deleted | 409 `idempotency_conflict` | -Keys are scoped to the Project; any key of the Project, including one issued after a rotation, can retry. A request that has an inline Agent without `model` or names a saved Agent, a template, initial files or preparation, `vault_ids` or credential references, `x_agents_core`, or an `openai_hosted` environment is compared as sent, before any of those sources is read: a matching retry returns the original Session even after the Agent, template, Credential or deployment default changes or is deleted. Other requests are compared by their resolved configuration. Model provider keys enter the comparison only as fingerprints. +Keys are scoped to the Project; any key of the Project, including one issued after a rotation, can retry. Every creation request is compared as sent, before any saved Agent, template, Credential or deployment default is read: a matching retry returns the original Session even after those change or are deleted. Streaming and an empty or null `metadata` are outside the comparison; a spelled-out Agent default such as `text.verbosity` is not, so such a retry conflicts. Model provider keys enter the comparison only as fingerprints. ### Update and list diff --git a/contracts/agents-api/zh/environment-executor-credentials.md b/contracts/agents-api/zh/environment-executor-credentials.md index d78e8c421..41f91af76 100644 --- a/contracts/agents-api/zh/environment-executor-credentials.md +++ b/contracts/agents-api/zh/environment-executor-credentials.md @@ -1,7 +1,7 @@ --- title: "Environment 执行器凭证" source: contracts/agents-api/environment-executor-credentials.md -source_hash: 6c1db305481f9ab2a51bdd0c88243feaab48348b71f5f3ebbc8f00b17744f412 +source_hash: bf7777947375036cab900233d2002bfa1f29090b71df918045c4abbcbeacd245 --- 执行器凭证允许 `oac-daemon` 为一个 `self_hosted` Environment 注册并连接。它只授权该 Environment 的私有 daemon 传输(`/api/v1/agent-daemon/*`),不授权 `/v1`、`/core/v1`、sandbox node 注册或 Project 资源。Project 的 principal 是其执行 principal。Core 只保存密钥摘要。 @@ -97,4 +97,4 @@ Core 永久拒绝 daemon 时(注册 401/409、永久 WebSocket 拒绝,或 da ## 模型服务 {#model-provider} -`self_hosted` Session 携带自己的模型服务,部署默认值不适用。[模型执行](model-execution.md)负责交付规则。保存的 Agent 的服务 key 会交给 Project 中用该 Agent 创建的每个 `self_hosted` Session 的执行器,因此任何能在该 Project 创建 `self_hosted` Session 并运行执行器的人都能读取它。 +[模型执行](model-execution.md)负责模型服务的解析和交付。保存的 Agent 的服务 key 会交给 Project 中用该 Agent 创建的每个 `self_hosted` Session 的执行器,部署默认值的 key 会交给回退到它的每个 `self_hosted` Session 的执行器,因此任何能创建这类 Session 并运行执行器的人都能读取该 key。 diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index 676cb2675..82df7fb3c 100644 --- a/contracts/agents-api/zh/environments.md +++ b/contracts/agents-api/zh/environments.md @@ -1,7 +1,7 @@ --- title: "环境与模板" source: contracts/agents-api/environments.md -source_hash: a70ea3e004d5b7e2fc48c26296c792a5d888b754a012a7f0c624c3ff597369e7 +source_hash: 86541a1daff0778038c33edd2eb76583faced17e310c94ff587405dbf2a3af10 --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment Template 是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -73,7 +73,6 @@ Core 在 Session 创建事务中创建 Environment 记录;Session upsert 会 - `remote_url` 是根据 Core 的公共 URL 推导出的 daemon WebSocket URL,绝不根据请求头或 daemon 地址生成。它指定 Core 的私有 daemon 传输通道。 - 注册会将精确的 Session、Environment、设备和 executor key 绑定在一起。它不会创建任何分配,也无法将 Session 迁移到另一台设备。 - Session 的工作区必须等于 `/workspace` 别名,或等于 Runtime 绑定到的精确规范目录。指定某个路径并不会授予对它的访问权限。 -- Session 携带自己的模型提供方;部署默认值绝不适用([model execution](model-execution.md#saved-defaults-and-precedence))。 - Session 读取、列表和事件会返回带有 Environment ID、工作区及能力目录的 `self_hosted` 输出,但绝不返回私有配置。`capability_directories` 列出调用方选择的内容;Runtime 的安装位置保持私有。 - 计算资源、工作区和文件仍归应用程序所有。删除 Session 或撤销凭据会拒绝后续访问,但不会停止原生进程;机器所有者负责停止和清理。 - 工作区和原生历史必须能在 daemon 重启后继续存在。丢失它们绝不授权进行静默替换或重播。 diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 57c28f9b5..5f978d9b3 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "将原生 Harness 添加到 OpenAgentCore" source: contracts/agents-api/harness-onboarding.md -source_hash: 3d74f674c1cc68fbf40c2fe15b81c30f7fe69b6731da779fb75003b5f438e5f4 +source_hash: 12984c6ca7c8fb3166ce33bfe83bc86894124e5ce2427c4d1f8fe0c09666159e --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 @@ -170,7 +170,7 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 准入映射是显式的。`Steering` 控制非持久化 `Steerer` 输入。`DurableInputReceipts` 控制 `DurableSteerer` 输入,并且还要求 Turn 结算契约;二者互不隐含,而且 Core 的公共文本 profile 要求同时具备二者。`Permissions` 一起认定权限响应和用户选择响应的资格,并要求两条原生响应路径均存在。工作区声明描述授权资源所有者,包括通用 Runtime 工作区实现。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 -可运行的仅测试示例 [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) 会注册一个仅支持文本的合成 Harness。它展示 Session 所有的 Executor、全新的 Turn、持久化引导、取消和历史绑定,并且绝不会发布。 +可运行的仅测试示例 [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) 会以 `mcode` 类型注册一个仅支持文本的合成 Harness,因为 Core 只接纳[目录](harness-catalog.md)中的 Harness。它展示 Session 所有的 Executor、全新的 Turn、持久化引导、取消和历史绑定,并且绝不会发布。 ## 将引擎添加到 Core {#add-the-engine-to-core} @@ -207,7 +207,7 @@ profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 负责共享配置声明和纯准备契约。每个适配器在 `internal/harnessconfig/` 中提供一个 `Configuration`,供 Core 组合和 Runtime 的 `RegisterKind` 使用。直接调用工厂、准备路径和 Executor 路径都会在产生原生副作用之前通过该声明进行验证,而 Registry 包装器会将声明与工厂保留在一起。线协议对象是 `proto.HarnessConfig`。[Model execution](model-execution.md#native-model-parameters) 列出了每个 Harness 接受的字段。 -提供的 `model` 必须是非空字符串,并且显式指定 `model_provider` 时必须提供它。原生所有权连接路径可以省略二者;显式 null 无效。显式为空的声明不接受任何 Provider 或非空原生参数,也不宣称支持 Provider。未知协议格式和重复协议声明会导致注册失败。 +每个请求都指定非空的 `model` 和一个 `model_provider`;缺少任一项的请求,包括显式 null 或空值,都会在产生任何原生副作用之前被准备阶段拒绝。因此空声明不接受任何请求。未知协议格式和重复协议声明会导致注册失败。 声明中的有序 `protocols` 列表是接受协议及默认协议(第一个条目)的唯一来源;它还为 Core 的配置支持描述符提供数据,Core 和 Runtime 通过它拒绝不受支持的组合。适配器通过原生配置和[凭据网关](./model-execution.md#credential-gateway)连接;它们绝不引入自己的模型 API 代理或协议转换器、第二套模型能力 registry,也不会从模型名称推断能力。Claude 的私有 bridge 接收编译后的原生选项,并且只执行结构检查,而不是声明规则的第二份副本。 diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index d3ea0c0e9..3961e53e4 100644 --- a/contracts/agents-api/zh/model-execution.md +++ b/contracts/agents-api/zh/model-execution.md @@ -1,7 +1,7 @@ --- title: "模型执行" source: contracts/agents-api/model-execution.md -source_hash: 6b6a84f1355ef3a45e8ed8ed2f0b1b7ad9de1938abb49109b51d592d6ddf5ace +source_hash: 36c013fb36432a792ee693a7cee46d739710d6e3faba7f8448d418226dae38b4 --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接并声明[凭据网关](#credential-gateway)转发的内容,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -26,7 +26,7 @@ Session 的 `environment` 和 Environment Templates 用于选择准备流程, 已保存 Agent 是可编辑配置,而不是绑定的运行时。创建或更新时,应提供 `model`、可选的 `x_agents_core.harness` 以及可选但完整的 `x_agents_core.model_provider`。响应仅返回安全的提供商字段和只读输出标志 `api_key_configured`,绝不返回 `api_key`、密文或可复用的凭据引用。Agent JSON 仅存储安全视图;密钥配置包拥有自己加密后的数据库行,使用独立的加密用途绑定到 Project 和 Agent,并与 Agent 在同一事务中写入。仅编辑模型时不需要密钥。 -创建 Session 时,Core 会先解析每个显式的模型或 Harness 覆盖值,再应用已保存默认值;未选择 Harness 时,应用部署默认值。已保存 Agent 必须指定模型。内联 `openai_hosted` 或 `none` Session 可以省略模型,以使用解析后 Harness 的部署模型;`self_hosted` 绝不会使用部署模型设置。Core 绝不会根据模型名称推断模型。 +创建 Session 时,Core 会先解析每个显式的模型或 Harness 覆盖值,再应用已保存默认值;未选择 Harness 时,应用部署默认值。已保存 Agent 必须指定模型。内联 Session 可以省略模型,以使用解析后 Harness 的部署模型。Core 绝不会根据模型名称推断模型。 提供商配置的优先级依次为:完整的 Session 配置包、完整的已保存配置包,以及解析后 Harness 的部署默认值。Core 绝不会将替换后的端点与继承的密钥合并;仅覆盖模型时会复用整个继承的配置包。每个 Harness 仅通过其原生协议连接: @@ -38,15 +38,7 @@ Session 的 `environment` 和 Environment Templates 用于选择准备流程, MiniMax Code 要求上下文限制和输出限制均为正数。Core 会在写入 Session 前验证解析后的组合。Core 和 Runtime 读取 `internal/harnessconfig` 中相同的有序 `protocols` 声明。任何地方都不在协议之间转换,Harness 内部也不例外。不受支持的已保存配置和 Session 快照一旦使用便会失败;它们绝不会在何处被重写、创建别名或迁移。 -适用来源取决于接收密钥的计算资源由谁拥有: - -| Environment | Session 或已保存 Agent 配置包 | 部署默认值 | 未解析到配置包 | -| --- | --- | --- | --- | -| `openai_hosted` | 接受 | 应用 | 400 `model_provider_required` | -| `self_hosted` | 接受 | 从不应用 | 400 `model_provider_required` | -| `none` | 以 400 拒绝 | 已配置时应用 | 接受;模型由设备自身的环境提供 | - -部署默认值保存运营方的密钥,因此仅适用于运营方运行的 Environment:Core 管理的沙箱和运营方注册的 `none` 设备。`self_hosted` 执行器属于应用程序,由应用程序提供自己的配置包。托管 Runtime 和自托管 Runtime 均不携带自己的模型配置,因此其中的 Session 如果没有配置包,就会在发生任何写入之前被拒绝;错误参数为 `x_agents_core.model_provider`,并会返回说明应配置内容的消息。 +每种 Environment 类型都接受所有来源,因为密钥只会到达 agent host 上的[凭据网关](#credential-gateway),绝不会进入 Harness 或沙箱。对于 `self_hosted`,agent host 就是应用程序的执行器,因此在那里使用部署默认值会把运营方的密钥交给该执行器的网关。每个 Session 都必须解析到一个配置包:否则创建会在发生任何写入之前以 400 `model_provider_required` 被拒绝,错误参数为 `x_agents_core.model_provider`,并会返回说明应配置内容的消息。已保存 Agent 可以省略配置包,交由 Session 或部署默认值提供。 | 操作 | 省略 | 显式 null | | --- | --- | --- | @@ -61,7 +53,7 @@ MiniMax Code 要求上下文限制和输出限制均为正数。Core 会在写 Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式提供完整 Session 覆盖值时,无需解密已保存的配置包。Session 自身的加密快照会与 Session 及其 Environment 原子写入。现有 Session 绝不会再次查询 Agent:Agent 编辑、密钥替换、删除、暂停和重启均无法改变其模型、Harness 或提供商。加密密钥缺失或错误时会安全失败;重启前后应保持相同的 [credential key](../../../docs/zh/configuration.md#installation-directory)。不存在 Turn 级覆盖。 -新的托管请求以及省略内联模型的请求,会在解析可变默认值之前记录调用方意图。其他内联请求,例如 `none`,继续遵循已解析请求的重试规则;该哈希不包含部署默认值,因此更改默认值不会改变其重试标识。匹配的创建重试会在再次解析 Agent 或提供商之前恢复已提交的 Session,并且不会进一步加入输入。流式传输不参与重试标识的计算。[TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) 展示了已保存 Agent 和部署默认值。 +每个创建请求都会在解析已保存 Agent、模板、凭据或部署默认值之前记录调用方意图,因此更改或删除它们不会改变其重试标识。匹配的创建重试会在再次解析 Agent 或提供商之前恢复已提交的 Session,并且不会进一步加入输入。流式传输不参与重试标识的计算。[TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) 展示了已保存 Agent 和部署默认值。 ## Session 覆盖 {#session-override} @@ -82,7 +74,7 @@ Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式 ``` - `protocol` 指定上游 API(`anthropic`、`responses` 或 `chat_completions`),而不是引擎。所选 Harness 必须原生支持它。 -- `base_url` 使用 HTTPS 和有效主机名,且不得包含凭据、查询参数或片段。`anthropic` 的 `base_url` 不包含版本路径,因为 Harness 会自行追加 `/v1/messages`,所以以 `/v1` 或 `/v1/` 结尾的值会被拒绝。 +- `base_url` 使用 HTTPS 和有效主机名,且不得包含凭据、查询参数或片段。回环主机指 agent host 的网络命名空间,对每种 Environment 类型都是如此。`anthropic` 的 `base_url` 不包含版本路径,因为 Harness 会自行追加 `/v1/messages`,所以以 `/v1` 或 `/v1/` 结尾的值会被拒绝。 - `api_key` 不得为空,最长为 16 KiB,并且不得包含 NUL、CR 或 LF。 - `context_window` 和 `max_output_tokens` 是可选的非负整数,输出限制不得大于上下文限制;对于 MiniMax Code,两者都必须为正数。请使用真实模型的限制。 - `agent.model` 是准确的提供商模型 ID;只要提供该值,就始终会替换部署模型。 @@ -90,7 +82,7 @@ Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式 不受支持的协议、Harness 或 Environment 组合会在创建 Session 前被拒绝。提供商可用性在执行期间检查,而不是通过探测检查。 -解析后的提供商配置会在 Session 创建事务中被冻结并加密,使用自己的加密用途,并绑定到 Project 和 Session。创建重试会将其纳入请求哈希,因此使用相同 Idempotency-Key 时,更改密钥或端点会产生冲突;密钥进入任何存储哈希时,只会表现为由部署凭据密钥加键控的指纹。任何公开的 Session、Agent、Environment、事件或常规配置均不包含该密钥。顶层扩展仅可写入,无法更新。 +解析后的提供商配置会在 Session 创建事务中被冻结并加密,使用自己的加密用途,并绑定到 Project 和 Session。创建重试会将 Session 自身的配置包纳入请求哈希,因此使用相同 Idempotency-Key 时,更改密钥或端点会产生冲突;密钥进入任何存储哈希时,只会表现为由部署凭据密钥加键控的指纹。任何公开的 Session、Agent、Environment、事件或常规配置均不包含该密钥。顶层扩展仅可写入,无法更新。 在分派时,Core 会通过绑定到 Session 的 daemon 连接,将快照作为一个机密提供商配置包发送出去;适配器通过原生配置让 Harness 指向[凭据网关](#credential-gateway)。快照缺失或无法解密时,Core 绝不会回退到其他凭据。对于 `self_hosted`,接收方 daemon 是为该 Session 自身 Environment 注册的执行器,并持有 Session 创建者主体的当前执行器凭据;凭据轮换或吊销会在继续分派前关闭套接字。网关在 Session 期间将密钥保存在内存中,密钥从不进入 Harness 的环境、配置或 home,也不进入沙箱。 diff --git a/contracts/agents-api/zh/wire-semantics.md b/contracts/agents-api/zh/wire-semantics.md index 840c3a8f7..725bbde21 100644 --- a/contracts/agents-api/zh/wire-semantics.md +++ b/contracts/agents-api/zh/wire-semantics.md @@ -1,7 +1,7 @@ --- title: "Core 协议行为" source: contracts/agents-api/wire-semantics.md -source_hash: 5524db9b90aaf51026746316d6305da396033f50e88ed50792a8f52cf5aac9db +source_hash: 08b1d6adaabbc67bb28ff412dbea21095f4f56954fb62d4d244bd175319ca862 --- 已锁定版本的 OpenAI Python SDK([upstream.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))定义了 `/v1` 路由、字段和类型。本页面说明这些类型未作规定之处 Core 的行为,例如状态码、错误字段、默认值和列表边界,以及 Core 与官方服务存在差异的地方。[coverage ledger](index.md) 列出了这些差异和尚存缺口;[Sessions, events and history](sessions-events.md)、[message content](message-content.md)、[Vaults](vaults.md)、[source Files and Skills](source-files.md) 和 [Environment files and Artifacts](environment-files.md) 分别负责各自资源的规则。 @@ -225,7 +225,7 @@ Session 创建会将 Agent 的有效配置复制到不可变快照中。使用 ` | 键相同但请求不同 | 409 `idempotency_conflict` | | 删除 Session 后使用相同键 | 409 `idempotency_conflict` | -键的作用域限定为 Project;该 Project 的任何键,包括轮换后签发的键,都可以用于重试。如果请求包含缺少 `model` 的内联 Agent,或者指定了已保存的 Agent、Template、初始文件或准备配置、`vault_ids` 或凭据引用、`x_agents_core`,或者 `openai_hosted` 环境,则会在读取上述任何来源之前按发送内容进行比较:即使 Agent、Template、Credential 或 deployment 默认值已更改或删除,匹配的重试仍会返回原有 Session。其他请求则按解析后的配置进行比较。模型提供商密钥仅以指纹形式参与比较。 +键的作用域限定为 Project;该 Project 的任何键,包括轮换后签发的键,都可以用于重试。每个创建请求都会在读取任何已保存 Agent、Template、Credential 或 deployment 默认值之前按发送内容进行比较:即使这些来源已更改或删除,匹配的重试仍会返回原有 Session。流式传输以及空的或为 null 的 `metadata` 不参与比较;显式写出的 Agent 默认值(例如 `text.verbosity`)参与比较,因此这样的重试会产生冲突。模型提供商密钥仅以指纹形式参与比较。 ### 更新和列表 {#update-and-list} diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index 768d7e63c..27200285e 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -154,7 +154,7 @@ Any other member is rejected with 400. `api_key` is write-only: reads return `ap The harness is the agent program that runs a Session: Codex (`codex`), Claude Code (`claude_sdk`) or MiniMax Code (`mcode`). Set `x_agents_core.harness` on the Agent or the inline `agent`; without it, the installation's default harness applies ([`core.default_harness`](../configuration.md#settings), Codex unless the operator changed it). -- **Model.** `model` is the provider's exact model ID. An inline Agent on an `openai_hosted` or `none` Session may omit it to use the default model configuration of its harness. A saved Agent always needs one. +- **Model.** `model` is the provider's exact model ID. An inline Agent may omit it to use the default model configuration of its harness. A saved Agent always needs one. - **Provider.** The harness calls your provider with one of the harness's native protocols, through a [credential gateway](../../contracts/agents-api/model-execution.md#credential-gateway) that keeps your key out of the harness; there is no conversion, and a mismatch is rejected when the Session is created. [Model execution](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) lists each harness's protocols and which provider a Session uses on each Environment type. A Session freezes its provider at creation. - **Native parameters.** `harness_config` carries the harness's own model settings; see [native model parameters](../../contracts/agents-api/model-execution.md#native-model-parameters). @@ -238,18 +238,18 @@ Returns 201 with the Session: | Field | Meaning | | --- | --- | | `environment` | Required. Where the agent works; see the table below | -| `agent_id` or `agent` | A saved Agent, or an inline Agent object (same fields as create). An inline Agent on `openai_hosted` or `none` may omit `model` to use the installation default | +| `agent_id` or `agent` | A saved Agent, or an inline Agent object (same fields as create). An inline Agent may omit `model` to use the installation default | | `input` | The first message: a string or a message array. Required on `none`, and with `stream: true` except on `self_hosted` ([initial input](../../contracts/agents-api/sessions-events.md#initial-input-at-session-creation)) | | `metadata` | Your own string key-value pairs | | `vault_ids` | [Vaults](#vaults) whose credentials MCP servers may use | | `stream` | `true` returns [server-sent events](#stream-events) instead of JSON | -| `x_agents_core.model_provider` | This Session's model access, if not from the Agent or the default. Rejected on `none` | +| `x_agents_core.model_provider` | This Session's model access, if not from the Agent or the default | | `environment.type` | Runs on | Notes | | --- | --- | --- | | `openai_hosted` | A sandbox Core creates on a node or E2B; the administrator provides the capacity | Optional `network`, `packages`, `files`, `skills`, `plugins`, `env`, `capability_directories`, `setup_commands`, or a template | -| `self_hosted` | Your own Linux, macOS or Windows machine | Requires an absolute `workspace_directory`. Skills, packages, files or a template go in `x_agents_core.environment`. The response carries install commands in `x_agents_core.installation`; see [self-hosted execution](../getting-started/self-hosted.md). The Session brings its own `model_provider` | -| `none` | A device connection an operator registered, with no workspace | `input` required. The model comes from the installation default, or from the device when no default is configured | +| `self_hosted` | Your own Linux, macOS or Windows machine | Requires an absolute `workspace_directory`. Skills, packages, files or a template go in `x_agents_core.environment`. The response carries install commands in `x_agents_core.installation`; see [self-hosted execution](../getting-started/self-hosted.md) | +| `none` | A device connection an operator registered, with no workspace | `input` required | A new `openai_hosted` Session reads `idle` while Core prepares its sandbox; its first Turn starts when the Environment is ready. The [Environment contract](../../contracts/agents-api/environments.md) owns placement, expiry and preparation. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index a4849fd8d..566cbc111 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -66,7 +66,7 @@ Applications, nodes and sandboxes reach Core at one address, the public URL. HTT ## Set a default model -Core-hosted Sessions without their own model provider use their harness's default model. On **System**, under **Default model configuration**, find the harness marked **Default** (Codex unless you changed `core.default_harness`) and choose **Set**. Enter the model ID, the protocol, and the provider's base URL and API key. MiniMax Code also needs the context window and max output tokens. See [default models](../configuration.md#default-models). +Sessions without their own model provider use their harness's default model. On **System**, under **Default model configuration**, find the harness marked **Default** (Codex unless you changed `core.default_harness`) and choose **Set**. Enter the model ID, the protocol, and the provider's base URL and API key. MiniMax Code also needs the context window and max output tokens. See [default models](../configuration.md#default-models). ## Issue a Project API key diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index ad027bc0e..09911618f 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -181,7 +181,7 @@ Mutating `oac` commands hold `.oac.lock`. If another command holds it, retry aft | `This installation did not finish installing …` | The installer stopped before reporting that the services were running. Rerun the installer command, which [removes what is left](./install.md#install) and installs again, or [uninstall](#uninstall) it | | Web answers 403 `Forbidden` | The browser host is not `OAC_PUBLIC_URL`. Open that origin; a reverse proxy must pass the original Host | | Web shows that Core is unavailable (502) | Core is stopped or failing: `docker compose ps`, then Core's log | -| Session creation returns 400 `model_provider_required` | No model provider: set a [default model](../configuration.md#default-models) for the harness, or pass one; self-hosted Sessions always pass their own | +| Session creation returns 400 `model_provider_required` | No model provider: set a [default model](../configuration.md#default-models) for the harness, or pass one | | Add node shows no command | See [Before you add a node](./nodes.md#before-you-add-a-node) | | A node is not ready | See [node troubleshooting](./nodes.md#troubleshooting) | diff --git a/docs/getting-started/self-hosted.md b/docs/getting-started/self-hosted.md index 426ab539e..30827d897 100644 --- a/docs/getting-started/self-hosted.md +++ b/docs/getting-started/self-hosted.md @@ -6,7 +6,7 @@ A `self_hosted` Session runs on a machine your application owns: a workstation, **The daemon is not a sandbox.** Tools run with the permissions of the account that starts it and can reach whatever that account can. Use a container or VM when you need isolation; see [Runtime and outer isolation](../concepts.md#runtime-and-outer-isolation). The daemon does not restrict network access, so a Template that requires a network policy is rejected for a self-hosted Session. -The Session brings its own model provider; the installation default never applies ([why](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence)). The machine gets an executor credential that works for this one Environment and nothing else. +The Session's model provider resolves as for any other Session, so the installation default applies when neither the Session nor its Agent supplies one; its key then reaches this machine ([model execution](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence)). The machine gets an executor credential that works for this one Environment and nothing else. ## Platforms diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index f2e995dfe..5d0504340 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -57,7 +57,7 @@ The prompt request (`prompt_request`, or the configuration of `execution_prepare | Field | Set by Core | | --- | --- | -| `model`, `system_prompt`, `model_provider`, `harness_config` | From the Session's frozen configuration: the Agent's model and instructions, the provider bundle when the Session has one, and the [native model parameters](../contracts/agents-api/model-execution.md#native-model-parameters). The Harness validates them before any native effect | +| `model`, `system_prompt`, `model_provider`, `harness_config` | From the Session's frozen configuration: the Agent's model and instructions, the Session's provider bundle, and the [native model parameters](../contracts/agents-api/model-execution.md#native-model-parameters). The Harness validates them before any native effect | | `execution_controls` | Always: web search `disabled`, the resolved text verbosity (default `medium`), an explicit programmatic-tool-calling disable and any `json_schema` output format. Native option names belong to the adapter | | `observe_tool_observations` | Always. Tool-call frames then carry the engine-neutral `observation` | | `observe_messages` | When the Runtime declares `message_items`. Text deltas then carry the native item ID, and `output_message` frames report message start, completion, phase and the completion text | diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 3b9ef7c78..354d7ce40 100644 --- a/docs/zh/api/public-agent-api.md +++ b/docs/zh/api/public-agent-api.md @@ -1,7 +1,7 @@ --- title: "Agents API 指南" source: docs/api/public-agent-api.md -source_hash: facf77cd5eb7e4edb8fcb74b38915dea87932ecf77b36e2da92d6532b22dad91 +source_hash: f7999939977b8cf4a516c2bea09cd50c8078d6fb05dffeb137dfba45c47839d1 --- Core 在 `/v1` 提供 [OpenAI Agents API](https://platform.openai.com/docs/api-reference)。可以使用官方 OpenAI SDK 或普通 HTTP。本指南针对每项常见操作同时展示这两种方式,并说明 Core 与 OpenAI 存在差异的地方。 @@ -156,7 +156,7 @@ Core 可以运行多种 harness,并允许接入你自己的模型访问方式 harness 是运行 Session 的 Agent 程序:Codex(`codex`)、Claude Code(`claude_sdk`)或 MiniMax Code(`mcode`)。请在 Agent 或内联 `agent` 上设置 `x_agents_core.harness`;如果未设置,则使用安装的默认 harness([`core.default_harness`](../configuration.md#settings),除非操作员另行更改,否则为 Codex)。 -- **模型。** `model` 是提供商给出的准确模型 ID。`openai_hosted` 或 `none` Session 上的内联 Agent 可以省略此字段,以使用其 harness 的默认模型配置。保存的 Agent 始终必须指定模型。 +- **模型。** `model` 是提供商给出的准确模型 ID。内联 Agent 可以省略此字段,以使用其 harness 的默认模型配置。保存的 Agent 始终必须指定模型。 - **提供商。** harness 会使用其原生协议之一,经由一个让你的密钥不进入 harness 的[凭据网关](../../../contracts/agents-api/zh/model-execution.md#credential-gateway)调用你的提供商;系统不会进行转换,不匹配时会在创建 Session 阶段拒绝请求。[Model execution](../../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence) 列出了每个 harness 的协议,以及各类 Environment 上 Session 使用的提供商。Session 会在创建时冻结其提供商。 - **原生参数。** `harness_config` 承载 harness 自身的模型设置;请参阅[原生模型参数](../../../contracts/agents-api/zh/model-execution.md#native-model-parameters)。 @@ -240,18 +240,18 @@ oac "/agents/sessions" -H "Idempotency-Key: $(uuidgen)" -d '{ | 字段 | 含义 | | --- | --- | | `environment` | 必填。Agent 的工作位置;请参阅下表 | -| `agent_id` 或 `agent` | 已保存的 Agent,或内联 Agent 对象(字段与 Agent 创建操作相同)。`openai_hosted` 或 `none` 上的内联 Agent 可以省略 `model`,以使用安装的默认模型 | +| `agent_id` 或 `agent` | 已保存的 Agent,或内联 Agent 对象(字段与 Agent 创建操作相同)。内联 Agent 可以省略 `model`,以使用安装的默认模型 | | `input` | 第一条消息:字符串或消息数组。在 `none` 上为必填;在 `self_hosted` 之外使用 `stream: true` 时也为必填([初始输入](../../../contracts/agents-api/zh/sessions-events.md#initial-input-at-session-creation)) | | `metadata` | 你自己的字符串键值对 | | `vault_ids` | MCP 服务器可使用其凭据的 [Vaults](#vaults) | | `stream` | `true` 时返回[服务器发送事件](#stream-events),而不是 JSON | -| `x_agents_core.model_provider` | 如果未从 Agent 或默认值继承,则指定此 Session 的模型访问方式。在 `none` 上会被拒绝 | +| `x_agents_core.model_provider` | 如果未从 Agent 或默认值继承,则指定此 Session 的模型访问方式 | | `environment.type` | 运行位置 | 备注 | | --- | --- | --- | | `openai_hosted` | Core 在某个节点或 E2B 上创建的沙箱;容量由管理员提供 | 可选 `network`、`packages`、`files`、`skills`、`plugins`、`env`、`capability_directories`、`setup_commands`,也可指定模板 | -| `self_hosted` | 你自己的 Linux、macOS 或 Windows 计算机 | 要求提供绝对路径 `workspace_directory`。Skills、软件包、文件或模板应放在 `x_agents_core.environment` 中。响应会在 `x_agents_core.installation` 中携带安装命令;请参阅 [self-hosted execution](../getting-started/self-hosted.md)。Session 会自带自己的 `model_provider` | -| `none` | 由操作员注册的设备连接,无工作区 | `input` 为必填。模型来自安装的默认配置;如果未配置默认模型,则来自设备 | +| `self_hosted` | 你自己的 Linux、macOS 或 Windows 计算机 | 要求提供绝对路径 `workspace_directory`。Skills、软件包、文件或模板应放在 `x_agents_core.environment` 中。响应会在 `x_agents_core.installation` 中携带安装命令;请参阅 [self-hosted execution](../getting-started/self-hosted.md) | +| `none` | 由操作员注册的设备连接,无工作区 | `input` 为必填 | 新建的 `openai_hosted` Session 在 Core 准备沙箱期间会读取到 `idle`;Environment 准备就绪后,其首个 Turn 才会启动。[Environment contract](../../../contracts/agents-api/zh/environments.md) 负责部署位置、过期和准备过程。 diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index 55e711e89..f45e2806b 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,7 +1,7 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: b588dc9d68ba909fe9fadc440ee3f6fda9b79ebe0f14f6011d8bab603e17c710 +source_hash: d35808e863a229c231ad94b3be5077b15b7d2ae8950695cc86f205babe806653 --- 一条命令即可在 Linux 主机上安装 Core、Web 控制台和 PostgreSQL。用 Core 密钥登录 Web,设置默认模型并签发 Project API 密钥。应用使用这些密钥调用 Core。Session 在你添加的节点上的沙箱中运行,也可以在 E2B 上运行。 @@ -68,7 +68,7 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/ ## 设置默认模型 {#set-a-default-model} -没有自带模型提供商的 Core 托管 Session 使用其 Harness 的默认模型。在 **System** 的 **Default model configuration** 下,找到标记为 **Default** 的 Harness(除非修改了 `core.default_harness`,否则为 Codex),选择 **Set**。输入模型 ID、协议以及提供商的基础 URL 和 API 密钥。MiniMax Code 还需要上下文窗口和最大输出 token 数。参阅[默认模型](../configuration.md#default-models)。 +没有自带模型提供商的 Session 使用其 Harness 的默认模型。在 **System** 的 **Default model configuration** 下,找到标记为 **Default** 的 Harness(除非修改了 `core.default_harness`,否则为 Codex),选择 **Set**。输入模型 ID、协议以及提供商的基础 URL 和 API 密钥。MiniMax Code 还需要上下文窗口和最大输出 token 数。参阅[默认模型](../configuration.md#default-models)。 ## 签发 Project API 密钥 {#issue-a-project-api-key} diff --git a/docs/zh/getting-started/operations.md b/docs/zh/getting-started/operations.md index 41ee66157..032fad44d 100644 --- a/docs/zh/getting-started/operations.md +++ b/docs/zh/getting-started/operations.md @@ -1,7 +1,7 @@ --- title: "管理你的安装" source: docs/getting-started/operations.md -source_hash: 5ac3e57f943b8bb3fadbf9cda0a72399317ad101416ec28ca4037eea07703a82 +source_hash: c9df6b31ca54f1601aad360c3c3f8039ef35eb7bd4f6084b8495a18c131dcbfb --- 安装运维人员负责 Core 主机、存储和可用性。节点主机运行各自的服务;参阅[节点](nodes.md)。设置见[配置参考](../configuration.md)。 @@ -184,7 +184,7 @@ cd && rm -rf ~/.oac/core | `This installation did not finish installing …` | 安装程序在报告服务运行前停止。重新执行安装命令,[清理残留](install.md#install)后重新安装,或[卸载](#uninstall) | | Web 返回 403 `Forbidden` | 浏览器主机名不是 `OAC_PUBLIC_URL`。打开该源地址;反向代理必须传递原始 Host | | Web 显示 Core 不可用(502) | Core 停止或失败:先 `docker compose ps`,再查看 Core 日志 | -| 创建 Session 返回 400 `model_provider_required` | 缺少模型提供商:为 Harness 设置[默认模型](../configuration.md#default-models),或显式提供;自托管 Session 始终自带提供商 | +| 创建 Session 返回 400 `model_provider_required` | 缺少模型提供商:为 Harness 设置[默认模型](../configuration.md#default-models),或显式提供 | | Add node 不展示命令 | 参阅[添加节点前](nodes.md#before-you-add-a-node) | | 节点未就绪 | 参阅[节点问题排查](nodes.md#troubleshooting) | diff --git a/docs/zh/getting-started/self-hosted.md b/docs/zh/getting-started/self-hosted.md index 7765a1739..010d57197 100644 --- a/docs/zh/getting-started/self-hosted.md +++ b/docs/zh/getting-started/self-hosted.md @@ -1,14 +1,14 @@ --- title: "自托管执行器" source: docs/getting-started/self-hosted.md -source_hash: 800b3eb891c20d34215344c0ea147dbb79b5921d25d6f2405bcfe5edccd88038 +source_hash: 93c50f38d5f44bea3290e32359aeea3c5d196df4717e6c2a6893dc1242d13d25 --- `self_hosted` Session 在应用拥有的机器上运行:工作站、虚拟机或你管理的沙箱。应用通过 `/v1` 创建 Session,并获得安装 `oac-daemon`、启动它并连接 Core 的命令。Web 在 Session 页面展示同一命令;Web 是可选的。Core 不创建、停止或回收这台机器。 **守护进程不是沙箱。** 工具以启动守护进程的账号权限运行,能访问该账号可访问的所有资源。需要隔离时,请使用容器或虚拟机;参阅 [Runtime 与外层隔离](../concepts.md#runtime-and-outer-isolation)。守护进程不限制网络访问,因此要求网络策略的 Template 会被自托管 Session 拒绝。 -Session 自带模型提供商;安装默认模型不适用([原因](../../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence))。机器获得的执行器凭据仅适用于这一个 Environment。 +Session 的模型提供商与其他 Session 一样解析,因此当 Session 及其 Agent 都未提供时会使用安装默认模型,其密钥随后会到达这台机器([模型执行](../../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence))。机器获得的执行器凭据仅适用于这一个 Environment。 ## 平台 {#platforms} diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 2e0bc3d7d..8655a2782 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: 2b5d80e228532628ebf3fac18c37b32bc20ce235833bef96e9f4e2bb78a8b7ae +source_hash: 05ce7a3d0cedd37b775d74a572b079586e1678e687738dd5aaa503c3c3e50d9e --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -59,7 +59,7 @@ prompt 请求(`prompt_request` 或 `execution_prepare` 的配置)携带 Sess | 字段 | Core 设置方式 | | --- | --- | -| `model`, `system_prompt`, `model_provider`, `harness_config` | 来自 Session 冻结的配置:Agent 的 model 和 instructions、Session 拥有的 provider bundle,以及[原生模型参数](../../contracts/agents-api/zh/model-execution.md#native-model-parameters)。Harness 在产生任何原生效果之前验证它们 | +| `model`, `system_prompt`, `model_provider`, `harness_config` | 来自 Session 冻结的配置:Agent 的 model 和 instructions、Session 的 provider bundle,以及[原生模型参数](../../contracts/agents-api/zh/model-execution.md#native-model-parameters)。Harness 在产生任何原生效果之前验证它们 | | `execution_controls` | 始终设置:web search 为 `disabled`、解析后的 text verbosity(默认 `medium`)、明确禁用 programmatic tool calling,以及任何 `json_schema` 输出格式。原生选项名称由 adapter 负责 | | `observe_tool_observations` | 始终设置。tool-call frame 随后携带与 engine 无关的 `observation` | | `observe_messages` | Runtime 声明 `message_items` 时设置。文本 delta 随后携带原生 item ID,`output_message` frame 报告消息开始、完成、phase 和完成文本 | diff --git a/internal/agentdaemon/proto/prototest/model.go b/internal/agentdaemon/proto/prototest/model.go new file mode 100644 index 000000000..5e49b1008 --- /dev/null +++ b/internal/agentdaemon/proto/prototest/model.go @@ -0,0 +1,22 @@ +package prototest + +import ( + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" +) + +// Every Core request names a model and a provider, and Runtime preparation +// rejects a request without them. ModelConfiguration declares the fixture +// provider's protocol for a fixture Harness, and WithModel gives a request the +// fixture model and provider. + +func ModelConfiguration() harnessconfig.Configuration { + return harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: string(modelprovider.Responses)}}} +} + +func WithModel(req proto.PromptRequestPayload) proto.PromptRequestPayload { + req.Model = "fixture-model" + req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://model.example/v1", APIKey: "fixture-key"} + return req +} diff --git a/internal/agentdaemon/proto/prototest/wire.go b/internal/agentdaemon/proto/prototest/wire.go index d27120aad..2f05c524c 100644 --- a/internal/agentdaemon/proto/prototest/wire.go +++ b/internal/agentdaemon/proto/prototest/wire.go @@ -110,7 +110,7 @@ func WireScenarios() []WireScenario { } prepare := send(Core, proto.TypeExecutionPrepare, PreparationID, proto.ExecutionPreparePayload{ SessionID: SessionID, - Configuration: proto.PromptRequestPayload{AgentKind: HarnessKind, AgentStateKey: StateKey, StrictResume: true, DisableExecutionEnvironment: true}, + Configuration: WithModel(proto.PromptRequestPayload{AgentKind: HarnessKind, AgentStateKey: StateKey, StrictResume: true, DisableExecutionEnvironment: true}), }) started := []Step{ prepare, diff --git a/internal/harnessconfig/builtin/route_test.go b/internal/harnessconfig/builtin/route_test.go index a65c00619..6b96e6eaf 100644 --- a/internal/harnessconfig/builtin/route_test.go +++ b/internal/harnessconfig/builtin/route_test.go @@ -37,7 +37,7 @@ func TestNativeProviderDeclarationIsSharedWithRuntime(t *testing.T) { if err != nil && strings.Contains(err.Error(), "key-canary") { t.Fatal("secret in error") } - if accepted && *prepared.Provider != raw { + if accepted && prepared.Provider != raw { t.Fatal("native bundle was rewritten") } } diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 862b3829c..72d2736fa 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -48,9 +48,7 @@ type Provider struct { } // Configuration is an adapter-owned declaration, not live provider readiness. -// Providers is ordered; its first entry is the default. An explicit empty -// declaration accepts no provider or nonempty native parameters. It is useful -// only for direct adapters whose model connection remains native-owned. +// Providers is ordered; its first entry is the default. type Configuration struct { Providers []Provider // ValidateNativeConfig belongs to the selected adapter, never Core. It @@ -58,17 +56,18 @@ type Configuration struct { ValidateNativeConfig func(map[string]any) bool } -// PreparedConfiguration owns a validated snapshot without native side effects. -// Model and Provider may be absent only for the existing native-owned connection -// path. A supplied model must be nonempty; an explicit provider requires an -// explicit model. HarnessConfig is always an independently owned object. +// PreparedConfiguration owns a validated snapshot without native side effects: +// a nonempty model, the provider and an independently owned HarnessConfig. type PreparedConfiguration struct { Model string - Provider *modelprovider.Provider + Provider modelprovider.Provider HarnessConfig map[string]any } -var ErrModel = errors.New("model must be a nonempty model identifier") +var ( + ErrModel = errors.New("model must be a nonempty model identifier") + ErrModelProvider = errors.New("a model provider is required") +) // ValidateModel is shared by public admission and Runtime preparation. func ValidateModel(value any) (string, error) { @@ -80,33 +79,29 @@ func ValidateModel(value any) (string, error) { } // Prepare validates the request's model, model provider and native parameters -// against this declaration before creating native resources. The provider must -// be one this declaration supports, with the token limits it requires. +// against this declaration before creating native resources. Every request +// names a model and a provider; the provider must be one this declaration +// supports, with the token limits it requires. func (c Configuration) Prepare(req proto.PromptRequestPayload) (PreparedConfiguration, error) { - var result PreparedConfiguration - if req.Model != "" || req.ModelProvider != nil { - model, err := ValidateModel(req.Model) - if err != nil { - return PreparedConfiguration{}, err - } - result.Model = model + model, err := ValidateModel(req.Model) + if err != nil { + return PreparedConfiguration{}, err } - if req.ModelProvider != nil { - provider := *req.ModelProvider - if err := provider.Validate(); err != nil { - return PreparedConfiguration{}, err - } - if err := c.Validate(string(provider.Protocol), provider.ContextWindow, provider.MaxOutputTokens); err != nil { - return PreparedConfiguration{}, err - } - result.Provider = &provider + if req.ModelProvider == nil { + return PreparedConfiguration{}, ErrModelProvider + } + provider := *req.ModelProvider + if err := provider.Validate(); err != nil { + return PreparedConfiguration{}, err + } + if err := c.Validate(string(provider.Protocol), provider.ContextWindow, provider.MaxOutputTokens); err != nil { + return PreparedConfiguration{}, err } native, err := c.ParseHarnessConfig(req.HarnessConfig) if err != nil { return PreparedConfiguration{}, err } - result.HarnessConfig = native - return result, nil + return PreparedConfiguration{Model: model, Provider: provider, HarnessConfig: native}, nil } // ValidateDeclaration rejects ambiguous support before registration. A caller diff --git a/internal/harnessconfig/preparation_test.go b/internal/harnessconfig/preparation_test.go index 9da20f4d4..485efaed0 100644 --- a/internal/harnessconfig/preparation_test.go +++ b/internal/harnessconfig/preparation_test.go @@ -15,22 +15,24 @@ func TestPrepareModelConfiguration(t *testing.T) { provider := func() *modelprovider.Provider { return &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://provider.example/v1", APIKey: "private-sentinel"} } + with := func(native string) proto.PromptRequestPayload { + return proto.PromptRequestPayload{Model: "fixture", ModelProvider: provider(), HarnessConfig: json.RawMessage(native)} + } for _, tc := range []struct { name string req proto.PromptRequestPayload valid bool }{ - {"native owned", proto.PromptRequestPayload{}, true}, - {"native model", proto.PromptRequestPayload{Model: "fixture"}, true}, {"explicit", proto.PromptRequestPayload{Model: "fixture", ModelProvider: provider()}, true}, + {"missing provider", proto.PromptRequestPayload{Model: "fixture"}, false}, {"missing model", proto.PromptRequestPayload{ModelProvider: provider()}, false}, - {"blank model", proto.PromptRequestPayload{Model: " "}, false}, + {"blank model", proto.PromptRequestPayload{Model: " ", ModelProvider: provider()}, false}, {"invalid provider", proto.PromptRequestPayload{Model: "fixture", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses}}, false}, {"undeclared protocol", proto.PromptRequestPayload{Model: "fixture", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example", APIKey: "private-sentinel"}}, false}, - {"empty native", proto.PromptRequestPayload{HarnessConfig: json.RawMessage(`{}`)}, true}, - {"null native", proto.PromptRequestPayload{HarnessConfig: json.RawMessage(`null`)}, false}, - {"array native", proto.PromptRequestPayload{HarnessConfig: json.RawMessage(`[]`)}, false}, - {"undeclared native", proto.PromptRequestPayload{HarnessConfig: json.RawMessage(`{"effort":"high"}`)}, false}, + {"empty native", with(`{}`), true}, + {"null native", with(`null`), false}, + {"array native", with(`[]`), false}, + {"undeclared native", with(`{"effort":"high"}`), false}, } { t.Run(tc.name, func(t *testing.T) { got, err := c.Prepare(tc.req) @@ -74,3 +76,23 @@ func TestConfigurationDeclarationRejectsUnknownAndDuplicateProtocols(t *testing. } } } + +// The typed wire fields decode an explicit null or "" as absent, so Prepare +// must reject each of these decoded prompt requests. +func TestPrepareRejectsDecodedRequestsWithoutModelOrProvider(t *testing.T) { + c := Configuration{Providers: []Provider{{Protocol: "responses"}}} + provider := `{"protocol":"responses","base_url":"https://provider.example/v1","api_key":"private-sentinel"}` + for payload, want := range map[string]error{ + `{"model":"m","model_provider":null}`: ErrModelProvider, + `{"model":"","model_provider":` + provider + `}`: ErrModel, + `{"model":null,"model_provider":` + provider + `}`: ErrModel, + } { + var req proto.PromptRequestPayload + if err := (proto.Envelope{Type: proto.TypePromptRequest, Payload: json.RawMessage(payload)}).DecodeRequest(&req); err != nil { + t.Fatalf("%s: %v", payload, err) + } + if _, err := c.Prepare(req); !errors.Is(err, want) { + t.Fatalf("%s: err=%v, want %v", payload, err, want) + } + } +} diff --git a/internal/modelprovider/config.go b/internal/modelprovider/config.go index 34c64dcf6..ab0186d9b 100644 --- a/internal/modelprovider/config.go +++ b/internal/modelprovider/config.go @@ -56,8 +56,8 @@ func (p Provider) Validate() error { if err != nil || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || strings.ContainsAny(p.BaseURL, "\x00\r\n") || !p.Protocol.ValidBasePath(u.Path) { return ErrConfiguration } - // Remote providers require HTTPS. Runtime-local providers may use loopback - // HTTP; Core retains stricter public provider admission. + // Providers require HTTPS. Loopback HTTP exists only for the gateway + // listener a view hands its Harness; Core admits only HTTPS providers. if u.Scheme != "https" && !(u.Scheme == "http" && net.ParseIP(u.Hostname()).IsLoopback()) { return ErrConfiguration } diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index f32734849..3d449870a 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -72,7 +72,7 @@ SDK state lives under `paths.ProfileDir(profile)/runtime/claude-sdk`, independen The SDK descriptor advertises the validated daemon subset, including durable Turns/input receipts, text observations, function tools, raw usage and restrictive execution controls. It does not advertise permissions, raw tool Items, general web-search control or text-verbosity levels. Router admission for `environment:none` uses the available engine capability, not an engine name. -Core owns public admission for Claude: [harness selection](../../contracts/agents-api/model-execution.md#harness-selection) chooses the engine for each Session; one [engine policy](../../contracts/agents-api/harness-onboarding.md#add-the-engine-to-core) serves API admission, device selection and the final claim; the [qualified operations](../../contracts/agents-api/harness-capabilities.md) table records Claude's medium-only verbosity and object-root function schemas; [function result images](../../contracts/agents-api/message-content.md#function-results) defines which placements accept image results; and [deployment defaults](../../contracts/agents-api/model-execution.md#deployment-defaults) define the model provider Core freezes for a Session. The adapter receives that provider in the request's typed `model_provider`, never in public Session configuration. Without one, a `none` host uses the daemon's own provider environment. The adapter alone selects the provider environment and removes credentials from native tool environments. +Core owns public admission for Claude: [harness selection](../../contracts/agents-api/model-execution.md#harness-selection) chooses the engine for each Session; one [engine policy](../../contracts/agents-api/harness-onboarding.md#add-the-engine-to-core) serves API admission, device selection and the final claim; the [qualified operations](../../contracts/agents-api/harness-capabilities.md) table records Claude's medium-only verbosity and object-root function schemas; [function result images](../../contracts/agents-api/message-content.md#function-results) defines which placements accept image results; and [deployment defaults](../../contracts/agents-api/model-execution.md#deployment-defaults) define the model provider Core freezes for a Session. The adapter receives that provider in the request's typed `model_provider`, never in public Session configuration. The adapter alone selects the provider environment and removes credentials from native tool environments. The `none` profile accepts only text, explicit model/system instructions, managed state, exact native resume and declared functions with ordered text or successful inline PNG/JPEG results, and the HTTP MCP subset described above. It rejects unsupported request fields and disables built-in tools and undeclared MCP discovery. `DisableExecutionEnvironment` and `DisableSubagents` are accepted assertions about the single-Agent restrictive profile. Omission does not enable built-in tools. Explicit Subagent observation enables only the native delegation tools described under [Subagents](#subagents). Single-Agent new and resumed queries use the SDK's empty built-in tool set, explicit function MCP configuration and allowlist, strict MCP configuration and empty user/project/local setting sources. Without HTTP MCP declarations, native initialization and real provider request inventories must contain only the declared host functions. Managed operator policy may further restrict execution; it must not widen the profile. The profile limits model tool access; it does not isolate native state files or filesystem access by an explicitly supplied host function. The private factory accepts typed execution controls only for disabled search and medium text verbosity. Search remains excluded by the native tool inventory; medium retains the SDK's default text generation, without adding instructions or changing caller input. The pinned SDK has no native verbosity-level option, so low and high verbosity and enabled search are unsupported. Missing/invalid fields in a supplied control block fail before native setup; omitting the block keeps the same restrictive profile. Use the SDK's history lookup before explicit resume; never fall back to a new Session. Native files remain device-affine under a caller-selected managed runtime directory. The launch configuration supplies trusted provider environment; the request cannot supply environment variables or business write authority. An omitted or empty `system_prompt` maps to empty SDK instructions only at this adapter boundary; a request without a model is rejected. diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index e80ea729c..c6d817b57 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -106,7 +106,7 @@ Session deletion takes its decision and commits the `deleted_at` marker under th Every new Session needs an explicit typed creator at the `sessions` boundary, internal callers included; public creation derives it from the authenticated principal only. Creator kind and ID persist in the creation transaction and never change on retry, update or source mutation. Both the early saved-reference recovery and the authoritative creation upsert require a matching creator before returning a Session or event cursor. Tenant scope always comes from the authenticated identity before any storage call; metadata grants nothing. Tests supply explicit synthetic creators. -New saved-reference Sessions, and inline requests with Vault attachments or credential references, record a separate caller-intent hash: the source ID (empty for inline), supplied overrides with field presence, Environment and Vaults, original metadata and normalized initial input, excluding response streaming and resolved source values. Compare that same-tenant retry identity before looking up the source; a match returns the existing Session without input admission or source revalidation. Recheck after a source resolution failure for a concurrently committed creator, without holding a lock across resolution; the unique creation upsert stays authoritative. Credential-bound retries recover before reading mutable Vault contents. Every new hosted Session also records caller intent, before deployment defaults are resolved; the resolved-configuration hash of other inline Sessions leaves the deployment default out. Provider keys enter retry hashes only as keyed fingerprints. +Every new Session records a separate caller-intent hash: the source ID (empty for inline), supplied overrides with field presence, Environment and Vaults, original metadata and normalized initial input, excluding response streaming and resolved source values. Compare that same-tenant retry identity before looking up the source; a match returns the existing Session without input admission or source revalidation. Recheck after a source resolution failure for a concurrently committed creator, without holding a lock across resolution; the unique creation upsert stays authoritative. Credential-bound retries recover before reading mutable Vault contents. The resolved-configuration hash leaves the deployment default out. Provider keys enter retry hashes only as keyed fingerprints. Session listing by `agent_id` filters on the immutable root `configuration.agent.id` with the tenant, Agent and creation index, before pagination and activity projection. diff --git a/services/core/internal/api/dependencies_test.go b/services/core/internal/api/dependencies_test.go index c7a5bf73b..70822e5eb 100644 --- a/services/core/internal/api/dependencies_test.go +++ b/services/core/internal/api/dependencies_test.go @@ -1,12 +1,10 @@ package api import ( - "context" "net/http" "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) @@ -168,11 +166,6 @@ func newTestHandler(t testing.TB, deps Dependencies) http.Handler { return h } -// noDeploymentModelProvider is a deployment without a default model provider. -func noDeploymentModelProvider(context.Context, string) (*modelconfiguration.Snapshot, error) { - return nil, nil -} - func TestNewHandlerAcceptsCompleteDependencies(t *testing.T) { deps, f := testDependencies(t) if _, err := NewHandler(deps); err != nil { diff --git a/services/core/internal/api/environment_creation_test.go b/services/core/internal/api/environment_creation_test.go index 7feca96c7..a1a8a3a0f 100644 --- a/services/core/internal/api/environment_creation_test.go +++ b/services/core/internal/api/environment_creation_test.go @@ -166,7 +166,6 @@ func TestSelfHostedCreationRejectsBeforePersistence(t *testing.T) { {name: "newline workspace placement", environment: `{"type":"self_hosted","workspace_directory":"/remote/\n"}`}, {name: "carriage return workspace placement", environment: `{"type":"self_hosted","workspace_directory":"/remote/\r"}`}, {name: "backslash workspace placement", environment: `{"type":"self_hosted","workspace_directory":"/remote/\\"}`}, - {name: "capabilities", environment: `{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":["/remote/skills"]}`}, {name: "null capability entry", environment: `{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":[null]}`}, {name: "scalar capabilities", environment: `{"type":"self_hosted","workspace_directory":"/workspace","capability_directories":"/remote/skills"}`}, {name: "output field", environment: `{"type":"self_hosted","workspace_directory":"/workspace","remote_url":"https://forged.example"}`}, diff --git a/services/core/internal/api/handler.go b/services/core/internal/api/handler.go index a06ca1d0a..af771c4f1 100644 --- a/services/core/internal/api/handler.go +++ b/services/core/internal/api/handler.go @@ -125,7 +125,7 @@ func (h *Handler) routes() *chi.Mux { // createSession atomically reserves or admits initial text with the Session. // @Summary Create an execution Session -// @Description The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then, for openai_hosted and none, the deployment default of the resolved harness; self_hosted never uses the deployment default and none accepts only it. openai_hosted and self_hosted Sessions that resolve no bundle return 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Explicit environment-origin HTTP MCP is supported on managed and self-hosted workspaces through the same Runtime bindings; native OAuth login remains unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using explicit environment origin, optionally authenticated by the attached Vault rules. Service-origin HTTP remains restricted to service-side environment:none. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; new hosted inline requests also freeze caller intent before deployment defaults resolve; unrelated non-hosted inline retries preserve resolved/default equivalences, and their resolved hash leaves out any deployment default. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none, Core-managed Docker openai_hosted and self_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; explicit environment-origin HTTP MCP uses the common Runtime path. MiniMax accepts public environment-origin HTTP MCP only with null or omitted allowed_tools and required=false; even an empty non-null allowlist rejects. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude function profile on none or a managed/user-owned workspace, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. +// @Description The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then the deployment default of the resolved harness, for every environment type. A Session that resolves no bundle returns 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Explicit environment-origin HTTP MCP is supported on managed and self-hosted workspaces through the same Runtime bindings; native OAuth login remains unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using explicit environment origin, optionally authenticated by the attached Vault rules. Service-origin HTTP remains restricted to service-side environment:none. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Every creation records caller intent before saved Agents, templates, Vault credentials or deployment defaults resolve, so a same-intent retry returns the committed Session independently of later changes to those resources. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none, Core-managed Docker openai_hosted and self_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; explicit environment-origin HTTP MCP uses the common Runtime path. MiniMax accepts public environment-origin HTTP MCP only with null or omitted allowed_tools and required=false; even an empty non-null allowlist rejects. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude function profile on none or a managed/user-owned workspace, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. // @Tags Sessions // @Accept json // @Produce json,text/event-stream diff --git a/services/core/internal/api/handler_test.go b/services/core/internal/api/handler_test.go index 8257827b7..b768cb627 100644 --- a/services/core/internal/api/handler_test.go +++ b/services/core/internal/api/handler_test.go @@ -57,9 +57,9 @@ func (s *recordingStore) record(f *testFakes) { // testHandler serves strict fakes for a fresh tenant whose caller // authenticates with "Bearer test-api-key". A recordingStore answers Session -// creation, reads and listing, and the deployment has no default model -// provider. Each configure func adjusts the dependencies before the handler is -// built. +// creation, reads and listing, and the deployment has a default model provider +// for every harness. Each configure func adjusts the dependencies before the +// handler is built. func testHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, *recordingStore, string) { t.Helper() tenant := uuid.NewString() @@ -68,7 +68,7 @@ func testHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (ht fakes.projectsReader.resolveAPIKey = projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: hex.EncodeToString(hash[:]), TenantID: tenant}).ResolveAPIKey s := &recordingStore{} s.record(fakes) - fakes.modelProviders.resolve = noDeploymentModelProvider + fakes.modelProviders.resolve = fixtureDeploymentProvider for _, c := range configure { c(&deps, fakes) } diff --git a/services/core/internal/api/model_provider_fixture_test.go b/services/core/internal/api/model_provider_fixture_test.go index 845218400..e2e44dcf4 100644 --- a/services/core/internal/api/model_provider_fixture_test.go +++ b/services/core/internal/api/model_provider_fixture_test.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" ) -// Hosted and self-hosted Sessions must freeze a model provider. Tests that -// exercise other behavior supply these fixtures instead of relaxing that check. +// Every Session must freeze a model provider. Tests that exercise other +// behavior supply these fixtures instead of relaxing that check. // fixtureModelProvider returns a valid bundle for the harness. func fixtureModelProvider(harness string) *v1.ModelProviderInput { diff --git a/services/core/internal/api/session_creation_identity.go b/services/core/internal/api/session_creation_identity.go index 8a3c96385..b174220db 100644 --- a/services/core/internal/api/session_creation_identity.go +++ b/services/core/internal/api/session_creation_identity.go @@ -11,15 +11,9 @@ import ( ) // sessionCreationRequest records caller intent before mutable sources resolve: -// saved Agents, templates, credentials and hosted deployment defaults. A retry -// then returns the committed Session even after those sources change. Other -// inline requests keep the resolved-request retry rule; the sessions package -// leaves the deployment default out of that hash, so it cannot change their -// identity. +// saved Agents, templates, credentials and deployment defaults. A retry then +// returns the committed Session even after those sources change or go away. func sessionCreationRequest(input sessionRequest, initial []sessions.Input) (json.RawMessage, error) { - if input.Agent != nil && input.Agent.Model != nil && (input.Environment == nil || input.Environment.Type != "openai_hosted") && input.XAgentsCore == nil && input.AgentID == nil && input.templateID == "" && len(input.initialFiles) == 0 && input.initialization.Empty() && !inlineCredentialIntent(input) && input.agentFields["x_agents_core"] == nil { - return nil, nil - } agentID := "" if input.AgentID != nil { agentID = *input.AgentID @@ -43,9 +37,6 @@ func sessionCreationRequest(input sessionRequest, initial []sessions.Input) (jso } func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, key string, request json.RawMessage, stream bool) bool { - if len(request) == 0 { - return false - } result, err := h.SessionCreation.FindSessionCreation(r.Context(), tenantID(r), key, request, sessionCreator(r)) if errors.Is(err, sessions.ErrNotFound) { return false diff --git a/services/core/internal/api/session_creation_stream_test.go b/services/core/internal/api/session_creation_stream_test.go index ada43b580..589e32281 100644 --- a/services/core/internal/api/session_creation_stream_test.go +++ b/services/core/internal/api/session_creation_stream_test.go @@ -148,7 +148,7 @@ func newCreationStreamHarness(t *testing.T) *creationStreamHarness { fakes.sessionsReader.getSession, fakes.sessions.auditSessionOperation = fixture.GetSession, fixture.AuditSessionOperation fakes.sessionCreation.findSessionCreation = fixture.FindSessionCreation fakes.sessionEvents.sessionEventCursor, fakes.sessionEvents.sessionStreamSnapshot, fakes.sessionEvents.listSessionEvents = fixture.SessionEventCursor, fixture.SessionStreamSnapshot, fixture.ListSessionEvents - fakes.modelProviders.resolve = noDeploymentModelProvider + fakes.modelProviders.resolve = fixtureDeploymentProvider deps.Execution = fakes.execution() fakes.sessionAdmission.createSession = fixture.CreateSession handler := newTestHandler(t, deps) diff --git a/services/core/internal/api/session_credentials.go b/services/core/internal/api/session_credentials.go index ac03df820..8766de817 100644 --- a/services/core/internal/api/session_credentials.go +++ b/services/core/internal/api/session_credentials.go @@ -95,23 +95,3 @@ func attachedVault(attached []string, vault string) bool { } return false } - -// Attached inline requests need recorded caller intent before reading mutable -// Vault contents. Other inline requests retain their resolved/default identity. -func inlineCredentialIntent(input sessionRequest) bool { - if len(input.VaultIDs) > 0 { - return true - } - var tools []struct { - Type string `json:"type"` - CredentialID *string `json:"credential_id"` - } - if json.Unmarshal(input.agentFields["tools"], &tools) == nil { - for _, tool := range tools { - if tool.Type == "mcp" && tool.CredentialID != nil { - return true - } - } - } - return false -} diff --git a/services/core/internal/api/session_credentials_test.go b/services/core/internal/api/session_credentials_test.go index 7401b5d1f..1e47cbe2f 100644 --- a/services/core/internal/api/session_credentials_test.go +++ b/services/core/internal/api/session_credentials_test.go @@ -39,8 +39,8 @@ func TestSessionVaultTypesAndCreationIntent(t *testing.T) { continue } intent, err := sessionCreationRequest(input, nil) - if err != nil || (len(intent) != 0) != (len(input.VaultIDs) > 0) { - t.Fatal("attached inline intent missing or unrelated inline identity changed", err) + if err != nil || len(intent) == 0 { + t.Fatal("inline intent missing", err) } } var request decodedSessionRequest diff --git a/services/core/internal/api/session_execution_configuration.go b/services/core/internal/api/session_execution_configuration.go index 9acdc758c..8a194e174 100644 --- a/services/core/internal/api/session_execution_configuration.go +++ b/services/core/internal/api/session_execution_configuration.go @@ -35,16 +35,13 @@ func sessionExecutionProjection(input sessionRequest, saved *v1.SavedAgent, inhe } else if saved != nil && saved.XAgentsCore != nil && saved.XAgentsCore.Harness != "" { harnessSource = "agent" } - selection := v1.ExecutionProviderSelection{Source: "unknown", Status: "unavailable"} - if provider != nil { - // Deployment defaults are readable with the same Core key, so new - // Sessions record their safe view too; historical rows stay redacted. - selection.Source, selection.Status, selection.Configuration = "deployment", "available", provider.SafeView() - if input.XAgentsCore != nil && input.XAgentsCore.ModelProvider != nil { - selection.Source = "session" - } else if inherited != nil { - selection.Source = "agent" - } + // Deployment defaults are readable with the same Core key, so new Sessions + // record their safe view too; historical rows stay redacted. + selection := v1.ExecutionProviderSelection{Source: "deployment", Status: "available", Configuration: provider.SafeView()} + if input.XAgentsCore != nil && input.XAgentsCore.ModelProvider != nil { + selection.Source = "session" + } else if inherited != nil { + selection.Source = "agent" } native := json.RawMessage(`{}`) if configuration.Agent.Core != nil { diff --git a/services/core/internal/api/session_execution_configuration_test.go b/services/core/internal/api/session_execution_configuration_test.go index 6294695e4..88e4d85a4 100644 --- a/services/core/internal/api/session_execution_configuration_test.go +++ b/services/core/internal/api/session_execution_configuration_test.go @@ -28,7 +28,7 @@ func TestExecutionConfigurationSources(t *testing.T) { {"explicit bundle", ``, `,"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://explicit.example/v1","api_key":"explicit-secret"}}`, saved, nil, provider, "agent", "agent", "session", "available"}, {"provider null inherits", ``, `,"x_agents_core":{"model_provider":null}`, saved, provider, provider, "agent", "agent", "agent", "available"}, {"inline deployment", `,"agent":{"model":"inline"}`, ``, nil, nil, provider, "session", "deployment", "deployment", "available"}, - {"inline explicit harness", `,"agent":{"model":"inline","x_agents_core":{"harness":"codex"}}`, ``, nil, nil, nil, "session", "session", "unknown", "unavailable"}, + {"inline explicit harness", `,"agent":{"model":"inline","x_agents_core":{"harness":"codex"}}`, ``, nil, nil, provider, "session", "session", "deployment", "available"}, } { t.Run(tc.name, func(t *testing.T) { var decoded decodedSessionRequest diff --git a/services/core/internal/api/session_model_configuration.go b/services/core/internal/api/session_model_configuration.go index 4cde53a67..86304ea40 100644 --- a/services/core/internal/api/session_model_configuration.go +++ b/services/core/internal/api/session_model_configuration.go @@ -34,7 +34,7 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s explicitModel := input.Agent != nil && input.Agent.Model != nil explicitProvider := input.XAgentsCore != nil && input.XAgentsCore.ModelProvider != nil needsModel := !explicitModel && saved == nil - if v1.ModelProviderAllowed(input.Environment.Type, v1.ModelProviderSourceDeployment) && ((inherited == nil && !explicitProvider) || needsModel) { + if (inherited == nil && !explicitProvider) || needsModel { input.deploymentDefaults, err = h.ModelProviders.Resolve(ctx, engine) if err != nil { var configurationError *v1.ModelProviderError @@ -110,9 +110,6 @@ func freezeSessionHarnessConfig(raw json.RawMessage, native json.RawMessage) (js } func validateSessionModelConfiguration(engine string, provider *v1.ModelProviderInput, raw json.RawMessage) error { - if provider == nil { - return nil - } var cfg configuration if err := json.Unmarshal(raw, &cfg); err != nil { return err diff --git a/services/core/internal/api/session_model_configuration_test.go b/services/core/internal/api/session_model_configuration_test.go index bda85d060..7a7fbc5d7 100644 --- a/services/core/internal/api/session_model_configuration_test.go +++ b/services/core/internal/api/session_model_configuration_test.go @@ -28,7 +28,7 @@ func TestSessionNativeConfigurationSources(t *testing.T) { {"session beats inline", `{"agent":{"model":"other","x_agents_core":{"harness_config":{"model_reasoning_effort":"medium"}}},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{}}}`, nil, "other", `{}`, "session", false}, {"null rejects", `{"agent":{"model":"other"},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":null}}`, nil, "", "", "", true}, {"reserved rejects", `{"agent":{"model":"other"},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{"api_key":"secret"}}}`, nil, "", "", "", true}, - {"self hosted never defaults", `{"agent":{},"environment":{"type":"self_hosted","workspace_directory":"/tmp/work"}}`, nil, "", "", "", true}, + {"self_hosted deployment", `{"agent":{},"environment":{"type":"self_hosted","workspace_directory":"/tmp/work"}}`, nil, "deployment-model", `{"model_reasoning_effort":"high"}`, "deployment", false}, } { t.Run(tc.name, func(t *testing.T) { var decoded decodedSessionRequest @@ -39,10 +39,8 @@ func TestSessionNativeConfigurationSources(t *testing.T) { if err != nil { t.Fatal(err) } - calls := 0 deps, fakes := testDependencies(t) fakes.modelProviders.resolve = func(context.Context, string) (*modelconfiguration.Snapshot, error) { - calls++ return &modelconfiguration.Snapshot{Provider: provider, Model: "deployment-model", HarnessConfig: json.RawMessage(`{"model_reasoning_effort":"high"}`)}, nil } h := &Handler{Dependencies: deps} @@ -50,9 +48,6 @@ func TestSessionNativeConfigurationSources(t *testing.T) { if (err != nil) != tc.wantError { t.Fatalf("error=%v", err) } - if input.Environment.Type == "self_hosted" && calls != 0 { - t.Fatal("self-hosted accessed deployment credentials") - } if tc.wantError { return } diff --git a/services/core/internal/api/session_model_defaults.go b/services/core/internal/api/session_model_defaults.go index c897bd7c7..5d45f501c 100644 --- a/services/core/internal/api/session_model_defaults.go +++ b/services/core/internal/api/session_model_defaults.go @@ -39,8 +39,8 @@ func (h *Handler) sessionAgentDefaults(ctx context.Context, tenant string, input return saved, provider, nil } -// modelProviderRequiredError reports a hosted or self-hosted Session that -// would have no model provider. The message says what to configure. +// modelProviderRequiredError reports a Session that would have no model +// provider. The message says what to configure. type modelProviderRequiredError struct{ message string } func (e *modelProviderRequiredError) Error() string { return e.message } @@ -52,16 +52,13 @@ type modelProviderDefaultsError struct{ err error } func (e *modelProviderDefaultsError) Error() string { return e.err.Error() } func (e *modelProviderDefaultsError) Unwrap() error { return e.err } -func modelProviderRequired(environment, engine string) error { - if environment == "self_hosted" { - return &modelProviderRequiredError{"self_hosted Sessions need a model provider for harness " + engine + ": pass x_agents_core.model_provider or use an Agent that has one saved. Deployment default model providers apply to openai_hosted and none Sessions, never to self_hosted."} - } +func modelProviderRequired(engine string) error { return &modelProviderRequiredError{"No model provider is configured for harness " + engine + ". Pass x_agents_core.model_provider, use an Agent that has one saved, or ask the Core administrator to set a deployment default model provider for " + engine + "."} } // resolveSessionExecution applies provider precedence: the Session bundle, the -// saved Agent bundle, then the deployment default where the environment allows -// it. Bundles are never merged. +// saved Agent bundle, then the deployment default. Every Environment type +// accepts every source, and every Session needs one. Bundles are never merged. func (h *Handler) resolveSessionExecution(ctx context.Context, input sessionRequest, inherited *v1.ModelProviderInput, raw json.RawMessage) (string, *v1.ModelProviderInput, string, uuid.UUID, error) { engine, err := h.sessionHarness(raw) if err != nil { @@ -77,22 +74,11 @@ func (h *Handler) resolveSessionExecution(ctx context.Context, input sessionRequ provider, source = extension.ModelProvider, v1.ModelProviderSourceSession } } - environment := input.Environment.Type - if provider == nil && v1.ModelProviderAllowed(environment, v1.ModelProviderSourceDeployment) { - snapshot := input.deploymentDefaults - if snapshot != nil { - provider, revision = snapshot.Provider, snapshot.Revision - } - source = v1.ModelProviderSourceDeployment + if provider == nil && input.deploymentDefaults != nil { + provider, source, revision = input.deploymentDefaults.Provider, v1.ModelProviderSourceDeployment, input.deploymentDefaults.Revision } if provider == nil { - if v1.ModelProviderRequired(environment) { - return "", nil, "", uuid.Nil, modelProviderRequired(environment, engine) - } - return engine, nil, "", uuid.Nil, nil - } - if !v1.ModelProviderAllowed(environment, source) { - return "", nil, "", uuid.Nil, errors.New("caller model credentials require an openai_hosted or self_hosted environment") + return "", nil, "", uuid.Nil, modelProviderRequired(engine) } if err := provider.ValidateConfiguration(engine, input.resolvedHarnessConfig); err != nil { return "", nil, "", uuid.Nil, err diff --git a/services/core/internal/execution/disabled_tools_test.go b/services/core/internal/execution/disabled_tools_test.go index 53aa85f46..e5ca26517 100644 --- a/services/core/internal/execution/disabled_tools_test.go +++ b/services/core/internal/execution/disabled_tools_test.go @@ -32,13 +32,13 @@ func TestDisabledToolsUseCommonOperationQualification(t *testing.T) { func TestDisabledToolRequestPreservesIntentOnResume(t *testing.T) { for _, disabled := range []bool{false, true} { - snapshot := Snapshot{Agent: v1.Agent{Model: "model"}} + snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model"}} if disabled { snapshot.Agent.Tools = []json.RawMessage{json.RawMessage(`{"type":"programmatic_tool_calling","enabled":false}`)} } before, _ := json.Marshal(snapshot) for _, nativeID := range []string{"", "native-session"} { - request, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "codex"}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) if err != nil || request.ExecutionControls.DisableProgrammaticToolCalling != disabled || request.ExecutionControls.WebSearch != "disabled" || request.AgentSessionID != nativeID { t.Fatal(request, err) } diff --git a/services/core/internal/execution/environment_admission.go b/services/core/internal/execution/environment_admission.go index d078186e7..52d1c3b05 100644 --- a/services/core/internal/execution/environment_admission.go +++ b/services/core/internal/execution/environment_admission.go @@ -87,13 +87,6 @@ func (w *Worker) submitEnvironmentInputs(ctx context.Context, session sessions.S // Neither kind creates a Turn. The Session lock preserves target and retry identity. return w.admitInputs(ctx, session.TenantID, session.ID, key, inputs) } - // Messages start work. A Session from before deployment defaults moved into - // Core may have no frozen provider; reject it here instead of queueing work - // its harness cannot run. Cancellation and results above stay available. - var snapshot Snapshot - if json.Unmarshal(session.Configuration, &snapshot) != nil || !snapshot.ModelProviderConfigured { - return nil, ErrModelProviderRequired - } changed, unsubscribe := w.dispatcher.notifications.subscribe(session.TenantID, session.ID) defer unsubscribe() reserve, cancel := context.WithTimeout(ctx, 5*time.Second) diff --git a/services/core/internal/execution/mcp_support_test.go b/services/core/internal/execution/mcp_support_test.go index ab3901dcf..27a4032ae 100644 --- a/services/core/internal/execution/mcp_support_test.go +++ b/services/core/internal/execution/mcp_support_test.go @@ -29,7 +29,7 @@ func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, runtimede t.Helper() vault, credential := uuid.NewString(), uuid.NewString() tool := json.RawMessage(`{"type":"mcp","server_label":"tickets","connection_origin":"service","transport":{"type":"http","server_url":"https://mcp.example/tools"}}`) - snapshot := Snapshot{Agent: v1.Agent{Model: "model", Tools: []json.RawMessage{tool}}, Environment: &v1.Environment{Type: "none"}, VaultIDs: []string{vault}, + snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model", Tools: []json.RawMessage{tool}}, Environment: &v1.Environment{Type: "none"}, VaultIDs: []string{vault}, MCPCredentials: []vaults.MCPCredentialBinding{{ServerLabel: "tickets", ServerURL: "https://mcp.example/tools", VaultID: vault, CredentialID: credential, AuthType: "static_bearer"}}} tools, err := executionTools(snapshot.Agent.Tools) if err != nil { @@ -57,9 +57,9 @@ func TestMCPPublicBearerPolicyIsIndependentOfRuntimeCapabilities(t *testing.T) { } credentials := &recordingCredentials{token: "scoped-token"} session := sessions.Session{TenantID: uuid.NewString(), Engine: engine} - request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), session, snapshot, caps, sessions.ExecutionBinding{}) + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}, Credentials: credentials}).executionRequest(t.Context(), session, snapshot, caps, sessions.ExecutionBinding{}) if !allowed { - if err == nil || err.Error() != "The configured engine does not support this MCP connection origin." || request.MCPHTTPServers != nil || len(credentials.requests) != 0 { + if err == nil || request.MCPHTTPServers != nil || len(credentials.requests) != 0 { t.Fatal("unverified profile bypassed public policy", err) } return @@ -111,7 +111,7 @@ func TestMCPExecutionChecksRequireVerifiedCapabilityCombinations(t *testing.T) { } if !allowed { credentials := &recordingCredentials{token: "scoped-token"} - request, requestErr := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, caps, sessions.ExecutionBinding{}) + request, requestErr := (&Dispatcher{SessionsReader: frozenProvider{engine: "codex"}, Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, caps, sessions.ExecutionBinding{}) if requestErr == nil || request.MCPHTTPServers != nil || len(credentials.requests) != 0 { t.Fatal("request bypassed capability checks before credential lookup", requestErr) } @@ -143,7 +143,7 @@ func TestMCPAnonymousExecutionPreservesFrozenDecision(t *testing.T) { t.Fatal("anonymous binding validation changed", err) } credentials := &recordingCredentials{token: "scoped-token"} - request, err := (&Dispatcher{Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: engine}, snapshot, caps, sessions.ExecutionBinding{}) + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}, Credentials: credentials}).executionRequest(t.Context(), sessions.Session{Engine: engine}, snapshot, caps, sessions.ExecutionBinding{}) if len(credentials.requests) != 0 { t.Fatal("anonymous or invalid binding reached credential lookup") } diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index 2d4ea6af5..ec08ee9a0 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -1,6 +1,7 @@ package execution import ( + "context" "errors" "strings" "testing" @@ -14,6 +15,20 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) +// frozenProvider reads, for every Session, the bundle a Session of engine +// froze: Responses for Codex, Anthropic otherwise. +type frozenProvider struct { + sessions.Reader + engine string +} + +func (r frozenProvider) SessionModelExecution(context.Context, string, string) (*v1.ModelProviderInput, error) { + if r.engine == "codex" { + return &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.example/v1", APIKey: "key"}, nil + } + return &v1.ModelProviderInput{Protocol: "anthropic", BaseURL: "https://model.example", APIKey: "key"}, nil +} + func TestSessionModelExecutionNeverFallsBack(t *testing.T) { reader, _ := testSessions(t, pgtest.Open(t), nil) d := Dispatcher{SessionsReader: reader} @@ -21,21 +36,13 @@ func TestSessionModelExecutionNeverFallsBack(t *testing.T) { if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("missing Session credentials fell back", err) } - // Hosted and self-hosted Runtimes have no model configuration of their own. - for _, environment := range []string{"openai_hosted", "self_hosted"} { - snapshot := Snapshot{Environment: &v1.Environment{Type: environment}} + // No Runtime has model configuration of its own. + for _, environment := range []string{"openai_hosted", "self_hosted", "none"} { + snapshot := Snapshot{Agent: v1.Agent{Model: "m"}, Environment: &v1.Environment{Type: environment}} if _, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, ErrModelProviderRequired) { t.Fatal("provider-free Session dispatched", environment, err) } } - // A none device without a frozen provider uses its own provider environment: - // Core sends only the Agent's model and instructions. - instructions := "Keep this instruction." - snapshot := Snapshot{Agent: v1.Agent{Model: "device-model", Instructions: &instructions}, Environment: &v1.Environment{Type: "none"}} - request, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}) - if err != nil || request.Model != "device-model" || request.SystemPrompt != instructions || request.ModelProvider != nil || request.HarnessConfig != nil { - t.Fatal("none Session received model settings Core does not own", err) - } } func TestSessionModelProviderPreservesUpstreamBundleForEveryHarness(t *testing.T) { diff --git a/services/core/internal/execution/prepared_dispatch.go b/services/core/internal/execution/prepared_dispatch.go index 6cb5817f6..5bbce163b 100644 --- a/services/core/internal/execution/prepared_dispatch.go +++ b/services/core/internal/execution/prepared_dispatch.go @@ -7,7 +7,6 @@ import ( "strings" "time" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -42,10 +41,6 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t if json.Unmarshal(session.Configuration, &snapshot) != nil || strings.TrimSpace(snapshot.Agent.Model) == "" { return run, sessions.ErrInvalidInput } - if !snapshot.ModelProviderConfigured && snapshot.Environment != nil && v1.ModelProviderRequired(snapshot.Environment.Type) { - // Reserved before providers were required; the caller settles it as failed. - return run, ErrModelProviderRequired - } bound, err := d.SessionsReader.GetSessionExecutionBinding(ctx, tenantID, sessionID) if err != nil { return run, err diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index e71df9b27..97af3a8a8 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -8,12 +8,12 @@ import ( ) func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { - for _, engine := range []string{"codex", "claude_sdk", "future-engine"} { + for _, engine := range []string{"codex", "claude_sdk"} { for _, started := range []bool{false, true} { for _, nativeID := range []string{"", "native"} { for _, capable := range []bool{false, true} { wantRecovery := started && nativeID == "" - req, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) + req, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) if wantRecovery && !capable { if err == nil { t.Fatal("unverified recovery admitted", engine) diff --git a/services/core/internal/execution/request.go b/services/core/internal/execution/request.go index e7c9daa09..14301e1f1 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -4,16 +4,14 @@ import ( "context" "errors" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) -// ErrModelProviderRequired reports a hosted or self-hosted Session that has no -// frozen model provider and therefore cannot run. +// ErrModelProviderRequired reports a stored Session that has no frozen model +// provider and therefore cannot run. var ErrModelProviderRequired = errors.New("the Session has no model provider") func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps runtimedevice.KindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { @@ -25,17 +23,13 @@ func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Sess if err != nil { return proto.PromptRequestPayload{}, err } - var provider *modelprovider.Provider - if snapshot.ModelProviderConfigured { - provider, err = d.sessionModelProvider(ctx, session) - if err != nil { - return proto.PromptRequestPayload{}, err - } - } else if snapshot.Environment != nil && v1.ModelProviderRequired(snapshot.Environment.Type) { - // Require the frozen bundle before dispatch so the harness cannot - // select an implicit provider endpoint. + if !snapshot.ModelProviderConfigured { return proto.PromptRequestPayload{}, ErrModelProviderRequired } + provider, err := d.sessionModelProvider(ctx, session) + if err != nil { + return proto.PromptRequestPayload{}, err + } var harnessConfig proto.HarnessConfig if snapshot.Agent.XAgentsCore != nil { harnessConfig = snapshot.Agent.XAgentsCore.HarnessConfig diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index 0fa4475e9..ec7f7408b 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -31,8 +31,8 @@ func TestStructuredOutputNeedsOperationQualification(t *testing.T) { func TestStructuredOutputRequestKeepsFrozenSchemaAndInstructions(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"number":{"const":9007199254740992}}}`) instructions := "Keep these original instructions." - snapshot := Snapshot{Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} - request, err := (&Dispatcher{}).executionRequest(context.Background(), sessions.Session{}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, sessions.ExecutionBinding{}) + snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "claude_sdk"}}).executionRequest(context.Background(), sessions.Session{Engine: "claude_sdk"}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, sessions.ExecutionBinding{}) if err != nil || request.ExecutionControls.OutputFormat == nil { t.Fatal(err) } diff --git a/services/core/internal/execution/worker.go b/services/core/internal/execution/worker.go index 2ebb2c20f..026a0a015 100644 --- a/services/core/internal/execution/worker.go +++ b/services/core/internal/execution/worker.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "slices" "sync" "time" @@ -127,6 +128,14 @@ func (w *Worker) SubmitInputs(ctx context.Context, tenant, session, key string, if err := w.dispatcher.validateEngineInputs(value.Engine, value.Configuration, inputs); err != nil { return nil, err } + // Messages start work. Every Session freezes a provider at creation, so a + // stored one without it cannot run; reject it instead of queueing work. + // Cancellation and results stay available. + var snapshot Snapshot + if slices.ContainsFunc(inputs, func(input sessions.Input) bool { return input.Kind == "message" }) && + (json.Unmarshal(value.Configuration, &snapshot) != nil || !snapshot.ModelProviderConfigured) { + return nil, ErrModelProviderRequired + } if preparedEnvironmentConfiguration(value.Configuration) { return w.submitEnvironmentInputs(ctx, value, key, inputs) } diff --git a/services/core/internal/sessions/creation.go b/services/core/internal/sessions/creation.go index a970db327..08b31c203 100644 --- a/services/core/internal/sessions/creation.go +++ b/services/core/internal/sessions/creation.go @@ -389,16 +389,12 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro if err := input.ModelProvider.ValidateHarness(engine); err != nil { return NewSession{}, nil, nil, fmt.Errorf("%w: %s", ErrInvalidInput, err) } - var fields map[string]any + // Raw values keep the caller's numbers exact. + var fields map[string]json.RawMessage if json.Unmarshal(configuration, &fields) != nil { return NewSession{}, nil, nil, ErrInvalidInput } - environment, _ := fields["environment"].(map[string]any) - environmentType, _ := environment["type"].(string) - if !v1.ModelProviderAllowed(environmentType, input.ModelProviderSource) { - return NewSession{}, nil, nil, fmt.Errorf("%w: this model provider source is not supported for the Session environment", ErrInvalidInput) - } - fields["model_provider_configured"] = true + fields["model_provider_configured"] = json.RawMessage("true") if configuration, err = json.Marshal(fields); err != nil { return NewSession{}, nil, nil, err } diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go index 2fa58c0da..29257e2be 100644 --- a/services/core/internal/sessions/creation_test.go +++ b/services/core/internal/sessions/creation_test.go @@ -223,11 +223,10 @@ func TestPrepareCreation(t *testing.T) { "configuration over 512K": {func(input *CreateSession) { input.Configuration = json.RawMessage(`"` + strings.Repeat("x", 512*1024) + `"`) }, fingerprints, ErrInvalidInput}, - "configuration array": {func(input *CreateSession) { input.Configuration = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, - "cancel initial input": {func(input *CreateSession) { input.InitialInputs = []Input{cancelInput} }, fingerprints, ErrInvalidInput}, - "deployment self_hosted": {withProvider("self_hosted", "key", v1.ModelProviderSourceDeployment), fingerprints, ErrInvalidInput}, - "no credential key": {withProvider("self_hosted", "key", "session"), unavailable, credentialcrypto.ErrUnavailable}, - "unreadable intent": {func(input *CreateSession) { input.CreationRequest = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, + "configuration array": {func(input *CreateSession) { input.Configuration = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, + "cancel initial input": {func(input *CreateSession) { input.InitialInputs = []Input{cancelInput} }, fingerprints, ErrInvalidInput}, + "no credential key": {withProvider("self_hosted", "key", "session"), unavailable, credentialcrypto.ErrUnavailable}, + "unreadable intent": {func(input *CreateSession) { input.CreationRequest = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, } { t.Run(name, func(t *testing.T) { input := base diff --git a/services/core/internal/sessions/session.go b/services/core/internal/sessions/session.go index eb43abedc..da31907f2 100644 --- a/services/core/internal/sessions/session.go +++ b/services/core/internal/sessions/session.go @@ -46,7 +46,7 @@ type CreateSession struct { DeploymentProviderRevision uuid.UUID `json:"-"` ExecutionConfiguration *v1.SessionExecutionConfiguration ModelProvider *v1.ModelProviderInput - ModelProviderSource string // session, agent or deployment; empty allows only openai_hosted + ModelProviderSource string // session, agent or deployment Initialization environmentconfig.Setup InitialFiles []environmentconfig.InitialFile Creator identity.Subject diff --git a/services/core/tests/integration/agent_execution_defaults_http_test.go b/services/core/tests/integration/agent_execution_defaults_http_test.go index 42c474d7a..d65f1ba26 100644 --- a/services/core/tests/integration/agent_execution_defaults_http_test.go +++ b/services/core/tests/integration/agent_execution_defaults_http_test.go @@ -113,7 +113,6 @@ func TestAgentExecutionDefaultsPublicSnapshotAndPrecedence(t *testing.T) { strings.Replace(replacement, `,"api_key":"override-canary"`, "", 1), strings.Replace(replacement, `"protocol":"responses"`, `"protocol":"unknown"`, 1), strings.Replace(modelOnly, `"model":"model-override"`, `"model":"model-override","x_agents_core":{"harness":"unknown"}`, 1), - strings.TrimSuffix(strings.Replace(body, `"type":"openai_hosted"`, `"type":"none"`, 1), "}") + `,"input":"test"}`, } { call("POST", "/v1/agents/sessions", raw, uuid.NewString(), 400) } @@ -160,13 +159,6 @@ func TestAgentExecutionDefaultsPublicSnapshotAndPrecedence(t *testing.T) { preparedInline := strings.TrimSuffix(inline, "}") + `,"x_agents_core":{"environment":{"env":{"PREPARATION_PROOF":"deployment"}}}}` preparedID := id(call("POST", "/v1/agents/sessions", preparedInline, uuid.NewString(), 201)) assertSnapshot(preparedID, "inline-model", "https://changed.example/v1", "changed-key") - callsBefore := defaultsCalls - selfHosted := strings.Replace(preparedInline, `"type":"openai_hosted"`, `"type":"self_hosted","workspace_directory":"/work"`, 1) - call("POST", "/v1/agents/sessions", selfHosted, uuid.NewString(), 400) - if defaultsCalls != callsBefore { - t.Fatal("preparation extension sent deployment credentials to a user machine") - } - } // An Agent whose saved provider an earlier release accepted but validation diff --git a/services/core/tests/integration/agents_delete_public_test.go b/services/core/tests/integration/agents_delete_public_test.go index 0f37041d4..0db4233c0 100644 --- a/services/core/tests/integration/agents_delete_public_test.go +++ b/services/core/tests/integration/agents_delete_public_test.go @@ -17,20 +17,20 @@ func TestAgentDeletionOfficialClient(t *testing.T) { if python == "" { t.Skip("pinned official Python SDK required") } - s, _ := testStore(t) + s, _ := NewModelTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() - recoveredStore := New(s.pool) - h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) + recoveredStore := NewWithCredentialCipher(s.pool, fixtureCipher) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/agents_update_public_test.go b/services/core/tests/integration/agents_update_public_test.go index 651a044b1..d0f5fc0f7 100644 --- a/services/core/tests/integration/agents_update_public_test.go +++ b/services/core/tests/integration/agents_update_public_test.go @@ -17,20 +17,20 @@ func TestAgentUpdateOfficialClient(t *testing.T) { if python == "" { t.Skip("pinned official Python SDK required") } - s, _ := testStore(t) + s, _ := NewModelTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() - recoveredStore := New(s.pool) - h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) + recoveredStore := NewWithCredentialCipher(s.pool, fixtureCipher) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/claude_execution_test.go b/services/core/tests/integration/claude_execution_test.go index 648969f7c..3c3d2ab68 100644 --- a/services/core/tests/integration/claude_execution_test.go +++ b/services/core/tests/integration/claude_execution_test.go @@ -17,7 +17,7 @@ import ( func claudeSession(t *testing.T, h *dispatchHarness, configuration string, prebound bool) { t.Helper() var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "claude_sdk", IdempotencyKey: "claude", Configuration: json.RawMessage(configuration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "claude_sdk", IdempotencyKey: "claude", Configuration: json.RawMessage(configuration)})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/configuration_validation_public_test.go b/services/core/tests/integration/configuration_validation_public_test.go index e1bbe8f26..ed26e3f67 100644 --- a/services/core/tests/integration/configuration_validation_public_test.go +++ b/services/core/tests/integration/configuration_validation_public_test.go @@ -30,7 +30,7 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/deployment_model_providers_http_test.go b/services/core/tests/integration/deployment_model_providers_http_test.go index 0ee2bec8e..059160423 100644 --- a/services/core/tests/integration/deployment_model_providers_http_test.go +++ b/services/core/tests/integration/deployment_model_providers_http_test.go @@ -54,7 +54,7 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { r.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, r) - for _, secret := range []string{"deployment-canary", "session-canary", "agent-canary", "invalid-canary", `"api_key":`} { + for _, secret := range []string{"deployment-canary", "changed-canary", "session-canary", "agent-canary", "invalid-canary", `"api_key":`} { if strings.Contains(w.Body.String(), secret) { t.Fatalf("%s %s returned a key", method, path) } @@ -123,11 +123,15 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { } call("PUT", "/core/v1/harnesses/mcode/model-configuration", coreKey, `{"model":"fixture","model_provider":{"protocol":"anthropic","base_url":"https://deployment.example/anthropic","api_key":"invalid-canary"}}`, 400) - // Without any provider, hosted and self-hosted creation fail before any write. - hosted := `{"agent":{"model":"hosted-model"},"environment":{"type":"openai_hosted"}}` - selfHosted := `{"agent":{"model":"self-hosted-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}` - for _, body := range []string{hosted, selfHosted} { - failure := call("POST", "/v1/agents/sessions", projectKey, body, 400) + // Without any provider, creation fails before any write in every Environment type. + environments := map[string]string{ + "openai_hosted": `"environment":{"type":"openai_hosted"}`, + "self_hosted": `"environment":{"type":"self_hosted","workspace_directory":"/workspace"}`, + "none": `"environment":{"type":"none"},"input":"hello"`, + } + hosted := `{"agent":{"model":"hosted-model"},` + environments["openai_hosted"] + `}` + for _, environment := range environments { + failure := call("POST", "/v1/agents/sessions", projectKey, `{"agent":{"model":"m"},`+environment+`}`, 400) if !strings.Contains(string(failure["error"]), `"code":"model_provider_required","param":"x_agents_core.model_provider"`) { t.Fatalf("unclear failure: %s", failure["error"]) } @@ -196,24 +200,25 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { if err != nil || projection.ModelProvider.Source != "deployment" || projection.ModelProvider.Status != "available" || projection.ModelProvider.Configuration == nil || projection.ModelProvider.Configuration.BaseURL != "https://deployment.example/api" { t.Fatal("deployment selection not recorded", projection, err) } - call("PUT", path, coreKey, strings.Replace(codexDefault, "deployment.example", "changed.example", 1), 200) + call("PUT", path, coreKey, strings.Replace(strings.Replace(codexDefault, "deployment.example", "changed.example", 1), "deployment-canary", "changed-canary", 1), 200) if providerOf(hostedID) != "deployment-canary" { t.Fatal("a changed default reached an existing Session") } - // Self-hosted Sessions take the request or saved Agent bundle, never the default. - failure := call("POST", "/v1/agents/sessions", projectKey, selfHosted, 400) - if !strings.Contains(string(failure["error"]), "never to self_hosted") { - t.Fatalf("self-hosted Session used or misreported the deployment default: %s", failure["error"]) - } - requestProvider := strings.TrimSuffix(selfHosted, "}") + `,"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://session.example/v1","api_key":"session-canary"}}}` - if id := text(call("POST", "/v1/agents/sessions", projectKey, requestProvider, 201)["id"]); providerOf(id) != "session-canary" { - t.Fatal("self-hosted Session lost its request provider") - } + // Every Environment type accepts every source and freezes it. agentID := text(call("POST", "/v1/agents", projectKey, `{"model":"agent-model","x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://agent.example/v1","api_key":"agent-canary"}}}`, 201)["id"]) - fromAgent := `{"agent_id":"` + agentID + `","environment":{"type":"self_hosted","workspace_directory":"/workspace"}}` - if id := text(call("POST", "/v1/agents/sessions", projectKey, fromAgent, 201)["id"]); providerOf(id) != "agent-canary" { - t.Fatal("self-hosted Session lost its saved Agent provider") + for name, environment := range environments { + for _, tc := range []struct{ source, body, key string }{ + {"session", `{"agent":{"model":"m"},` + environment + `,"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://session.example/v1","api_key":"session-canary"}}}`, "session-canary"}, + {"agent", `{"agent_id":"` + agentID + `",` + environment + `}`, "agent-canary"}, + {"deployment", `{"agent":{"model":"m"},` + environment + `}`, "changed-canary"}, + } { + id := text(call("POST", "/v1/agents/sessions", projectKey, tc.body, 201)["id"]) + projection, err := sessionAdapter(st).GetSessionExecutionConfiguration(t.Context(), tenant, id) + if providerOf(id) != tc.key || err != nil || projection.ModelProvider.Source != tc.source { + t.Fatal("Session did not freeze its provider", name, tc.source, err) + } + } } // Removal is idempotent and audited; hosted creation then fails fast again. @@ -230,9 +235,9 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { } } -// A hosted or self-hosted Session created before providers were required has -// no frozen provider: new work is rejected before anything is queued, and input -// reserved before the upgrade fails with that reason instead of waiting. +// A stored Session without a frozen provider cannot run: new work is rejected +// before anything is queued, and input already reserved fails with that reason +// instead of waiting. func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { h := newDispatchHarnessForSession(t, []byte(`{"agent":{"model":"test-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), true) legacy, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), @@ -262,6 +267,14 @@ func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { if _, err := worker.SubmitInputs(t.Context(), h.tenant, legacy.ID, uuid.NewString(), message); !errors.Is(err, execution.ErrModelProviderRequired) { t.Fatal("provider-free Session accepted work", err) } + none, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)}) + if err != nil { + t.Fatal(err) + } + if _, err := worker.SubmitInputs(t.Context(), h.tenant, none.ID, uuid.NewString(), message); !errors.Is(err, execution.ErrModelProviderRequired) { + t.Fatal("provider-free none Session accepted work", err) + } if after := reservations(); after != before { t.Fatal("rejected work was queued", before, after) } @@ -285,10 +298,10 @@ func TestLegacySessionWithoutProviderCannotStartWork(t *testing.T) { } } -// A none Session may freeze the deployment default, so its caller intent is -// recorded first: a same-key retry returns the committed Session after the -// default was replaced or removed. -func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { +// A Session may freeze the deployment default, so its caller intent is recorded +// first: a same-key retry returns the committed Session after the default was +// replaced or removed. +func TestSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { st, _ := NewModelTestStore(t) if _, err := st.pool.Exec(t.Context(), "DELETE FROM deployment_model_providers"); err != nil { t.Fatal(err) @@ -307,13 +320,9 @@ func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { t.Fatal(err) } } - create := func(key string, agent ...string) string { + create := func(key string) string { t.Helper() - body := `{"agent":{"model":"m"},"environment":{"type":"none"},"input":"hello"}` - if len(agent) > 0 { - body = `{"agent":` + agent[0] + `,"environment":{"type":"none"},"input":"hello"}` - } - r := httptest.NewRequest("POST", "/v1/agents/sessions", strings.NewReader(body)) + r := httptest.NewRequest("POST", "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"m"},"environment":{"type":"none"},"input":"hello"}`)) r.Header.Set("Authorization", "Bearer "+token) r.Header.Set("OpenAI-Beta", "agents=v1") r.Header.Set("Content-Type", "application/json") @@ -337,7 +346,7 @@ func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { t.Fatal("none Session did not freeze the deployment default", err) } setDefault("rotated-default-key") - if create(key) != original || create(key, `{"model":"m","text":{"verbosity":"medium"}}`) != original { + if create(key) != original { t.Fatal("retry after rotation created another Session") } if err := defaults.Delete(admin, "codex"); err != nil { diff --git a/services/core/tests/integration/fixtures_test.go b/services/core/tests/integration/fixtures_test.go index 6fecfab2c..ecf898f09 100644 --- a/services/core/tests/integration/fixtures_test.go +++ b/services/core/tests/integration/fixtures_test.go @@ -29,9 +29,8 @@ func NewModelTestStore(t *testing.T) (*Store, *pgxpool.Pool) { return NewWithCredentialCipher(pool, fixtureCipher), pool } -// FixtureModelProvider is a valid bundle for the harness. Hosted and self-hosted -// Sessions cannot run without one, so fixtures supply it instead of relaxing -// that check. +// FixtureModelProvider is a valid bundle for the harness. No Session runs +// without one, so fixtures supply it instead of relaxing that check. func FixtureModelProvider(harness string) *v1.ModelProviderInput { if harness == "codex" { return &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-model-key"} @@ -40,15 +39,9 @@ func FixtureModelProvider(harness string) *v1.ModelProviderInput { } // WithFixtureModelProvider adds the fixture provider, as a Session-supplied -// bundle, to a hosted or self-hosted creation that has none. The store must -// have a credential key; other inputs are returned unchanged. +// bundle, to a creation that has none. The store must have a credential key. func WithFixtureModelProvider(input sessions.CreateSession) sessions.CreateSession { - var configuration struct { - Environment struct { - Type string `json:"type"` - } `json:"environment"` - } - if input.ModelProvider != nil || json.Unmarshal(input.Configuration, &configuration) != nil || !v1.ModelProviderRequired(configuration.Environment.Type) { + if input.ModelProvider != nil { return input } input.ModelProvider, input.ModelProviderSource = FixtureModelProvider(input.Engine), v1.ModelProviderSourceSession diff --git a/services/core/tests/integration/function_execution_test.go b/services/core/tests/integration/function_execution_test.go index 16b67e438..73a8ed394 100644 --- a/services/core/tests/integration/function_execution_test.go +++ b/services/core/tests/integration/function_execution_test.go @@ -20,7 +20,7 @@ func newFunctionHarness(t *testing.T) *dispatchHarness { t.Helper() h := newDispatchHarness(t) var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)})) if err != nil { t.Fatal(err) } @@ -189,7 +189,7 @@ func TestExecutionFunctionsRejectUndeclaredCallsAndPrematureDone(t *testing.T) { func TestExecutionFunctionsRequireAdvertisedCapability(t *testing.T) { h := newDispatchHarness(t) - session, err := h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)}) + session, err := h.s.CreateSession(t.Context(), h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "functions", Configuration: json.RawMessage(functionConfiguration)})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/function_worker_test.go b/services/core/tests/integration/function_worker_test.go index 916f4f113..71582ac3d 100644 --- a/services/core/tests/integration/function_worker_test.go +++ b/services/core/tests/integration/function_worker_test.go @@ -36,7 +36,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { } if !prebound || isMCP { var err error - h.session, err = h.s.CreateSession(t.Context(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "unbound", Configuration: []byte(configuration)}) + h.session, err = h.s.CreateSession(t.Context(), h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "unbound", Configuration: []byte(configuration)})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/harness_onboarding_test.go b/services/core/tests/integration/harness_onboarding_test.go index b655f11cb..6272a3f57 100644 --- a/services/core/tests/integration/harness_onboarding_test.go +++ b/services/core/tests/integration/harness_onboarding_test.go @@ -42,7 +42,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } return nil } - policy := execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"fixture_harness": profile})} + policy := execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"mcode": profile})} h.d.Policy = policy // The fixture only supplies an adapter and registration to the real daemon router. // Core sees its ordinary authenticated gateway connection and neutral frames. @@ -62,7 +62,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { }() token := uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) - handler, err := publicHandler(t, h.s, auth, "fixture_harness", workerExecution(t, worker), withPolicy(policy)) + handler, err := publicHandler(t, h.s, auth, "mcode", workerExecution(t, worker), withPolicy(policy), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { t.Fatal(err) } first := awaitOnboardingPrompt(t, started) - if first.AgentKind != "fixture_harness" || first.AgentSessionID != "" { + if first.AgentKind != "mcode" || first.AgentSessionID != "" { t.Fatal(first) } request("POST", "/v1/agents/sessions/"+created.ID+"/events", `{"events":[{"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"finish"}]}]}]}`, 202) @@ -133,7 +133,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { t.Fatal(err) } for deadline := time.Now().Add(3 * time.Second); ; { - current, _, _ := peer.AgentKindStatus("fixture_harness") + current, _, _ := peer.AgentKindStatus("mcode") if !current.Capabilities.DurableInputReceipts { break } @@ -215,7 +215,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR return } for _, info := range heartbeat.SupportedAgentKinds { - if info.Kind == "fixture_harness" { + if info.Kind == "mcode" { select { case declarations <- info: default: @@ -298,7 +298,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR for { peer, err := h.registry.LookupDevice(h.device.ID) if err == nil { - _, found, known := peer.AgentKindStatus("fixture_harness") + _, found, known := peer.AgentKindStatus("mcode") if found && known { return started, write, <-declarations } diff --git a/services/core/tests/integration/input_conflicts_public_test.go b/services/core/tests/integration/input_conflicts_public_test.go index 41578612f..3dc0a1b9f 100644 --- a/services/core/tests/integration/input_conflicts_public_test.go +++ b/services/core/tests/integration/input_conflicts_public_test.go @@ -44,7 +44,7 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "conflict-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "conflict-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example"), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/list_cursor_public_test.go b/services/core/tests/integration/list_cursor_public_test.go index 9cf08ae1a..2378b7229 100644 --- a/services/core/tests/integration/list_cursor_public_test.go +++ b/services/core/tests/integration/list_cursor_public_test.go @@ -233,7 +233,7 @@ func TestListCursorErrorsPostgres(t *testing.T) { {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/mcp_credential_selection_public_test.go b/services/core/tests/integration/mcp_credential_selection_public_test.go index b047c7608..20a6b372e 100644 --- a/services/core/tests/integration/mcp_credential_selection_public_test.go +++ b/services/core/tests/integration/mcp_credential_selection_public_test.go @@ -36,7 +36,7 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) { {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-a", TokenSHA256: runtimedevice.HashCredential(tokenA), TenantID: tenantA}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-b", TokenSHA256: runtimedevice.HashCredential(tokenB), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } @@ -171,12 +171,7 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) { t.Fatalf("Session origin case %d: %d %s", index, created.status, created.body) } } - // A Session created with an explicit origin, as before this change, recovers - // from a same-key retry that omits it: the resolved configuration is equal. - if retry := send(tokenA, http.MethodPost, "/v1/agents/sessions", inline(tool("records", url, ""), ""), "origin-explicit"); retry.status != http.StatusCreated || retry.body != explicitSession.body { - t.Fatal("same-key retry without origin", retry.status, retry.body) - } - // Recorded caller intent (attached Vaults) keeps comparing the request itself. + // Recorded caller intent compares the request itself. attachedExplicit := send(tokenA, http.MethodPost, "/v1/agents/sessions", inline(tool("records", url, `,"connection_origin":"service"`), vaults(attachedA)), "origin-attached") if attachedExplicit.status != http.StatusCreated { t.Fatal("attached explicit origin", attachedExplicit.status, attachedExplicit.body) diff --git a/services/core/tests/integration/model_provider_fixture_test.go b/services/core/tests/integration/model_provider_fixture_test.go index c70091f34..5dbbf8d7e 100644 --- a/services/core/tests/integration/model_provider_fixture_test.go +++ b/services/core/tests/integration/model_provider_fixture_test.go @@ -10,9 +10,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" ) -// Hosted and self-hosted Sessions must freeze a model provider. HTTP fixtures -// supply one here instead of relaxing that check; the store needs a credential -// key (NewModelTestStore). +// Every Session must freeze a model provider. HTTP fixtures supply one here +// instead of relaxing that check; the store needs a credential key +// (NewModelTestStore). // fixtureDeploymentProvider configures a deployment default for every harness. func fixtureDeploymentProvider() func(*api.Dependencies) { @@ -22,7 +22,7 @@ func fixtureDeploymentProvider() func(*api.Dependencies) { } // fixtureSessionProvider is the top-level Session request member that supplies -// the harness's fixture bundle, for self-hosted Sessions. +// the harness's fixture bundle. func fixtureSessionProvider(harness string) string { raw, _ := json.Marshal(map[string]any{"model_provider": FixtureModelProvider(harness)}) return `"x_agents_core":` + string(raw) diff --git a/services/core/tests/integration/path_id_semantics_public_test.go b/services/core/tests/integration/path_id_semantics_public_test.go index ebb0b24df..b2ddb9dc3 100644 --- a/services/core/tests/integration/path_id_semantics_public_test.go +++ b/services/core/tests/integration/path_id_semantics_public_test.go @@ -98,7 +98,7 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) { {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/public_execution_test.go b/services/core/tests/integration/public_execution_test.go index 2546c3d55..a51476e35 100644 --- a/services/core/tests/integration/public_execution_test.go +++ b/services/core/tests/integration/public_execution_test.go @@ -14,7 +14,7 @@ import ( func publicSession(t *testing.T, h *dispatchHarness, key string) sessions.Session { t.Helper() - value, err := h.s.CreateSession(context.Background(), h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"test-model","instructions":"Keep this."},"environment":{"type":"none"}}`)}) + value, err := h.s.CreateSession(context.Background(), h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: key, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"test-model","instructions":"Keep this."},"environment":{"type":"none"}}`)})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/request_body_public_test.go b/services/core/tests/integration/request_body_public_test.go index f075d4ce3..b61d39393 100644 --- a/services/core/tests/integration/request_body_public_test.go +++ b/services/core/tests/integration/request_body_public_test.go @@ -37,7 +37,7 @@ func TestRequestBodyGateRejectsWithoutWritesPostgres(t *testing.T) { }) // No Runtime is connected, so a file write that passes the gate is unavailable. unavailable := func(d *api.Dependencies) { d.Execution.Workspaces = unavailableWorkspaces{strictStandIn{t}} } - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), unavailable, acceptUnavailable(t)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), unavailable, acceptUnavailable(t), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/saved_web_search_public_test.go b/services/core/tests/integration/saved_web_search_public_test.go index dd31102ac..9f358f797 100644 --- a/services/core/tests/integration/saved_web_search_public_test.go +++ b/services/core/tests/integration/saved_web_search_public_test.go @@ -25,7 +25,7 @@ func TestSavedWebSearchPostgres(t *testing.T) { {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/session_agent_filter_public_test.go b/services/core/tests/integration/session_agent_filter_public_test.go index e6239c311..63dcbfe98 100644 --- a/services/core/tests/integration/session_agent_filter_public_test.go +++ b/services/core/tests/integration/session_agent_filter_public_test.go @@ -17,20 +17,20 @@ func TestSessionAgentFilterOfficialClient(t *testing.T) { if python == "" { t.Skip("pinned official Python SDK required") } - s, _ := testStore(t) + s, _ := NewModelTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() - recoveredStore := New(s.pool) - h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) + recoveredStore := NewWithCredentialCipher(s.pool, fixtureCipher) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/session_deletion_public_test.go b/services/core/tests/integration/session_deletion_public_test.go index 65fae5e91..4856f2ea8 100644 --- a/services/core/tests/integration/session_deletion_public_test.go +++ b/services/core/tests/integration/session_deletion_public_test.go @@ -17,19 +17,20 @@ func TestSessionDeletionOfficialClient(t *testing.T) { if python == "" { t.Skip("pinned official Python SDK required") } - s, _ := testStore(t) + s, _ := NewModelTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() - h, err = publicHandler(t, New(s.pool), auth, "codex", storeExecution(t, New(s.pool))) + recoveredStore := NewWithCredentialCipher(s.pool, fixtureCipher) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/session_reference_retry_public_test.go b/services/core/tests/integration/session_reference_retry_public_test.go index 5ee70df0a..1f4f6c44e 100644 --- a/services/core/tests/integration/session_reference_retry_public_test.go +++ b/services/core/tests/integration/session_reference_retry_public_test.go @@ -20,7 +20,7 @@ func TestSavedReferenceRetryOfficialClient(t *testing.T) { if python == "" { t.Skip("pinned official Python SDK required") } - s, _ := testStore(t) + s, _ := NewModelTestStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) worker := startWorker(t, t.Context(), s, &execution.Dispatcher{}) @@ -31,13 +31,13 @@ func TestSavedReferenceRetryOfficialClient(t *testing.T) { t.Error(err) } }) - handler, err := publicHandler(t, s, auth, "codex", workerExecution(t, worker)) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(t, worker), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } server := httptest.NewServer(handler) defer server.Close() - recovered, err := publicHandler(t, New(s.pool), auth, "codex") + recovered, err := publicHandler(t, NewWithCredentialCipher(s.pool, fixtureCipher), auth, "codex", fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/subagent_dispatch_test.go b/services/core/tests/integration/subagent_dispatch_test.go index e56fa1df2..58ad23e75 100644 --- a/services/core/tests/integration/subagent_dispatch_test.go +++ b/services/core/tests/integration/subagent_dispatch_test.go @@ -19,7 +19,7 @@ func TestSubagentIdentityUsesLeasedDispatchJournal(t *testing.T) { "environment": map[string]string{"type": "none"}, }) var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "identity-dispatch", Configuration: configuration}) + h.session, err = h.s.CreateSession(ctx, h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "identity-dispatch", Configuration: configuration})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/subagent_visibility_public_test.go b/services/core/tests/integration/subagent_visibility_public_test.go index b6b7ec69e..2cec8f53b 100644 --- a/services/core/tests/integration/subagent_visibility_public_test.go +++ b/services/core/tests/integration/subagent_visibility_public_test.go @@ -70,13 +70,13 @@ func subagentFixture(kind string, value any) sessions.ExecutionEvent { // routes with the Session's Agent ID, Subagent lists use the common envelope and // child Item lists clamp their limit. Tenant B sees none of it. func TestSubagentVisibilityPublic(t *testing.T) { - s, _ := testStore(t) + s, _ := NewModelTestStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/unified_model_configuration_http_test.go b/services/core/tests/integration/unified_model_configuration_http_test.go index 3f93c9e13..522a92f9b 100644 --- a/services/core/tests/integration/unified_model_configuration_http_test.go +++ b/services/core/tests/integration/unified_model_configuration_http_test.go @@ -141,21 +141,14 @@ func TestUnifiedModelConfigurationHTTP(t *testing.T) { {"explicit empty inline parameters", `{"agent":{"x_agents_core":{"harness_config":{}}},"environment":{"type":"openai_hosted"}}`, "model-replacement", "deployment", "deployment", "deployment-canary"}, {"explicit empty Session parameters", `{"agent":{},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{}}}`, "model-replacement", "deployment", "deployment", "deployment-canary"}, } { - t.Run(tc.name, func(t *testing.T) { - id := create(tc.body, uuid.NewString()) - assertSession(id, tc.model, `{}`, tc.modelSource, "session", tc.providerSource, tc.key) - }) - } - const selfHosted = `{"agent":{"model":"self-model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}` - failure := object(call("POST", "/v1/agents/sessions", token, selfHosted, uuid.NewString(), 400)) - if !strings.Contains(string(failure["error"]), `"code":"model_provider_required"`) { - t.Fatal("self-hosted provider error was not explicit") + // self_hosted resolves deployment defaults exactly as openai_hosted does. + for _, environment := range []string{`{"type":"openai_hosted"}`, `{"type":"self_hosted","workspace_directory":"/workspace"}`} { + t.Run(tc.name+" "+environment, func(t *testing.T) { + id := create(strings.Replace(tc.body, `{"type":"openai_hosted"}`, environment, 1), uuid.NewString()) + assertSession(id, tc.model, `{}`, tc.modelSource, "session", tc.providerSource, tc.key) + }) + } } - selfExplicit := strings.TrimSuffix(selfHosted, "}") + `,"x_agents_core":{"model_provider":` + explicitProvider + `}}` - selfID := create(selfExplicit, uuid.NewString()) - assertSession(selfID, "self-model", `{}`, "session", "session", "session", "explicit-canary") - selfWithoutModel := `{"agent":{},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"x_agents_core":{"model_provider":` + explicitProvider + `}}` - call("POST", "/v1/agents/sessions", token, selfWithoutModel, uuid.NewString(), 400) // Saved Agent changes must discard parameters belonging to the former selection. for _, tc := range []struct{ name, update string }{ diff --git a/services/core/tests/integration/unstorable_text_public_test.go b/services/core/tests/integration/unstorable_text_public_test.go index 01f1f9804..56d4b895a 100644 --- a/services/core/tests/integration/unstorable_text_public_test.go +++ b/services/core/tests/integration/unstorable_text_public_test.go @@ -27,7 +27,7 @@ func TestUnstorableTextRejectsWithoutWritesPostgres(t *testing.T) { s := NewWithCredentialCipher(pool, cipher) token := uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "nul-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/whitespace_input_public_test.go b/services/core/tests/integration/whitespace_input_public_test.go index 3954c0b49..df4e51c89 100644 --- a/services/core/tests/integration/whitespace_input_public_test.go +++ b/services/core/tests/integration/whitespace_input_public_test.go @@ -22,7 +22,7 @@ func TestWhitespaceInputStoredVerbatimPostgres(t *testing.T) { s, _ := newManagedTestStore(t) token := uuid.NewString() auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) - h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } @@ -106,7 +106,7 @@ func TestWhitespaceOnlyTextHarnessAdmissionPostgres(t *testing.T) { } }) serve := func(engine string) pathIDClient { - handler, err := publicHandler(t, s, auth, engine, workerExecution(t, worker), executorURL("https://offline-executor.example")) + handler, err := publicHandler(t, s, auth, engine, workerExecution(t, worker), executorURL("https://offline-executor.example"), fixtureDeploymentProvider()) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/worker_input_race_test.go b/services/core/tests/integration/worker_input_race_test.go index 76030a770..ba17a919c 100644 --- a/services/core/tests/integration/worker_input_race_test.go +++ b/services/core/tests/integration/worker_input_race_test.go @@ -59,7 +59,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) { defer instrumented.Close() ctx, cancel := context.WithCancel(t.Context()) defer cancel() - worker := startWorker(t, ctx, New(instrumented), h.d) + worker := startWorker(t, ctx, NewWithCredentialCipher(instrumented, fixtureCipher), h.d) done := make(chan error, 1) go func() { done <- worker.Run(ctx) }() defer func() { @@ -83,7 +83,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) { t.Fatal("candidate was not cancelled", err) } // A later Session must still execute through this same Worker. - h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "healthy", Configuration: h.session.Configuration}) + h.session, err = h.s.CreateSession(ctx, h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "healthy", Configuration: h.session.Configuration})) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/official_client.py b/services/core/tests/official_client.py index d5ad580e0..4a277a98d 100644 --- a/services/core/tests/official_client.py +++ b/services/core/tests/official_client.py @@ -165,6 +165,14 @@ def issue_key(project_id, name): assert issued["project_id"] == project_id return issued["key"] + # Core admits a Session only with a model provider; nothing here calls these. + for harness, provider in (("codex", {"protocol": "responses", "base_url": "https://model.fixture.example/v1"}), + ("claude_sdk", {"protocol": "anthropic", "base_url": "https://model.fixture.example/anthropic", + "context_window": 200000, "max_output_tokens": 8000})): + response = admin.put(f"harnesses/{harness}/model-configuration", + json={"model": "deployment-test-model", "model_provider": {**provider, "api_key": "fixture-model-key"}}) + assert response.status_code == 200, "Fixture model configuration failed" + for index in range(4): response = admin.post("projects", json={"name": f"Official SDK fixture {index}"}) assert response.status_code == 201, "Fixture Project creation failed" diff --git a/services/core/tests/official_execution.py b/services/core/tests/official_execution.py index 61f2c1963..27f828500 100644 --- a/services/core/tests/official_execution.py +++ b/services/core/tests/official_execution.py @@ -156,8 +156,6 @@ def about_answer(value): agent = {"model": "gpt-5.5", "text": {"verbosity": verbosity, "format": {"type": "text"}}} key = "text-" + verbosity configured = sessions.create(agent=agent, environment={"type": "none"}, input="TEXT-VERBOSITY:" + verbosity, extra_headers={"Idempotency-Key": key}) - agent["text"]["format"] = None - assert sessions.create(agent=agent, environment={"type": "none"}, input="TEXT-VERBOSITY:" + verbosity, extra_headers={"Idempotency-Key": key}).id == configured.id assert configured.agent.text.model_dump() == {"format": {"type": "text"}, "verbosity": verbosity} for count in (1, 2): if count > 1: @@ -173,10 +171,14 @@ def about_answer(value): default_agent = {"model": "custom-provider-model"} default = sessions.create(agent=default_agent, environment={"type": "none"}, input="DEFAULT-VERBOSITY", extra_headers={"Idempotency-Key": "native-default"}) + # A retry is compared as sent, so spelling out the default conflicts. for text in (None, {"verbosity": None}, {"verbosity": "medium"}): - configured = sessions.create(agent=dict(default_agent, text=text), environment={"type": "none"}, - input="DEFAULT-VERBOSITY", extra_headers={"Idempotency-Key": "native-default"}) - assert configured.id == default.id and configured.agent.text.verbosity == "medium" + try: + sessions.create(agent=dict(default_agent, text=text), environment={"type": "none"}, + input="DEFAULT-VERBOSITY", extra_headers={"Idempotency-Key": "native-default"}) + raise AssertionError("a retry that spells out the default reused the key") + except ConflictError: + pass for count in (1, 2): if count > 1: sessions.events.create(default.id, events=[message("DEFAULT-VERBOSITY")])