diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5dee072e..acd2a271e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,8 +18,7 @@ This guide owns how to work in the repository: documentation ownership, the repo | Core implementation constraints beyond the public contracts | [Implementation constraints](services/core/IMPLEMENTATION.md) | | Environment ownership, preparation, Skills, Plugins, packages and MCP bindings | [Environments](contracts/agents-api/environments.md) | | Built-in Harness identifiers and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) | -| Harness registration, service qualification and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) | -| Harness capabilities by placement | [Harness capabilities](contracts/agents-api/harness-capabilities.md) | +| Harness registration, support declaration and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) | | Harness selection, model providers and native parameters | [Model execution](contracts/agents-api/model-execution.md) | | Provider registration and lifecycle | [Sandbox Provider guide](docs/sandbox-provider.md) | | Sandbox deployment, selection and administrative transitions | [Sandbox deployment](contracts/agents-api/sandbox-deployment.md) | diff --git a/README.md b/README.md index d8056f965..290a46d42 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The [installation guide](https://openagentcore.dev/docs/getting-started/install) | Build an application on the API | [Quickstart](https://openagentcore.dev/docs/getting-started/quickstart), then the [Agents API guide](https://openagentcore.dev/docs/api/public-agent-api) | | See a complete application | [Examples](https://openagentcore.dev/docs/examples) | | Run agents on my own machine | [Self-hosted execution](https://openagentcore.dev/docs/getting-started/self-hosted) | -| Check Harness capabilities and limits | [Harness capabilities](https://openagentcore.dev/contracts/agents-api/harness-capabilities) | +| Check Harness differences and limits | [Known gaps](https://openagentcore.dev/contracts/agents-api/#known-gaps) | | Understand the design | [Architecture](https://openagentcore.dev/docs/architecture) | | Add a sandbox, harness or other component | [Developer guide](https://openagentcore.dev/docs/development) | diff --git a/README.zh-CN.md b/README.zh-CN.md index fb55a62a5..ddf6f2a23 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -71,7 +71,7 @@ irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install | 基于 API 开发应用 | [快速开始](https://openagentcore.dev/zh/docs/getting-started/quickstart),然后看 [Agents API 指南](https://openagentcore.dev/zh/docs/api/public-agent-api) | | 看一个完整的应用 | [示例](https://openagentcore.dev/zh/docs/examples) | | 在自己的机器上运行 Agent | [自托管执行](https://openagentcore.dev/zh/docs/getting-started/self-hosted) | -| 查看 Harness 能力和限制 | [Harness 能力](https://openagentcore.dev/zh/contracts/agents-api/harness-capabilities) | +| 查看 Harness 差异和限制 | [已知缺口](https://openagentcore.dev/zh/contracts/agents-api/#known-gaps) | | 了解设计 | [架构说明](https://openagentcore.dev/zh/docs/architecture) | | 接入新的沙箱、Harness 或其他组件 | [开发指南](https://openagentcore.dev/zh/docs/development) | 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 57aec4a38..7000d0172 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go @@ -66,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, 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."} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{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 := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index 4c4334ec1..2717c58d8 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -18,27 +18,11 @@ import ( const claudeSDKEntrypointEnv = "OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT" const claudeSDKNodeEnv = "OAC_RUNTIME_CLAUDE_SDK_NODE" -// Declaration owns Claude SDK discovery, configuration and execution factories. -var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{ - SubagentObservations: proto.CapabilityUnsupported, - NativeSessionRecovery: proto.CapabilityUnsupported, - EnvironmentNone: proto.CapabilitySupported, - LocalEnvironment: proto.CapabilityUnsupported, - WorkspaceReadPreparation: proto.CapabilityUnsupported, - WorkspaceOutputExport: proto.CapabilityUnsupported, - ProgrammaticToolCallingDisable: proto.CapabilitySupported, - WebSearchControl: proto.CapabilityUnsupported, - TextVerbosity: proto.CapabilityUnsupported, - StructuredOutput: proto.CapabilityUnsupported, - ToolSearch: proto.CapabilityUnsupported, - MessageImages: proto.CapabilityUnsupported, - FunctionResultImages: proto.CapabilityUnsupported, - SubagentControl: proto.CapabilitySupported, - FunctionTools: proto.CapabilitySupported, - MCPHTTPTools: proto.CapabilityUnsupported, - MCPHTTPRequired: proto.CapabilityUnsupported, - MCPHTTPBearerAuth: proto.CapabilityUnsupported, -}}, Configuration: configuration.Configuration(), Discover: discover} +// Declaration owns Claude SDK discovery, configuration and execution +// factories. Discovery narrows the declared support to what the installed +// bundle serves. +var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: configuration.Configuration().Declaration.Capabilities}, + Configuration: configuration.Configuration(), Discover: discover} func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime { return discoverWithCheck(ctx, options, info, CheckRuntime) @@ -100,13 +84,11 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d if err != nil { return fail(err) } - caps := &out.Info.Capabilities - if config.Workspace != nil { - if !info.SupportsLocalRuntime() { - return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract")) - } - caps.NativeSessionRecovery = proto.CapabilitySupported + if config.Workspace != nil && !info.SupportsLocalRuntime() { + return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract")) } + caps := &out.Info.Capabilities + caps.NativeSessionRecovery = proto.CapabilityFromBool(config.Workspace != nil) // One declaration holds for every Executor of the install: the workspace // bridge, the agent-host view and a Runtime without a workspace, so each // feature is its workspace variant, which the others also support. diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index 330aa96fd..d6988adc9 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -6,13 +6,11 @@ import ( "io" "os" "path/filepath" - "reflect" "slices" "strings" "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func TestClaudeSDKInvalidPathsFailBeforeProbe(t *testing.T) { @@ -66,25 +64,6 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) { } } -// The declaration must retain the complete baseline capability descriptor. -func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "SubagentControl": true, "FunctionTools": true} - value := reflect.ValueOf(Declaration.Info.Capabilities) - for i := 0; i < value.NumField(); i++ { - name := value.Type().Field(i).Name - want := proto.CapabilityUnsupported - if expected[name] { - want = proto.CapabilitySupported - } - if got := value.Field(i).Interface(); got != want { - t.Errorf("%s = %v, want %v", name, got, want) - } - } - if err := Declaration.Info.ValidateDeclaration(); err != nil { - t.Fatal(err) - } -} - func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) @@ -118,7 +97,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { } continue } - if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || runtime.Info.Capabilities.LocalEnvironment.IsSupported() { + if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || ready && runtime.Info.Capabilities.LocalEnvironment.IsSupported() { t.Fatalf("runtime: %+v", runtime) } } diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index e2ea827c3..269098a48 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -22,7 +22,7 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { if err != nil { t.Fatal(err) } - request.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"} + request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium"} before, _ := json.Marshal(request) controlled, _, err := prepareConfiguration(config, request) if err != nil { @@ -34,33 +34,6 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { } } -func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) { - cases := map[string]proto.ExecutionControls{ - "empty": {}, "missing-search": {TextVerbosity: "medium"}, "missing-verbosity": {WebSearch: "disabled"}, - "cached-search": {WebSearch: "cached", TextVerbosity: "medium"}, - "live-search": {WebSearch: "live", TextVerbosity: "medium"}, - "unknown-search": {WebSearch: "invalid", TextVerbosity: "medium"}, - "low-verbosity": {WebSearch: "disabled", TextVerbosity: "low"}, - "high-verbosity": {WebSearch: "disabled", TextVerbosity: "high"}, - "unknown-verbosity": {WebSearch: "disabled", TextVerbosity: "invalid"}, - } - for name, controls := range cases { - t.Run(name, func(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{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, Model: "native-model"} - _, err := startSingleTurn(t.Context(), config, 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) - } - if _, err := os.Stat(config.StateDir); !os.IsNotExist(err) { - t.Fatal("unsupported controls reached native setup", err) - } - }) - } -} - func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) @@ -81,7 +54,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{ModelProvider: fixtureProvider(), 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(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}} start, _, err := prepareConfiguration(config, request) if err != nil { t.Fatal(err) @@ -89,36 +62,24 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) { if start.OutputFormat == nil || string(start.OutputFormat.Schema) != string(schema) || start.SystemPrompt != "Original instructions." { t.Fatal("native configuration changed") } - request.ExecutionControls.OutputFormat.Schema = json.RawMessage(`{"type":"object","const":9007199254740993}`) - if _, _, err := prepareConfiguration(config, request); err == nil { - t.Fatal("lossy schema accepted") - } - request.ExecutionControls.OutputFormat.Schema = schema - request.DisableSubagents = false - if _, _, err := prepareConfiguration(config, request); err == nil { - t.Fatal("unqualified subagent combination accepted") - } } -func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing.T) { +// Tool discovery keeps the frozen definitions; a typeless parameters root +// becomes an object root, which admits the same arguments. +func TestToolDiscoveryPreservesFrozenFunctions(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{ModelProvider: fixtureProvider(), 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"}`)}, + {Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"properties":{}}`)}, + {Name: "note", Description: "Note", Parameters: json.RawMessage(`{"type":["object","null"]}`)}, }} start, _, err := prepareConfiguration(config, request) - if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) { + if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions[0], request.FunctionTools[0]) || string(start.Functions[1].Parameters) != `{"properties":{},"type":"object"}` || string(start.Functions[2].Parameters) != `{"type":"object"}` { t.Fatal("function discovery changed native definitions", err) } - request.DisableSubagents = false - if _, _, err := prepareConfiguration(config, request); err == nil { - t.Fatal("unqualified combination admitted") - } - request.DisableSubagents = true - request.ToolSearch = false - if _, _, err := prepareConfiguration(config, request); err == nil { - t.Fatal("deferred definitions became eager") + if string(request.FunctionTools[1].Parameters) != `{"properties":{}}` { + t.Fatal("typing the root changed the frozen request") } } 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 738cf56a8..46b93d007 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go @@ -83,7 +83,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { _ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600) } defer persist() - request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: 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."} + request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{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/functions.go b/apps/daemon/internal/agent/claudesdk/functions.go index 752d1347f..9dc4660a3 100644 --- a/apps/daemon/internal/agent/claudesdk/functions.go +++ b/apps/daemon/internal/agent/claudesdk/functions.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strings" "sync" "time" @@ -25,18 +26,34 @@ type functionState struct { closed bool } -func validateFunctions(tools []proto.FunctionTool) error { +// functionTools types each parameters root as an object, which the native SDK +// requires. A typeless root or a union with object admits the same tool +// arguments, since these are always objects. +func functionTools(tools []proto.FunctionTool) ([]proto.FunctionTool, error) { names := map[string]bool{} - for _, tool := range tools { - var schema struct { - Type string `json:"type"` - } - if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema.Type != "object" { - return fmt.Errorf("claudesdk: functions require unique names and object-root JSON schemas") + typed := slices.Clone(tools) + for i, tool := range typed { + var schema map[string]json.RawMessage + var root string + var union []string + if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema == nil { + return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas") } names[tool.Name] = true + switch t := schema["type"]; { + case json.Unmarshal(t, &root) == nil && root == "object": + case t == nil || json.Unmarshal(t, &union) == nil && slices.Contains(union, "object"): + schema["type"] = json.RawMessage(`"object"`) + parameters, err := json.Marshal(schema) + if err != nil { + return nil, err + } + typed[i].Parameters = parameters + default: + return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas") + } } - return nil + return typed, nil } func (s *session) receiveFunction(event bridgeEvent, start startRequest, emit func(string, any)) error { @@ -104,14 +121,6 @@ func (s *session) SubmitFunctionResult(ctx context.Context, result proto.Functio if err := result.ValidateContent(); err != nil { return err } - for _, part := range result.Content { - if part.Type == "input_image" && !result.Success { - return fmt.Errorf("claudesdk: native error results cannot retain images") - } - } - if err := (proto.MessageInput{{Content: result.Content}}).ValidateInlineImages(); err != nil { - return err - } data, err := json.Marshal(struct { Type string `json:"type"` TurnID string `json:"turn_id"` diff --git a/apps/daemon/internal/agent/claudesdk/functions_test.go b/apps/daemon/internal/agent/claudesdk/functions_test.go index 133c4d23d..8178a5fa0 100644 --- a/apps/daemon/internal/agent/claudesdk/functions_test.go +++ b/apps/daemon/internal/agent/claudesdk/functions_test.go @@ -49,11 +49,6 @@ func TestFunctionTurnNativeReceipts(t *testing.T) { if err := running.SubmitFunctionResult(ctx, invalid); err == nil { t.Fatal("missing content consumed call") } - image := "https://example.invalid/image" - invalid.Content = []proto.InputContent{{Type: "input_image", ImageURL: &image}} - if err := running.SubmitFunctionResult(ctx, invalid); err == nil { - t.Fatal("image should fail before delivery") - } first, second := "first-"+call.CallID, "second-"+call.CallID value := proto.FunctionResultPayload{DeliveryID: "delivery-" + call.CallID, CallID: call.CallID, Success: call.CallID == "b", Content: []proto.InputContent{{Type: "input_text", Text: &first}, {Type: "input_text", Text: &second}}} go func() { submissions <- running.SubmitFunctionResult(ctx, value) }() diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go index c02a24d33..dea9bb66d 100644 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/live_linux_test.go @@ -138,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, 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."} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{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.SystemPrompt = "Call lookup exactly once as requested, then report both result parts and any prior verification value. Never retry a failed tool." request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} diff --git a/apps/daemon/internal/agent/claudesdk/mcp.go b/apps/daemon/internal/agent/claudesdk/mcp.go index b193136d7..854a8fbff 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp.go +++ b/apps/daemon/internal/agent/claudesdk/mcp.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "net/url" - "regexp" "slices" "strings" @@ -14,9 +13,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -var mcpLabel = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) -var mcpTool = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) - func validateMCP(req proto.PromptRequestPayload) error { if req.MCPHTTPServers == nil { return nil @@ -32,7 +28,7 @@ func validateMCPServers(servers []proto.MCPHTTPServer) error { labels := map[string]bool{} for _, server := range servers { endpoint, err := url.Parse(server.ServerURL) - if !mcpLabel.MatchString(server.ServerLabel) || server.ServerLabel == "functions" || labels[server.ServerLabel] || + if server.ServerLabel == "" || labels[server.ServerLabel] || err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil || strings.ContainsAny(server.ServerURL, "?#") || endpoint.Opaque != "" { return fmt.Errorf("claudesdk: unsupported HTTP MCP declaration") @@ -41,13 +37,6 @@ func validateMCPServers(servers []proto.MCPHTTPServer) error { return fmt.Errorf("claudesdk: unsupported HTTPS MCP bearer credential") } labels[server.ServerLabel] = true - if server.AllowedTools != nil { - for _, name := range *server.AllowedTools { - if !mcpTool.MatchString(name) { - return fmt.Errorf("claudesdk: unsupported MCP tool allowlist") - } - } - } } return nil } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment.go b/apps/daemon/internal/agent/claudesdk/mcp_environment.go index 73720fe3c..99076465b 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment.go @@ -31,9 +31,6 @@ func mcpServers(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (s var servers []environmentMCPServer var env []string for _, binding := range bindings { - if !mcpLabel.MatchString(binding.ServerLabel) || binding.ServerLabel == "functions" { - return nil, nil, fmt.Errorf("claudesdk: unsupported MCP identity") - } if binding.Stdio != nil { command, args := stdio(*binding.Stdio) servers = append(servers, environmentMCPServer{mcpHTTPServer: mcpHTTPServer{ServerLabel: binding.ServerLabel}, Command: command, Args: args}) diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index 3b9f0fea9..72e02a64b 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -53,7 +53,6 @@ func TestEnvironmentMCPUsesInstalledLauncherAndSelectedCredential(t *testing.T) func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { for _, mutate := range []func(*proto.LocalEnvironment){ func(e *proto.LocalEnvironment) { e.NetworkAccess = "restricted" }, - func(e *proto.LocalEnvironment) { e.MCP[0].Server.Name = "functions" }, func(e *proto.LocalEnvironment) { e.MCP = append(e.MCP, e.MCP[0]) }, func(e *proto.LocalEnvironment) { e.MCP[0].Server.Type = "sse" }, func(e *proto.LocalEnvironment) { e.MCP[0].Server.HTTPHeaders = map[string]string{"X-Key": "literal"} }, diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index fd393182a..6dbb5037c 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -10,7 +10,7 @@ import ( ) func TestHTTPMCPDeclaration(t *testing.T) { - for _, mode := range []string{"unrestricted", "selected", "empty", "nil-slice", "required", "auth", "url-auth", "query", "wildcard", "reserved", "duplicate", "environment"} { + for _, mode := range []string{"unrestricted", "selected", "empty", "nil-slice", "required", "auth", "url-auth", "query", "duplicate", "environment"} { t.Run(mode, func(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) @@ -36,11 +36,6 @@ func TestHTTPMCPDeclaration(t *testing.T) { servers[0].ServerURL = "https://user:secret@example.invalid/mcp" case "query": servers[0].ServerURL += "?" - case "wildcard": - tools = []string{"*"} - servers[0].AllowedTools = &tools - case "reserved": - servers[0].ServerLabel = "functions" case "duplicate": servers = append(servers, servers[0]) case "environment": diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 880318ecd..c4bd96dfd 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -40,7 +40,7 @@ type startRequest struct { } func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { - start, provider, err := prepareOptions(req, req.MCPHTTPServers != nil || (req.LocalEnvironment != nil && len(req.LocalEnvironment.MCP) != 0)) + start, provider, err := prepareOptions(req) if err != nil { return startRequest{}, nil, err } @@ -91,12 +91,11 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR return start, env, nil } -// prepareOptions validates the request's execution configuration and renders -// the selected model provider. mcp reports whether the Executor serves MCP, which -// an agent-host view takes from its Session rather than the request. -func prepareOptions(req proto.PromptRequestPayload, mcp bool) (startRequest, []string, error) { - skills := req.LocalEnvironment != nil && len(req.LocalEnvironment.Skills) != 0 - start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools} +// prepareOptions renders the request's execution configuration and the +// selected model provider. The registered factory already admitted the +// selection against the declaration. +func prepareOptions(req proto.PromptRequestPayload) (startRequest, []string, error) { + start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ToolSearch: req.ToolSearch} fail := func(reason string) (startRequest, []string, error) { return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason) } @@ -105,36 +104,20 @@ func prepareOptions(req proto.PromptRequestPayload, mcp bool) (startRequest, []s return startRequest{}, nil, err } start.NativeModelOptions = compileNativeModelOptions(modelConfiguration.HarnessConfig) - if err := req.ValidateToolSearch(true); err != nil { - return startRequest{}, nil, err - } - if req.ToolSearch { - if skills || mcp || !req.DisableSubagents || (req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil) { - return fail("tool discovery requires the single-agent text/function profile") - } - start.ToolSearch = true - } if err := validateMCP(req); err != nil { return startRequest{}, nil, err } - // Search is disabled by the fixed native tool profile. Medium selects the - // SDK's default text generation; it has no native verbosity-level option. - if controls := req.ExecutionControls; controls != nil && (controls.WebSearch != "disabled" || controls.TextVerbosity != "medium") { - return fail("execution controls require disabled web search and medium text verbosity") - } + // The declaration admits only medium text verbosity, the SDK's default text + // generation, and the fixed native tool profile excludes search. if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { - format := req.ExecutionControls.OutputFormat - if format.Type != "json_schema" || !req.DisableSubagents || mcp || skills { - return fail("structured output requires the qualified single-agent function profile") - } - if err := proto.ValidateBinary64Schema(format.Schema); err != nil { - return startRequest{}, nil, err + if req.ExecutionControls.OutputFormat.Type != "json_schema" { + return fail("structured output requires a json_schema format") } - start.OutputFormat = format + start.OutputFormat = req.ExecutionControls.OutputFormat } if req.ObserveSubagentIdentities { - if req.DisableSubagents || len(req.FunctionTools) != 0 || mcp { - return fail("subagent execution does not support this tool combination") + if req.DisableSubagents { + return fail("subagent observation requires enabled subagents") } limit := 6 if req.MaxConcurrentSubagents != nil { @@ -145,7 +128,7 @@ func prepareOptions(req proto.PromptRequestPayload, mcp bool) (startRequest, []s } start.Subagents = &subagentOptions{MaxConcurrent: limit} } - if err := validateFunctions(req.FunctionTools); err != nil { + if start.Functions, err = functionTools(req.FunctionTools); err != nil { return startRequest{}, nil, err } start.Model, start.SystemPrompt = modelConfiguration.Model, req.SystemPrompt diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index 03e5b2557..861373fa1 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -97,7 +97,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { } func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) { - for _, name := range []string{"run", "prompt", "attachments", "subagents", "none", "functions", "mcp", "controls", "old-runtime"} { + for _, name := range []string{"run", "prompt", "attachments", "subagents", "none", "functions", "mcp", "old-runtime"} { t.Run(name, func(t *testing.T) { config := preparationFixture(t, name) req := preparationRequest() @@ -116,8 +116,6 @@ func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) req.FunctionTools = []proto.FunctionTool{{Name: "hello", Parameters: json.RawMessage(`{"type":"object"}`)}} case "mcp": req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} - case "controls": - req.ExecutionControls = &proto.ExecutionControls{WebSearch: "enabled", TextVerbosity: "medium"} } if _, err := NewExecutorFactory(config)(t.Context(), req); err == nil { t.Fatal("invalid preparation was accepted") diff --git a/apps/daemon/internal/agent/claudesdk/restrictions_test.go b/apps/daemon/internal/agent/claudesdk/restrictions_test.go index caa50512f..72d7d56c8 100644 --- a/apps/daemon/internal/agent/claudesdk/restrictions_test.go +++ b/apps/daemon/internal/agent/claudesdk/restrictions_test.go @@ -19,7 +19,7 @@ func TestTextTurnAcceptsRestrictiveCapabilities(t *testing.T) { controls *proto.ExecutionControls }{ {"environment", true, false, nil}, {"subagents", false, true, nil}, {"both", true, true, nil}, - {"execution-controls", true, true, &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}}, + {"execution-controls", true, true, &proto.ExecutionControls{TextVerbosity: "medium"}}, } { t.Run(test.name, func(t *testing.T) { root := t.TempDir() diff --git a/apps/daemon/internal/agent/claudesdk/session_test.go b/apps/daemon/internal/agent/claudesdk/session_test.go index 48cd21a66..6fb37780c 100644 --- a/apps/daemon/internal/agent/claudesdk/session_test.go +++ b/apps/daemon/internal/agent/claudesdk/session_test.go @@ -69,15 +69,13 @@ func TestTextTurnCompletionAndFailures(t *testing.T) { } func TestUnsupportedRequestRejectedBeforeLaunch(t *testing.T) { - for _, kind := range []string{"execution-controls", "tool", "outside"} { + for _, kind := range []string{"tool", "outside"} { t.Run(kind, func(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{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), Model: "fake"} switch kind { - case "execution-controls": - request.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "low"} case "tool": request.FunctionTools = []proto.FunctionTool{{}} case "outside": diff --git a/apps/daemon/internal/agent/claudesdk/subagents_test.go b/apps/daemon/internal/agent/claudesdk/subagents_test.go index 115fa05f2..4293d38f1 100644 --- a/apps/daemon/internal/agent/claudesdk/subagents_test.go +++ b/apps/daemon/internal/agent/claudesdk/subagents_test.go @@ -32,8 +32,6 @@ func TestSubagentConfigurationRejectsUnqualifiedAuthority(t *testing.T) { for _, change := range []func(*proto.PromptRequestPayload){ func(r *proto.PromptRequestPayload) { r.DisableSubagents = true }, func(r *proto.PromptRequestPayload) { n := 0; r.MaxConcurrentSubagents = &n }, - func(r *proto.PromptRequestPayload) { r.FunctionTools = []proto.FunctionTool{{Name: "function"}} }, - func(r *proto.PromptRequestPayload) { v := []proto.MCPHTTPServer{}; r.MCPHTTPServers = &v }, } { req := workspaceRequest() req.DisableSubagents, req.ObserveSubagentIdentities = false, true diff --git a/apps/daemon/internal/agent/claudesdk/view.go b/apps/daemon/internal/agent/claudesdk/view.go index a5dcd0c91..42567ae56 100644 --- a/apps/daemon/internal/agent/claudesdk/view.go +++ b/apps/daemon/internal/agent/claudesdk/view.go @@ -123,7 +123,7 @@ func prepareView(layout viewLayout, req proto.PromptRequestPayload, view agent.V if err != nil { return startRequest{}, nil, err } - start, provider, err := prepareOptions(req, len(servers) != 0) + start, provider, err := prepareOptions(req) if err != nil { return startRequest{}, nil, err } diff --git a/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go b/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go index 4dacb191c..4b77d59ee 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go @@ -19,7 +19,7 @@ func TestWorkspaceStructuredPreparationQualificationAndFrozenSchema(t *testing.T config := preparationFixture(t, mode) req := preparationRequest() schema := `{"type":"object","properties":{"n":{"const":9007199254740992}}}` - req.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: json.RawMessage(schema)}} + req.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: json.RawMessage(schema)}} e, err := NewExecutorFactory(config)(t.Context(), req) if mode == "structured-missing" { if err == nil || !strings.Contains(err.Error(), "workspace structured output") { diff --git a/apps/daemon/internal/agent/codex/declaration.go b/apps/daemon/internal/agent/codex/declaration.go index 07dc44dc6..244ee664d 100644 --- a/apps/daemon/internal/agent/codex/declaration.go +++ b/apps/daemon/internal/agent/codex/declaration.go @@ -11,29 +11,10 @@ import ( ) // Declaration owns Codex discovery, configuration and execution factories. -var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{ - Kind: "codex", - Capabilities: proto.AgentKindCapabilities{ - SubagentObservations: proto.CapabilitySupported, - NativeSessionRecovery: proto.CapabilityUnsupported, - EnvironmentNone: proto.CapabilitySupported, - LocalEnvironment: proto.CapabilityUnsupported, - WorkspaceReadPreparation: proto.CapabilityUnsupported, - WorkspaceOutputExport: proto.CapabilityUnsupported, - ProgrammaticToolCallingDisable: proto.CapabilitySupported, - WebSearchControl: proto.CapabilitySupported, - TextVerbosity: proto.CapabilityFromBool(SupportsTextVerbosity), - StructuredOutput: proto.CapabilityUnsupported, - ToolSearch: proto.CapabilityUnsupported, - MessageImages: proto.CapabilitySupported, - FunctionResultImages: proto.CapabilitySupported, - SubagentControl: proto.CapabilitySupported, - FunctionTools: proto.CapabilitySupported, - MCPHTTPTools: proto.CapabilitySupported, - MCPHTTPRequired: proto.CapabilityUnsupported, - MCPHTTPBearerAuth: proto.CapabilitySupported, - }, -}, Configuration: configuration.Configuration(), Discover: discover} +// Discovery narrows the declared support to what the installed version and +// platform serve. +var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "codex", Capabilities: configuration.Configuration().Declaration.Capabilities}, + Configuration: configuration.Configuration(), Discover: discover} func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime { return discoverWithCheck(ctx, options, info, CheckCLIAvailable) @@ -49,10 +30,10 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, i } runtime.Info.Available, runtime.Info.Version = true, version caps := &runtime.Info.Capabilities - caps.NativeSessionRecovery = proto.CapabilityFromBool(SupportsNativeSessionRecovery(version)) - // A local Environment requires native Session recovery. - caps.LocalEnvironment = caps.NativeSessionRecovery - caps.MCPHTTPRequired = proto.CapabilityFromBool(SupportsNativeSessionRecovery(version)) + // A local Environment and required MCP servers need native Session recovery. + recovery := proto.CapabilityFromBool(SupportsNativeSessionRecovery(version)) + caps.NativeSessionRecovery, caps.LocalEnvironment, caps.MCPHTTPRequired = recovery, recovery, recovery + caps.TextVerbosity = proto.CapabilityFromBool(SupportsTextVerbosity) runtime.Executor = NewExecutorFactory() runtime.View = discoverView(version) fmt.Fprintf(options.Stdout, "Codex preflight ok (%s)\n", version) diff --git a/apps/daemon/internal/agent/codex/declaration_test.go b/apps/daemon/internal/agent/codex/declaration_test.go index c0623229f..1e9d0608e 100644 --- a/apps/daemon/internal/agent/codex/declaration_test.go +++ b/apps/daemon/internal/agent/codex/declaration_test.go @@ -4,11 +4,9 @@ import ( "context" "errors" "io" - "reflect" "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { @@ -27,25 +25,6 @@ func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { } } -// The declaration must retain the complete baseline capability descriptor. -func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"SubagentObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "TextVerbosity": SupportsTextVerbosity, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} - value := reflect.ValueOf(Declaration.Info.Capabilities) - for i := 0; i < value.NumField(); i++ { - name := value.Type().Field(i).Name - want := proto.CapabilityUnsupported - if expected[name] { - want = proto.CapabilitySupported - } - if got := value.Field(i).Interface(); got != want { - t.Errorf("%s = %v, want %v", name, got, want) - } - } - if err := Declaration.Info.ValidateDeclaration(); err != nil { - t.Fatal(err) - } -} - func TestUnavailableRuntimeHasNoExecutionFactories(t *testing.T) { runtime := discoverWithCheck(t.Context(), agent.DiscoveryOptions{Stdout: io.Discard, Stderr: io.Discard}, Declaration.Info, func(context.Context, string) (string, error) { return "", errors.New("missing") }) if runtime.Info.Available || runtime.Executor != nil || runtime.View != nil { diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index 3b9260110..ca67f6f47 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -14,30 +14,25 @@ func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Fatal(err) } plan.Cleanup() - if want := [][2]string{{"tools.experimental_request_user_input.enabled", "false"}, {"model_provider", `"` + oacProviderSlug + `"`}}; !reflect.DeepEqual(plan.ExtraConfig, want) { + if want := [][2]string{{"tools.experimental_request_user_input.enabled", "false"}, {"web_search", `"disabled"`}, {"model_provider", `"` + oacProviderSlug + `"`}}; !reflect.DeepEqual(plan.ExtraConfig, want) { 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{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{WebSearch: search, TextVerbosity: verbosity}}) - if err != nil { - t.Fatal(err) - } - plan.Cleanup() - want := [][2]string{{"tools.experimental_request_user_input.enabled", "false"}, {"web_search", `"` + search + `"`}, {"model_verbosity", `"` + verbosity + `"`}, {"model_provider", `"` + oacProviderSlug + `"`}} - if !reflect.DeepEqual(plan.ExtraConfig, want) { - t.Fatalf("config = %v, want %v", plan.ExtraConfig, want) - } + for _, verbosity := range []string{"low", "medium", "high"} { + plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{TextVerbosity: verbosity}}) + if err != nil { + t.Fatal(err) + } + plan.Cleanup() + want := [][2]string{{"tools.experimental_request_user_input.enabled", "false"}, {"web_search", `"disabled"`}, {"model_verbosity", `"` + verbosity + `"`}, {"model_provider", `"` + oacProviderSlug + `"`}} + if !reflect.DeepEqual(plan.ExtraConfig, want) { + t.Fatalf("config = %v, want %v", plan.ExtraConfig, want) } } } func TestExecutionControlsRejectIncompleteOrInvalidValues(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - for _, controls := range []proto.ExecutionControls{ - {}, {WebSearch: "disabled"}, {TextVerbosity: "medium"}, - {WebSearch: "invalid", TextVerbosity: "medium"}, {WebSearch: "disabled", TextVerbosity: "invalid"}, - } { + for _, controls := range []proto.ExecutionControls{{}, {TextVerbosity: "invalid"}} { 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/mcp_http.go b/apps/daemon/internal/agent/codex/mcp_http.go index 0b1461a11..84dd40dbd 100644 --- a/apps/daemon/internal/agent/codex/mcp_http.go +++ b/apps/daemon/internal/agent/codex/mcp_http.go @@ -31,9 +31,6 @@ func mcpServersFromBindings(bindings []agent.MCPBinding, stdio func(proto.Enviro servers := make(map[string]mcpServerConfig, len(bindings)) var env []string for _, binding := range bindings { - if binding.ServerLabel == "codex_apps" { - return nil, nil, errors.New("codex: reserved MCP server label") - } server := mcpServerConfig{Name: binding.ServerLabel, URL: binding.ServerURL, Required: binding.Required, EnabledTools: binding.AllowedTools, ApproveTools: binding.ConnectionOrigin == "environment"} if binding.Stdio != nil { server.Command, server.Args = stdio(*binding.Stdio) 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 9d143b7c1..875f07bbe 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go @@ -81,7 +81,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{ModelProvider: fixtureProvider(), AgentStateKey: "catalog", DisableExecutionEnvironment: true, MCPHTTPServers: &servers, - Model: "fixture-model", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} + Model: "fixture-model", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} cfg := defaultSessionConfig() cfg.codexBinary = binary plan, err := prepareSessionPlan(t.Context(), req, cfg) diff --git a/apps/daemon/internal/agent/codex/mcp_http_test.go b/apps/daemon/internal/agent/codex/mcp_http_test.go index 0ab5c499c..309c5b183 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_test.go @@ -79,7 +79,6 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { } } for _, server := range []proto.MCPHTTPServer{ - {ConnectionOrigin: "service", ServerLabel: "codex_apps", ServerURL: "https://docs.example/mcp"}, {ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "https://user:synthetic-secret@docs.example/mcp"}, {ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "https://docs.example/mcp?token=synthetic-secret"}, {ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "file:///tmp/mcp"}, diff --git a/apps/daemon/internal/agent/codex/model_verbosity_test.go b/apps/daemon/internal/agent/codex/model_verbosity_test.go index 4cbb5ee4b..cef4d59ee 100644 --- a/apps/daemon/internal/agent/codex/model_verbosity_test.go +++ b/apps/daemon/internal/agent/codex/model_verbosity_test.go @@ -38,7 +38,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{ModelProvider: fixtureProvider(), 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{TextVerbosity: "high"}}) if err != nil { t.Fatal(err) } @@ -81,7 +81,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{ModelProvider: fixtureProvider(), 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{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 c1cde2f10..ab3d815c3 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -87,18 +87,14 @@ func buildSessionPlan(req proto.PromptRequestPayload, allocHome func() (agent.Vi if err != nil { return plan, err } + plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"web_search", strconv("disabled")}) if controls := req.ExecutionControls; controls != nil { - switch controls.WebSearch { - case "disabled", "cached", "live": - default: - return plan, fmt.Errorf("codex: web_search must be disabled, cached or live") - } switch controls.TextVerbosity { case "low", "medium", "high": default: return plan, fmt.Errorf("codex: text_verbosity must be low, medium or high") } - plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"web_search", strconv(controls.WebSearch)}, [2]string{"model_verbosity", strconv(controls.TextVerbosity)}) + plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_verbosity", strconv(controls.TextVerbosity)}) } plan.Model = prepared.Model plan.SystemPrompt = req.SystemPrompt diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 0b6094f6f..3c1f45bb7 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -12,9 +12,6 @@ import ( ) func newExecutor(parent context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (*Executor, error) { - if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { - return nil, errors.New("codex: structured output is not qualified") - } if req.WorkspaceReadOnly { return nil, errors.New("codex: workspace reads use the local Runtime interface") } diff --git a/apps/daemon/internal/agent/codex/preparation_helpers_test.go b/apps/daemon/internal/agent/codex/preparation_helpers_test.go index 57197d0cb..ccc527807 100644 --- a/apps/daemon/internal/agent/codex/preparation_helpers_test.go +++ b/apps/daemon/internal/agent/codex/preparation_helpers_test.go @@ -44,7 +44,7 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig AgentKind: "codex", AgentStateKey: "prepared-session", Model: "fixture-model", ModelProvider: fixtureProvider(), - ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, + ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, DisableExecutionEnvironment: true, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"value":{"type":"integer"}}}`)}}, } diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index a1ad66749..4b1e61c5e 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -9,13 +9,14 @@ 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" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) +// Every registered factory prepares the bound model configuration before +// native code. func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { registry := agent.NewRegistry() - configuration := harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "responses"}}} + configuration := prototest.ModelConfiguration() calls := 0 expected := errors.New("native entry reached") registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration) @@ -48,5 +49,7 @@ func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) { t.Fatal("invalid configuration registered") } }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}) + configuration := prototest.ModelConfiguration() + configuration.Providers[0].Protocol = "unknown" + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration) } diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index e089eefed..5bd10beaf 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -18,9 +18,10 @@ // in: Register composes its EnvironmentSupport with the Harness's own // declaration once. // -// Runtime registration and Core service qualification remain separate. A public -// Harness also needs a profile in services/core/internal/engine; advertising -// a capability cannot authorize it. Requests, events and capability descriptors +// The Harness's support is its harnessconfig Declaration, which Core reads +// too. Discovery and the Environment owner only narrow its Capabilities, and +// the registered Executor factory runs only for a request whose selection that +// narrowed declaration admits. Requests, events and capability descriptors // use the existing internal/agentdaemon/proto types. An Environment execution // request carries the Runtime's bound workspace directory in // LocalEnvironment.WorkspaceRoot; the native Harness runs there. @@ -87,7 +88,6 @@ type EnvironmentSupport struct { // Compose narrows caps, a Harness's own declaration, to what s serves. func (s EnvironmentSupport) Compose(caps proto.AgentKindCapabilities) proto.AgentKindCapabilities { caps.LocalEnvironment = proto.CapabilityFromBool(s.Local && caps.LocalEnvironment.IsSupported()) - caps.WorkspaceReadPreparation, caps.WorkspaceOutputExport = caps.LocalEnvironment, caps.LocalEnvironment caps.EnvironmentNone = proto.CapabilityFromBool(s.None && caps.EnvironmentNone.IsSupported()) return caps } @@ -516,15 +516,14 @@ func (r *Registry) ResolveView(kind string) (View, error) { return view.clone(), nil } -// Model configuration has one shared contract, authored in +// Model configuration and support have one shared contract, authored in // internal/harnessconfig/harness.go. RegisterKind requires that declaration; -// RegisterExecutor inherits it. Every registered entry -// validates model, provider and native parameters before calling native code. -// The declaration belongs to the adapter and is also consumed by Core. Keep +// RegisterExecutor inherits it. Every registered entry validates the selection, +// model, provider and native parameters before calling native code. The +// declaration belongs to the adapter and is also consumed by Core. Keep // adapter field rules and rendering private. That shared contract owns frozen // configuration and native application obligations. This file owns execution -// lifecycle only. Native image/tool/operation support is qualified through proto -// capabilities and the Core engine profile, not model configuration declarations. +// lifecycle only. // // Preparation failure retains unconfirmed native cleanup in a non-nil Executor // under the factory ownership contract below. @@ -595,6 +594,11 @@ func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration har panic(err) } configuration = configuration.Clone() + declaration, err := configuration.Declaration.Narrow(info.Capabilities) + if err != nil { + panic(err) + } + configuration.Declaration = declaration r.mu.Lock() defer r.mu.Unlock() r.configurations[kind] = configuration diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index d39d232e9..236fca8d5 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -10,33 +10,17 @@ import ( configuration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" ) -// Declaration owns MiniMax Code discovery, configuration and execution factories. -var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "mcode", Capabilities: proto.AgentKindCapabilities{ - SubagentObservations: proto.CapabilityUnsupported, - NativeSessionRecovery: proto.CapabilityUnsupported, - EnvironmentNone: proto.CapabilityUnsupported, - LocalEnvironment: proto.CapabilityUnsupported, - WorkspaceReadPreparation: proto.CapabilityUnsupported, - WorkspaceOutputExport: proto.CapabilityUnsupported, - ProgrammaticToolCallingDisable: proto.CapabilityUnsupported, - WebSearchControl: proto.CapabilityUnsupported, - TextVerbosity: proto.CapabilityUnsupported, - StructuredOutput: proto.CapabilityUnsupported, - ToolSearch: proto.CapabilityUnsupported, - MessageImages: proto.CapabilityUnsupported, - FunctionResultImages: proto.CapabilityUnsupported, - SubagentControl: proto.CapabilityUnsupported, - FunctionTools: proto.CapabilityUnsupported, - MCPHTTPTools: proto.CapabilityUnsupported, - MCPHTTPRequired: proto.CapabilityUnsupported, - MCPHTTPBearerAuth: proto.CapabilityUnsupported, -}}, Configuration: configuration.Configuration(), Discover: discover} +// Declaration owns MiniMax Code discovery, configuration and execution +// factories. Native preparation verifies the applied admission and tool +// profile before input. +var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "mcode", Capabilities: configuration.Configuration().Declaration.Capabilities}, + Configuration: configuration.Configuration(), Discover: discover} func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime { return discoverWithCheck(ctx, options, info, CheckCLIAvailable) } -func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, result proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime { - runtime := &agent.Runtime{Info: result} +func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime { + runtime := &agent.Runtime{Info: info} ctx, cancel := context.WithTimeout(parent, 15*time.Second) defer cancel() @@ -45,16 +29,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, r fmt.Fprintf(options.Stderr, "oac-daemon: mcode unavailable: %v\n Install: npm install -g @minimax-ai/code@0.4.12\n", err) return runtime } - result.Available, result.Version = true, version - result.Capabilities.ProgrammaticToolCallingDisable = proto.CapabilitySupported - result.Capabilities.SubagentControl = proto.CapabilitySupported - // Native preparation verifies the applied admission/tool profile before input. - result.Capabilities.SubagentObservations = proto.CapabilitySupported - result.Capabilities.EnvironmentNone = proto.CapabilitySupported - result.Capabilities.LocalEnvironment = proto.CapabilitySupported - result.Capabilities.MCPHTTPTools = proto.CapabilitySupported - result.Capabilities.MCPHTTPBearerAuth = proto.CapabilitySupported - runtime.Info = result + runtime.Info.Available, runtime.Info.Version = true, version workspace := discoverWorkspace(parent, options, runtime) if runtime.Info.Available { runtime.Executor = NewExecutorFactory(workspace) diff --git a/apps/daemon/internal/agent/mcode/declaration_test.go b/apps/daemon/internal/agent/mcode/declaration_test.go index 4393e7503..5c80e7393 100644 --- a/apps/daemon/internal/agent/mcode/declaration_test.go +++ b/apps/daemon/internal/agent/mcode/declaration_test.go @@ -4,11 +4,9 @@ import ( "context" "errors" "io" - "reflect" "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // An available runtime is execution-capable; a rejected native version is unavailable. @@ -24,25 +22,9 @@ func TestMCodeExecutionFollowsAvailability(t *testing.T) { if (runtime.Executor != nil) != available { t.Fatalf("factories: %+v", runtime) } - if info.Available != available || info.Capabilities.EnvironmentNone.IsSupported() != available || info.Capabilities.LocalEnvironment.IsSupported() != available || info.Capabilities.SubagentObservations.IsSupported() != available { - t.Fatalf("capabilities=%+v", info.Capabilities) - } - if info.Capabilities.NativeSessionRecovery.IsSupported() || info.Capabilities.FunctionTools.IsSupported() { - t.Fatal("unqualified capability advertised") + if info.Available != available { + t.Fatalf("available=%v", info.Available) } }) } } - -// The static declaration supports nothing until discovery finds the CLI. -func TestDeclaredCapabilityBaseline(t *testing.T) { - value := reflect.ValueOf(Declaration.Info.Capabilities) - for i := 0; i < value.NumField(); i++ { - if got := value.Field(i).Interface(); got != proto.CapabilityUnsupported { - t.Errorf("%s = %v, want unsupported", value.Type().Field(i).Name, got) - } - } - if err := Declaration.Info.ValidateDeclaration(); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/agent/mcode/environment_mcp.go b/apps/daemon/internal/agent/mcode/environment_mcp.go index 78def82a3..8381d24a1 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp.go @@ -24,9 +24,6 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, []agent.MCPBi func workspaceMCP(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) ([]map[string]any, error) { var servers []map[string]any for _, binding := range bindings { - if binding.ServerLabel == "oac_workspace" || binding.ConnectionOrigin != "environment" || binding.AllowedTools != nil || binding.Required { - return nil, fmt.Errorf("mcode: unsupported MCP binding") - } if binding.Transport != "http" { command, args := stdio(*binding.Stdio) servers = append(servers, map[string]any{"name": binding.ServerLabel, "command": command, "args": args, "env": []map[string]string{}}) diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index 5ca8565f9..04ecc4ea1 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -72,7 +72,7 @@ func TestEnvironmentMCPUsesFixedLauncherForNewAndLoadedSessions(t *testing.T) { } func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T) { - for _, name := range []string{"http-headers", "http-bearer-insecure", "http-bearer-missing", "restricted", "disabled", "duplicate", "reserved"} { + for _, name := range []string{"http-headers", "http-bearer-insecure", "http-bearer-missing", "restricted", "disabled", "duplicate"} { t.Run(name, func(t *testing.T) { c, req, _ := workspaceFixture(t) c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" @@ -95,8 +95,6 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T c.Network, req.LocalEnvironment.NetworkAccess = name, name case "duplicate": req.LocalEnvironment.MCP = append(req.LocalEnvironment.MCP, environmentMCPFixture()) - case "reserved": - req.LocalEnvironment.MCP[0].Server.Name = "oac_workspace" } if _, err := prepareWorkspaceOptions(c, req); err == nil || strings.Contains(err.Error(), "confidential-http-token") { t.Fatal("unqualified declaration accepted or credential exposed") @@ -264,14 +262,4 @@ func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) { if err != nil { t.Fatal(err) } - empty := []string{} - (*req.MCPHTTPServers)[0].AllowedTools = &empty - if _, err := prepareWorkspaceOptions(c, req); err == nil { - t.Fatal("empty allowlist silently treated as all") - } - (*req.MCPHTTPServers)[0].AllowedTools = nil - (*req.MCPHTTPServers)[0].Required = true - if _, err := prepareWorkspaceOptions(c, req); err == nil { - t.Fatal("required initialization silently ignored") - } } diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index c8438e680..094cec7ca 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -8,12 +8,9 @@ import ( ) func validateExecutionRequest(req proto.PromptRequestPayload) error { - if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || len(req.FunctionTools) != 0 || (req.MCPHTTPServers != nil && len(*req.MCPHTTPServers) != 0) { + if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || req.ExecutionControls == nil { return fmt.Errorf("mcode: unsupported execution configuration") } - if req.ExecutionControls == nil || req.ExecutionControls.OutputFormat != nil || req.ExecutionControls.WebSearch != "disabled" || (req.ExecutionControls.TextVerbosity != "" && req.ExecutionControls.TextVerbosity != "medium") { - return fmt.Errorf("mcode: unsupported execution controls") - } if !req.DisableSubagents && (req.MaxConcurrentSubagents == nil || *req.MaxConcurrentSubagents < 1) { return fmt.Errorf("mcode: Subagent concurrency limit is required") } diff --git a/apps/daemon/internal/agent/mcode/execution_test.go b/apps/daemon/internal/agent/mcode/execution_test.go index 9f188f97f..198532f2f 100644 --- a/apps/daemon/internal/agent/mcode/execution_test.go +++ b/apps/daemon/internal/agent/mcode/execution_test.go @@ -43,8 +43,7 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = false }, func(r *proto.PromptRequestPayload) { r.DisableSubagents = false }, func(r *proto.PromptRequestPayload) { r.RequireExistingNativeSession = true }, - func(r *proto.PromptRequestPayload) { r.FunctionTools = []proto.FunctionTool{{Name: "f"}} }, - func(r *proto.PromptRequestPayload) { r.ExecutionControls.WebSearch = "enabled" }, + func(r *proto.PromptRequestPayload) { r.ExecutionControls = nil }, } { r := testRequest(t) change(&r) diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index 4e9172b1f..d49709610 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -21,7 +21,7 @@ func testRequest(t *testing.T) proto.PromptRequestPayload { return proto.PromptRequestPayload{RunID: "run-1", AgentStateKey: "conversation-1/agent-1/mcode", Input: proto.TextInput("Hello"), Model: "fixture", SystemPrompt: "Current instructions", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example", APIKey: "fixture-key", ContextWindow: 64000, MaxOutputTokens: 4096}, - DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} + DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} } // helperRequest selects a protocol fixture scenario as the native CLI. diff --git a/apps/daemon/internal/agent/registry.go b/apps/daemon/internal/agent/registry.go index 19265955b..f82cfbbc6 100644 --- a/apps/daemon/internal/agent/registry.go +++ b/apps/daemon/internal/agent/registry.go @@ -62,6 +62,17 @@ func (r *Registry) ResolveExecutor(kind string) (ExecutorFactory, error) { return factory, nil } +// Declaration returns an available kind's support, narrowed to what the +// Runtime advertises. +func (r *Registry) Declaration(kind string) (proto.Declaration, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + if !r.kinds[kind].Available { + return proto.Declaration{}, false + } + return r.configurations[kind].Declaration, true +} + // Configuration returns an owned declaration for registry wrappers. Wrappers // transfer it with the factory; they must not infer configuration from kind names. func (r *Registry) Configuration(kind string) (harnessconfig.Configuration, error) { diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index 4c9c1ef6d..17354f190 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" @@ -15,8 +13,8 @@ import ( func TestRegistryRegisterOverwritesDescriptor(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v1", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v2", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v1", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration()) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v2", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration()) got := reg.SupportedAgentKinds() if len(got) != 1 || got[0].Version != "v2" { @@ -25,8 +23,7 @@ func TestRegistryRegisterOverwritesDescriptor(t *testing.T) { } // The heartbeat declares what the Harness runs within what the Environment -// owner serves, and the owner serves read preparation and export with every -// local Environment. +// owner serves. func TestRegisterComposesEnvironmentSupport(t *testing.T) { s, u := proto.CapabilitySupported, proto.CapabilityUnsupported for _, c := range []struct { @@ -42,11 +39,10 @@ func TestRegisterComposesEnvironmentSupport(t *testing.T) { info := proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ LocalEnvironment: proto.CapabilityFromBool(c.local), EnvironmentNone: proto.CapabilityFromBool(c.none)})} registry := agent.NewRegistry() - registry.Register(agent.Declaration{Info: info}, agent.Runtime{Info: info}, c.environments) + registry.Register(agent.Declaration{Info: info, Configuration: prototest.ModelConfiguration()}, agent.Runtime{Info: info}, c.environments) got := registry.SupportedAgentKinds()[0].Capabilities want := info.Capabilities want.LocalEnvironment, want.EnvironmentNone = c.wantLocal, c.wantNone - want.WorkspaceReadPreparation, want.WorkspaceOutputExport = c.wantLocal, c.wantLocal if got != want { t.Errorf("%+v over %+v: declared %+v, want %+v", c.environments, info.Capabilities, got, want) } @@ -59,7 +55,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{}) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration()) } func TestRegistryRegisterRejectsFactoriesForUnavailableRuntime(t *testing.T) { @@ -74,7 +70,7 @@ func TestRegistryRegisterRejectsFactoriesForUnavailableRuntime(t *testing.T) { t.Fatal("rejected runtime changed registry") } }() - registry.Register(agent.Declaration{Info: info}, agent.Runtime{Info: info, Executor: executor}, agent.EnvironmentSupport{Local: true}) + registry.Register(agent.Declaration{Info: info, Configuration: prototest.ModelConfiguration()}, agent.Runtime{Info: info, Executor: executor}, agent.EnvironmentSupport{Local: true}) } func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { @@ -86,7 +82,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ FunctionTools: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}) + }, prototest.ModelConfiguration()) reg.RegisterKind(proto.SupportedAgentKind{ Kind: "fake_alpha", Available: true, @@ -95,7 +91,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { FunctionTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}) + }, prototest.ModelConfiguration()) got := reg.SupportedAgentKinds() if len(got) != 2 { @@ -127,7 +123,7 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { 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{}) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration()) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("replacing a kind retained its old executor capability") } @@ -139,7 +135,7 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { t.Run(reflect.TypeOf(valid).Field(i).Name, func(t *testing.T) { registry := agent.NewRegistry() original := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid} - registry.RegisterKind(original, harnessconfig.Configuration{}) + registry.RegisterKind(original, prototest.ModelConfiguration()) registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil }) missing := valid reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) @@ -149,7 +145,7 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { t.Error("incomplete declaration registered") } }() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, harnessconfig.Configuration{}) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, prototest.ModelConfiguration()) }() if _, err := registry.ResolveExecutor("fixture"); err != nil || !reflect.DeepEqual(registry.SupportedAgentKinds(), []proto.SupportedAgentKind{original}) { t.Fatal("failed declaration changed registry") diff --git a/apps/daemon/internal/agent/view_test.go b/apps/daemon/internal/agent/view_test.go index f9108eddb..7e65e7327 100644 --- a/apps/daemon/internal/agent/view_test.go +++ b/apps/daemon/internal/agent/view_test.go @@ -11,7 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -53,7 +52,7 @@ func TestRegistryResolvesOnlyDeclaredViews(t *testing.T) { declared := validView(t) for kind, view := range map[string]*agent.View{"with_view": &declared, "without_view": nil} { info := proto.SupportedAgentKind{Kind: kind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})} - reg.Register(agent.Declaration{Info: info}, agent.Runtime{Info: info, View: view}, agent.EnvironmentSupport{}) + reg.Register(agent.Declaration{Info: info, Configuration: prototest.ModelConfiguration()}, agent.Runtime{Info: info, View: view}, agent.EnvironmentSupport{}) } if _, err := reg.ResolveView("without_view"); !errors.Is(err, agent.ErrUnsupportedOperation) { t.Fatalf("ResolveView without a view = %v, want ErrUnsupportedOperation", err) @@ -71,10 +70,7 @@ func TestViewExecutorReceivesOnlyGatewayConnections(t *testing.T) { return nil, errReached } info := proto.SupportedAgentKind{Kind: "viewed", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})} - declaration := agent.Declaration{ - Info: info, - Configuration: harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: string(modelprovider.Responses)}}}, - } + declaration := agent.Declaration{Info: info, Configuration: prototest.ModelConfiguration()} reg.Register(declaration, agent.Runtime{Info: info, View: &declared}, agent.EnvironmentSupport{}) view, err := reg.ResolveView("viewed") if err != nil { diff --git a/apps/daemon/internal/agenthost/admit_linux_test.go b/apps/daemon/internal/agenthost/admit_linux_test.go index 466c5f5ba..f6454fa5d 100644 --- a/apps/daemon/internal/agenthost/admit_linux_test.go +++ b/apps/daemon/internal/agenthost/admit_linux_test.go @@ -160,8 +160,7 @@ func TestRegistryRunsKindsWithViews(t *testing.T) { var kinds []string for _, info := range (&Host{cfg: f.cfg}).Registry().SupportedAgentKinds() { kinds = append(kinds, info.Kind) - if caps := info.Capabilities; !caps.LocalEnvironment.IsSupported() || !caps.EnvironmentNone.IsSupported() || - !caps.WorkspaceReadPreparation.IsSupported() || !caps.WorkspaceOutputExport.IsSupported() { + if caps := info.Capabilities; !caps.LocalEnvironment.IsSupported() || !caps.EnvironmentNone.IsSupported() { t.Errorf("%s does not run in the Environments the agent host serves: %+v", info.Kind, caps) } } diff --git a/apps/daemon/internal/agenthost/agenthost_linux_test.go b/apps/daemon/internal/agenthost/agenthost_linux_test.go index 18441612d..a1c92a984 100644 --- a/apps/daemon/internal/agenthost/agenthost_linux_test.go +++ b/apps/daemon/internal/agenthost/agenthost_linux_test.go @@ -23,7 +23,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink" @@ -75,7 +74,9 @@ func newConfig(t *testing.T, reg *agent.Registry, ca *x509.Certificate) Config { func register(reg *agent.Registry, kind string, view *agent.View) { info := proto.SupportedAgentKind{Kind: kind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ LocalEnvironment: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported})} - reg.RegisterKind(info, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: string(modelprovider.Anthropic)}}}) + configuration := prototest.ModelConfiguration() + configuration.Providers[0].Protocol = string(modelprovider.Anthropic) + reg.RegisterKind(info, configuration) if view != nil { reg.RegisterView(kind, *view) } diff --git a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go index ce56b56a5..2a3f651b2 100644 --- a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go +++ b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go @@ -149,7 +149,7 @@ func qualify(t *testing.T, h *agenthost.Host, cfg agenthost.Config, sb *sandbox, "3. Answer with exactly one line: VALUE= EXIT=" configuration := proto.PromptRequestPayload{AgentKind: kind, DisableSubagents: true, - Model: model.Model, ModelProvider: model.ModelProvider, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, + Model: model.Model, ModelProvider: model.ModelProvider, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, LocalEnvironment: &proto.LocalEnvironment{ID: uuid.UUID(sb.resource.EnvironmentID).String(), WorkspaceDirectory: workspace, NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}}} if caps.FunctionTools.IsSupported() { diff --git a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go index a85b8beec..1035023e2 100644 --- a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go +++ b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go @@ -91,7 +91,7 @@ func TestLiveRegisteredClaudeSDK(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second) defer cancel() id := uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "claude_sdk", AgentStateKey: prototest.StateKey, AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider} + request := proto.PromptRequestPayload{AgentKind: "claude_sdk", AgentStateKey: prototest.StateKey, AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider} if callFunction { request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} } diff --git a/apps/daemon/internal/dispatch/capabilities.go b/apps/daemon/internal/dispatch/capabilities.go deleted file mode 100644 index 66910b645..000000000 --- a/apps/daemon/internal/dispatch/capabilities.go +++ /dev/null @@ -1,12 +0,0 @@ -package dispatch - -import "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - -func (r *Router) availableCapabilities(kind string) (proto.AgentKindCapabilities, bool) { - for _, info := range r.registry.SupportedAgentKinds() { - if info.Kind == kind && info.Available { - return info.Capabilities, true - } - } - return proto.AgentKindCapabilities{}, false -} diff --git a/apps/daemon/internal/dispatch/environment.go b/apps/daemon/internal/dispatch/environment.go index e300c1a31..5156ca0f6 100644 --- a/apps/daemon/internal/dispatch/environment.go +++ b/apps/daemon/internal/dispatch/environment.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "net/url" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" @@ -85,21 +86,22 @@ var ( ErrWorkspaceWriteUnsafe = fmt.Errorf("%w: destination exists or its path is not a plain directory chain", ErrWorkspaceWriteRejected) ) -func validateExecutionEnvironment(req proto.PromptRequestPayload, caps proto.AgentKindCapabilities) error { +// validateExecutionEnvironment checks the request's structure; +// proto.ValidateSelection checks what the Harness supports. +func validateExecutionEnvironment(req proto.PromptRequestPayload) error { if (req.LocalEnvironment != nil) == req.DisableExecutionEnvironment { return errors.New("execution requires exactly one of local_environment and disable_execution_environment") } - if err := req.ValidateProgrammaticToolCallingDisable(caps.ProgrammaticToolCallingDisable.IsSupported()); err != nil { - return err + if req.MCPHTTPServers == nil { + return nil } - if err := req.ValidateToolSearch(caps.ToolSearch.IsSupported()); err != nil { - return err + for _, server := range *req.MCPHTTPServers { + if err := server.ValidateConnectionOrigin(req); err != nil { + return err + } + if endpoint, err := url.Parse(server.ServerURL); server.BearerToken != nil && (err != nil || endpoint.Scheme != "https" || endpoint.Hostname() == "") { + return errors.New("authenticated HTTP MCP requires HTTPS") + } } - if req.LocalEnvironment != nil && !caps.LocalEnvironment.IsSupported() { - return errors.New("engine does not support this local Environment configuration") - } - if req.DisableExecutionEnvironment && !caps.EnvironmentNone.IsSupported() { - return errors.New("engine does not support execution environment none") - } - return validateMCPHTTP(req, caps) + return nil } diff --git a/apps/daemon/internal/dispatch/executor.go b/apps/daemon/internal/dispatch/executor.go index f08407787..72b7c7e56 100644 --- a/apps/daemon/internal/dispatch/executor.go +++ b/apps/daemon/internal/dispatch/executor.go @@ -16,7 +16,7 @@ import ( const executorIdleCapacity = 16 type executorState struct { - capabilities proto.AgentKindCapabilities + declaration proto.Declaration id, sessionID, environmentID, stateKey string fingerprint [32]byte native agent.Executor @@ -49,7 +49,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, if strings.TrimSpace(input.SessionID) == "" || req.RunID != "" || len(req.Input) != 0 || req.AgentStateKey != "agents-api-"+input.SessionID { return r.rejectPreparation(env, "invalid_configuration") } - caps, available := r.availableCapabilities(req.AgentKind) + declaration, available := r.registry.Declaration(req.AgentKind) if !available { return r.rejectPreparation(env, "resource_unavailable") } @@ -69,7 +69,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, if err != nil { return r.rejectPreparation(env, "invalid_configuration") } - if validateExecutionEnvironment(req, caps) != nil || len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported() { + if validateExecutionEnvironment(req) != nil || proto.ValidateSelection(declaration, req.Selection()) != nil { return r.rejectPreparation(env, "unsupported_configuration") } fingerprint, err := executorFingerprint(req) @@ -155,12 +155,12 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "executor_capacity") } ownerCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) - owner = &executorState{capabilities: caps, id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), stateKey: req.AgentStateKey, fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, prepared: make(chan struct{}), nativeID: req.AgentSessionID} + owner = &executorState{declaration: declaration, id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), stateKey: req.AgentStateKey, fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, prepared: make(chan struct{}), nativeID: req.AgentSessionID} r.executors[input.SessionID] = owner r.log.Info("executor owner_created", "executor_id", owner.id, "session_id", owner.sessionID) } operation, cancel := context.WithCancel(context.WithoutCancel(ctx)) - p := &preparationState{capabilities: owner.capabilities, request: proto.Envelope{ID: env.ID, Trace: env.Trace, Assignment: env.Assignment}, fingerprint: requestFingerprint, ctx: operation, cancel: cancel, environmentID: req.EnvironmentID(), executor: owner, owns: true, busy: !reused, deadline: time.Now().Add(r.preparationTimeout)} + p := &preparationState{declaration: owner.declaration, request: proto.Envelope{ID: env.ID, Trace: env.Trace, Assignment: env.Assignment}, fingerprint: requestFingerprint, ctx: operation, cancel: cancel, environmentID: req.EnvironmentID(), executor: owner, owns: true, busy: !reused, deadline: time.Now().Add(r.preparationTimeout)} state := "preparing" if reused { state = "ready" diff --git a/apps/daemon/internal/dispatch/functions.go b/apps/daemon/internal/dispatch/functions.go index 026aa6b9a..6277ef7f3 100644 --- a/apps/daemon/internal/dispatch/functions.go +++ b/apps/daemon/internal/dispatch/functions.go @@ -64,8 +64,8 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e defer finishOperation() ctx, stop := r.shutdownContext(ctx) defer stop() - if !state.capabilities.FunctionTools.IsSupported() { - return r.sendInteractionDecisionAck(ctx, env, result.DeliveryID, false, "unsupported", "The runtime declaration does not support function results.") + if err := proto.ValidateSelection(state.declaration, proto.Selection{FunctionResult: &result}); err != nil { + return r.sendInteractionDecisionAck(ctx, env, result.DeliveryID, false, "unsupported", err.Error()) } if err := session.SubmitFunctionResult(ctx, result); err != nil { code := "runtime_error" diff --git a/apps/daemon/internal/dispatch/functions_native_test.go b/apps/daemon/internal/dispatch/functions_native_test.go index d1a1a69f6..2d0a04007 100644 --- a/apps/daemon/internal/dispatch/functions_native_test.go +++ b/apps/daemon/internal/dispatch/functions_native_test.go @@ -109,7 +109,7 @@ func TestNativeFunctionBridge(t *testing.T) { })) defer model.Close() reg := agent.NewRegistry() - registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}, codex.NewExecutorFactory()) + registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}, codex.NewExecutorFactory()) sender := make(nativeFunctionSender, 256) ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second) defer cancel() diff --git a/apps/daemon/internal/dispatch/functions_test.go b/apps/daemon/internal/dispatch/functions_test.go index 2c6474ddc..6559f0400 100644 --- a/apps/daemon/internal/dispatch/functions_test.go +++ b/apps/daemon/internal/dispatch/functions_test.go @@ -38,7 +38,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { reg := agent.NewRegistry() sender := &recSender{} sessions := map[string]*functionSession{} - registerSession(reg, proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(reg, proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported})}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { s := &functionSession{fakeSession: &fakeSession{out: out, ctx: ctx, closeOutOnCancel: true}} sessions[p.RunID] = s return s, nil diff --git a/apps/daemon/internal/dispatch/local_directory_test.go b/apps/daemon/internal/dispatch/local_directory_test.go index 730b48893..c2a8e1aad 100644 --- a/apps/daemon/internal/dispatch/local_directory_test.go +++ b/apps/daemon/internal/dispatch/local_directory_test.go @@ -14,7 +14,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/google/uuid" ) @@ -28,7 +27,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing } var harnessCalls atomic.Int32 reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, prototest.ModelConfiguration()) reg.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { harnessCalls.Add(1) return nil, errors.New("must not prepare a harness") @@ -100,7 +99,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { t.Fatal(err) } reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, prototest.ModelConfiguration()) reg.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, errors.New("must not prepare a harness") }) diff --git a/apps/daemon/internal/dispatch/mcp_http.go b/apps/daemon/internal/dispatch/mcp_http.go deleted file mode 100644 index c0cbe9add..000000000 --- a/apps/daemon/internal/dispatch/mcp_http.go +++ /dev/null @@ -1,37 +0,0 @@ -package dispatch - -import ( - "errors" - "net/url" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// Validate combined placement and authentication before factory selection. -func validateMCPHTTP(req proto.PromptRequestPayload, caps proto.AgentKindCapabilities) error { - if req.MCPHTTPServers == nil { - return nil - } - for _, server := range *req.MCPHTTPServers { - if err := server.ValidateConnectionOrigin(req); err != nil { - return err - } - if !caps.MCPHTTPTools.IsSupported() { - return errors.New("engine does not support HTTP MCP") - } - if server.Required && !caps.MCPHTTPRequired.IsSupported() { - return errors.New("engine does not support required HTTP MCP initialization") - } - if server.BearerToken == nil { - continue - } - if !caps.MCPHTTPTools.IsSupported() || !caps.MCPHTTPBearerAuth.IsSupported() { - return errors.New("engine does not support authenticated HTTP MCP") - } - endpoint, err := url.Parse(server.ServerURL) - if err != nil || endpoint.Scheme != "https" || endpoint.Hostname() == "" { - return errors.New("authenticated HTTP MCP requires HTTPS") - } - } - return nil -} diff --git a/apps/daemon/internal/dispatch/preparation.go b/apps/daemon/internal/dispatch/preparation.go index 151e41f41..5f31485a5 100644 --- a/apps/daemon/internal/dispatch/preparation.go +++ b/apps/daemon/internal/dispatch/preparation.go @@ -18,8 +18,8 @@ const preparationRecords = 64 // All mutable fields are protected by Router.mu. owns includes resources whose // cancellation is underway; a slow close cannot bypass the capacity bound. type preparationState struct { - capabilities proto.AgentKindCapabilities - executor *executorState + declaration proto.Declaration + executor *executorState // request is the execution_prepare's ID, trace and assignment, which // every status echoes. request proto.Envelope @@ -46,7 +46,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if !req.WorkspaceReadOnly { return r.handleExecutorPrepare(ctx, env, input) } - caps, available := r.availableCapabilities(req.AgentKind) + declaration, available := r.registry.Declaration(req.AgentKind) if !available { return r.rejectPreparation(env, "resource_unavailable") } @@ -56,7 +56,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if code != "" { return r.rejectPreparation(env, code) } - if environment == nil || !caps.LocalEnvironment.IsSupported() || !proto.ValidWorkspaceReadPreparation(req) { + if environment == nil || !declaration.Capabilities.LocalEnvironment.IsSupported() || !proto.ValidWorkspaceReadPreparation(req) { return r.rejectPreparation(env, "unsupported_read_preparation") } req, err := environment.Configure(req) @@ -66,7 +66,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if req.RunID != "" || len(req.Input) != 0 || req.EnvironmentID() == "" || strings.TrimSpace(req.AgentStateKey) == "" { return r.rejectPreparation(env, "invalid_configuration") } - if validateExecutionEnvironment(req, caps) != nil || (len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported()) { + if validateExecutionEnvironment(req) != nil || proto.ValidateSelection(declaration, req.Selection()) != nil { return r.rejectPreparation(env, "unsupported_configuration") } encoded, err := json.Marshal(req) @@ -109,7 +109,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) return r.rejectPreparation(env, "preparation_capacity") } owner, cancel := context.WithCancel(context.WithoutCancel(ctx)) - p := &preparationState{capabilities: caps, request: proto.Envelope{ID: env.ID, Trace: env.Trace, Assignment: env.Assignment}, fingerprint: fingerprint, ctx: owner, cancel: cancel, environmentID: req.EnvironmentID(), busy: true, owns: true, deadline: time.Now().Add(r.preparationTimeout)} + p := &preparationState{declaration: declaration, request: proto.Envelope{ID: env.ID, Trace: env.Trace, Assignment: env.Assignment}, fingerprint: fingerprint, ctx: owner, cancel: cancel, environmentID: req.EnvironmentID(), busy: true, owns: true, deadline: time.Now().Add(r.preparationTimeout)} p.status = proto.PreparationStatusPayload{Handle: uuid.NewString(), Revision: 1, State: "preparing", ExpiresAt: p.deadline.UnixMilli()} r.preparations[p.status.Handle], r.preparationRequests[p.request.ID] = p, p p.timer = time.AfterFunc(r.preparationTimeout, func() { r.releasePreparation(p, "expired", "", true) }) diff --git a/apps/daemon/internal/dispatch/preparation_start.go b/apps/daemon/internal/dispatch/preparation_start.go index dd803d938..557234eca 100644 --- a/apps/daemon/internal/dispatch/preparation_start.go +++ b/apps/daemon/internal/dispatch/preparation_start.go @@ -78,7 +78,7 @@ func (r *Router) handleExecutionStart(_ context.Context, env proto.Envelope) err } p.status.State, p.status.RunID, p.status.Revision = "starting", input.RunID, p.status.Revision+1 p.startFingerprint, p.busy = fingerprint, true - state := &sessionState{assignment: env.Assignment, capabilities: p.capabilities, runID: input.RunID, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, traceparent: env.Trace} + state := &sessionState{assignment: env.Assignment, declaration: p.declaration, runID: input.RunID, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, traceparent: env.Trace} state.preparedHandoff = newPreparedHandoff(p, owner.native) p.handoff, owner.run = state.preparedHandoff, state r.sessions[input.RunID] = state diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index dd01a2a34..12bd21717 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -121,7 +121,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory preparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, prototest.ModelConfiguration()) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported})}, prototest.ModelConfiguration()) reg.RegisterExecutor("prepared", preparationExecutorFixture(factory)) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, PreparationTimeout: timeout, Environments: preparationWorkspace(t).Resolve}) if err != nil { diff --git a/apps/daemon/internal/dispatch/router.go b/apps/daemon/internal/dispatch/router.go index fc4e53368..138ffe3be 100644 --- a/apps/daemon/internal/dispatch/router.go +++ b/apps/daemon/internal/dispatch/router.go @@ -69,7 +69,7 @@ type appliedFunctionResult struct { // frontend → server → daemon → agent → server attribution. type sessionState struct { assignment proto.AssignmentRef - capabilities proto.AgentKindCapabilities + declaration proto.Declaration runID string environmentID string session agent.Turn diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 93a2acf69..f0fd3d247 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -167,7 +167,7 @@ func run() error { registry := agent.NewRegistry() h := &harness{history: map[string]string{}} registry.RegisterKind(proto.SupportedAgentKind{Kind: "mcode", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, + EnvironmentNone: proto.CapabilitySupported, })}, mcode.Configuration()) registry.RegisterExecutor("mcode", h.prepare) sink := &sender{encoder: json.NewEncoder(os.Stdout)} diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index b4d99caac..bf60e712d 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -55,7 +55,7 @@ Both placements run the same Runtime: the daemon, the selected Harness, native t ### Hosted (`openai_hosted`) -The deployment's configured Sandbox Provider (E2B, Docker or microsandbox, see [sandbox deployment](./sandbox-deployment.md)) hosts the Environment. [Harness capabilities](./harness-capabilities.md) lists which Harnesses run there. +The deployment's configured Sandbox Provider (E2B, Docker or microsandbox, see [sandbox deployment](./sandbox-deployment.md)) hosts the Environment. Every Harness whose declaration supports `local_environment` runs there ([Declare support](./harness-onboarding.md#declare-support)). - Session creation, with or without initial input, commits the Session, Environment and retry identity before the Worker provisions compute. A creation interrupted before bootstrap is recovered without repeating the Provider's Create. - Provisioning needs no caller action; the Session stays idle until a Turn starts. @@ -362,9 +362,7 @@ The shared parser accepts HTTP `url`, `bearer_token_env_var` and literal `http_h A stdio server starts through the daemon's stdio helper, which resolves the installed declaration and launches the command with the Harness's permissions. On Unix the helper replaces itself with the server; on Windows it forwards stdio inside the owned process tree. Initialized values override the declaration's variables. Process groups and Windows Jobs own cancellation and descendant cleanup, not isolation. -[Harness capabilities](./harness-capabilities.md#environment-preparation) owns the supported Plugin transports and per-Harness limits. - -Environment MCP needs enabled network. Duplicate server identities are rejected. Claude rejects literal headers because the pinned client expands them again and forwards custom headers across origins. MiniMax ACP HTTP declarations stay in session-local native memory; tokens never enter native configuration files or process arguments. Required initialization and tool allowlists cannot be set through the Plugin manifest. +Environment MCP needs enabled network. Duplicate server identities are rejected. Claude rejects literal headers because the pinned client expands them again and forwards custom headers across origins, and MiniMax Code rejects them too. MiniMax ACP HTTP declarations stay in session-local native memory; tokens never enter native configuration files or process arguments. Required initialization and tool allowlists cannot be set through the Plugin manifest. ### Effective bindings @@ -379,9 +377,7 @@ An Agent's HTTP MCP tool ([declaration](./execution-tools.md#http-mcp)) has a `c | `service` | Core's service-side execution host | `none` only | | `environment` | The Environment's workspace | `openai_hosted` and `self_hosted` with enabled network | -Core's Harness profile declares `MCPOrigins`; admission and dispatch check the origin against the placement and the Runtime's advertised HTTP, bearer and required-initialization capabilities, and the Runtime validates the origin again before invoking an adapter. No Harness-name or Provider branch selects a different path. - -[Harness capabilities](./harness-capabilities.md#tools) owns per-Harness origin support and policy limits. +Each Harness declares the origins it supports ([Declare support](./harness-onboarding.md#declare-support)); MiniMax Code supports `environment` only. Admission and dispatch check the origin against the placement and the Runtime's declared HTTP, bearer and required-initialization support, and the Runtime validates the selection again before invoking an adapter. No Harness-name or Provider branch selects a different path. Both origins support anonymous HTTP and HTTPS bearer credentials. The attached-Vault selection freezes the credential identity, including a unique implicit URL match or an anonymous selection. Only that Project-authorized credential enters the transient Runtime request; Core defaults and unrelated Vaults are never searched. A decryption failure or missing credential fails execution without an anonymous fallback. Public Environment MCP keeps `project_vault` authority and Plugin credentials keep `environment_configuration` authority; neither overrides a duplicate server label. Bearers never enter persisted native configuration or process arguments. diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index 36ace1a5c..eefe52fc7 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -2,13 +2,13 @@ title: "Execution tools" --- -An Agent declares application functions, controls and MCP servers in `tools`, and an optional output schema in `text.format`. This contract states how Core validates each declaration, what crosses the Runtime boundary and how callers recover required actions. [Harness capabilities](./harness-capabilities.md) lists which Harness supports each operation on which placement. Native workspace tools and Environment Plugin MCP are part of the [Environment](./environments.md#skills-plugins-and-environment-mcp). +An Agent declares application functions, controls and MCP servers in `tools`, and an optional output schema in `text.format`. This contract states how Core validates each declaration, what crosses the Runtime boundary and how callers recover required actions. Each Harness's [declaration](./harness-onboarding.md#declare-support) states which of these it supports. Native workspace tools and Environment Plugin MCP are part of the [Environment](./environments.md#skills-plugins-and-environment-mcp). ## Admission - Saved Agents keep every pinned tool declaration as resource data. Saving never qualifies execution. -- Session creation resolves saved references and inline declarations with the execution parser into the immutable Session snapshot, then checks the combination against the selected Harness's profile in `services/core/internal/engine`. An unsupported combination returns 400 `unsupported_or_invalid_configuration` before anything is written. Protocol errors, such as a repeated `web_search` or `tool_search` or a non-object schema root, use the official error fields ([validation](./wire-semantics.md#configuration-validation)). -- Before dispatch, the selected Runtime must also advertise the operation's capability. An advertisement alone never enables an operation. +- Session creation resolves saved references and inline declarations with the execution parser into the immutable Session snapshot, then checks the combination against the selected Harness's declaration. An unsupported combination returns 400 `unsupported_or_invalid_configuration`, with the rejected field as `param`, before anything is written. Protocol errors, such as a repeated `web_search` or `tool_search` or a non-object schema root, use the official error fields ([validation](./wire-semantics.md#configuration-validation)). +- Before dispatch, the selected Runtime's heartbeat must also declare the operation. A heartbeat only narrows the static declaration and never enables an operation. - The native Harness runs the model and tool loop. Core adds no second loop, output repair, schema coercion or prompt wrapper, and selects no native tool names. ## Functions @@ -39,7 +39,7 @@ The Worker fails previously claimed work without replay after execution loss; qu `text.format` accepts `{type: "json_schema", schema: {...}}`, the Agents API form: it has no `name`, `strict` or other Responses API wrapper fields. The schema is saved, inherited through Agent and Session resolution and frozen in the Session snapshot. An explicit non-object root type is a protocol error on save and Session creation for every Harness. Claude requires an explicit `type: "object"` at the schema root. The Claude SDK reads JSON numbers as binary64, so Session admission rejects schemas whose numbers would change in that conversion; saved Agents keep them unchanged. -Core carries the schema in `ExecutionControls.OutputFormat` and requires the profile's structured-output qualification plus the Runtime's `structured_output` and message-observation capabilities, only for requests that use the option. Frozen schemas reach preparation before input and apply to initial and resumed execution; Start cannot replace them. +Core carries the schema in `ExecutionControls.OutputFormat` and requires `structured_output` in the Harness's declaration and the Runtime's heartbeat, only for requests that use the option. Frozen schemas reach preparation before input and apply to initial and resumed execution; Start cannot replace them. The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. A matching live root tool result and an attributed successful SDK result confirm the output. The adapter publishes the native `result.result` string unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Unvalidated retries and cancelled candidates never become the answer, and the adapter never serializes `structured_output` back to JSON. The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`. @@ -47,7 +47,7 @@ The Claude adapter passes `outputFormat` to the pinned SDK and allows its native A `tool_search` tool has only `type`; Responses-only execution fields are rejected. Function `defer_loading` marks which definitions load lazily. Discovery requires both: `tool_search` without a deferred function, or deferred functions without `tool_search`, is rejected. The saved-Agent tool union keeps `tool_search`; the pinned Session response union omits it, so Session and SSE resources project it out while the frozen configuration keeps it. The pinned Items union has no tool-search Item, and Core invents none. -Core sends `PromptRequestPayload.ToolSearch` and each `FunctionTool.DeferLoading`, and requires the profile's qualification plus the Runtime's `tool_search` capability. Native search and lazy schema loading belong to the adapter. The Claude adapter's MCP server marks eager definitions `anthropic/alwaysLoad:true` and deferred ones false, and enables native ToolSearch; the function profile allows only declared callbacks and ToolSearch besides the selected workspace tools. A workspace Runtime derives `tool_search` from the bridge's `workspace_tool_search` feature. The native Harness owns model and provider policy; known conflicting modes and beta settings reject in the adapter, and the SDK gives no reliable pre-input signal that deferral took effect after an opaque policy change. +Core sends `PromptRequestPayload.ToolSearch` and each `FunctionTool.DeferLoading`, and requires `tool_search` in the Harness's declaration and the Runtime's heartbeat. Native search and lazy schema loading belong to the adapter. The Claude adapter's MCP server marks eager definitions `anthropic/alwaysLoad:true` and deferred ones false, and enables native ToolSearch; the function profile allows only declared callbacks and ToolSearch besides the selected workspace tools. A workspace Runtime derives `tool_search` from the bridge's `workspace_tool_search` feature. The native Harness owns model and provider policy; known conflicting modes and beta settings reject in the adapter, and the SDK gives no reliable pre-input signal that deferral took effect after an opaque policy change. ## Web search and programmatic tool calling @@ -62,7 +62,7 @@ Saved Agents keep every pinned `web_search` mode: omitted or null is saved as `l Execution admits only `mode: "disabled"` and `enabled: false`. Enabled or omitted-mode search and enabled or omitted-`enabled` programmatic calling are rejected at Session admission unless the Session replaces the saved tools. Omitting programmatic configuration keeps each Harness's native behavior, which differs from the official default-on behavior. Unrelated native utility tools are not removed. -`DisableProgrammaticToolCalling` carries the disabled intent on initial execution and cold continuation and requires the Runtime capability only when present. Search uses the existing disabled control. +`DisableProgrammaticToolCalling` carries the disabled intent on initial execution and cold continuation. Every Harness runs with native web search disabled. | Harness | Native enforcement | | --- | --- | @@ -84,10 +84,10 @@ Execution admits only `mode: "disabled"` and `enabled: false`. Enabled or omitte ``` - `server_label` is nonempty and unique within the Session. Only the `http` transport is accepted; `server_url` is an absolute HTTP or HTTPS URL without credentials, query or fragment. Nonempty `headers` and `request_metadata` and an inline `authorization` are rejected. -- [Public MCP connection origin](./environments.md#public-mcp-connection-origin) owns origin defaults, placement and credential authority; [Harness capabilities](./harness-capabilities.md#tools) owns per-Harness support. +- [Public MCP connection origin](./environments.md#public-mcp-connection-origin) owns origin defaults, placement and credential authority. - Omitted or null `allowed_tools` permits every server tool; `[]` permits none. - `required: true` makes native thread creation and cold resume wait for the server to initialize; a failure stops execution without replacing retained history. It needs the Runtime's `mcp_http_required` capability. Public work can be accepted or queued during the wait. - Bearer authentication uses an attached static or OAuth Vault credential. [Vault credentials](./vaults.md) owns selection, and [MCP credential authority](./environments.md#public-mcp-connection-origin) owns the frozen Runtime binding. Authenticated execution requires `mcp_http_bearer_auth`. -- The Runtime must advertise `mcp_http_tools`. The native Harness owns discovery, calls and results; public `mcp_call` Items use the original server and tool names and keep the observed native result. +- The Harness's declaration and the Runtime's heartbeat must support `mcp_http_tools`. The native Harness owns discovery, calls and results; public `mcp_call` Items use the original server and tool names and keep the observed native result. Codex verifies the exact effective MCP configuration before starting or resuming a thread, excludes undeclared servers, disables native apps and plugins, and rejects reserved native labels and stored native MCP credentials. Claude accepts labels of ASCII letters, digits, underscore and hyphen except `functions`, tool names that may also contain dots, and requires connected servers with static inventories. Anonymous Claude requests send a blank Authorization header to suppress native OAuth injection. Native OAuth login is not supported. diff --git a/contracts/agents-api/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md deleted file mode 100644 index c1ad61d8f..000000000 --- a/contracts/agents-api/harness-capabilities.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: "Harness capabilities" ---- - -This page lists what each Harness supports on each placement. Core decides admission from the Harness's engine profile in `services/core/internal/engine`, and the Runtime that runs the Session must also advertise the operation. The linked contracts define each operation; [Harness onboarding](./harness-onboarding.md#qualify-the-adapter) describes how a Harness is qualified. - -| Status | Meaning | -| --- | --- | -| Verified | Core admits it, and real-model acceptance through the pinned official client passed on that placement | -| Admitted | Core admits it through the same Runtime path, but no real-model acceptance has run on that placement | -| Rejected | Core rejects the request before execution | - -Placements are `none` (no Environment), hosted (`openai_hosted`) and self-hosted (`self_hosted`). Hosted acceptance ran on Docker nodes; E2B and microsandbox run the same Runtime and adapters, and every Verified hosted cell counts as Admitted there. Self-hosted acceptance ran on Linux machines; the [self-hosted guide](../../docs/getting-started/self-hosted.md#platforms) lists the supported platforms. A verified operation is verified on its own, not in every combination with other options; combinations that a profile rejects are listed in the operation's contract. - -## Execution and input - -| Operation | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| Text Turns, active input, cancellation, restart and continuation | Verified on all placements | Verified on all placements | Verified on all placements | -| [Files and Artifacts](./environment-files.md) | Verified: hosted, self-hosted | Verified: hosted, self-hosted | Verified: hosted, self-hosted | -| [Whitespace-only message text](./message-content.md) | Admitted; delivered unchanged | Rejected | Rejected | -| [Inline PNG and JPEG message images](./message-content.md) | Verified on all placements | Verified on all placements | Rejected | -| Remote image URLs | Rejected | Rejected | Rejected | -| Explicit `reasoning`; `service_tier` other than `auto` | Rejected | Rejected | Rejected | -| `text.verbosity` other than `medium` | Admitted; the native model decides | Rejected | Rejected | -| [Public token usage](./sessions-events.md) | Measured counters | Null | Null | - -Native model parameters and provider protocols per Harness are in [model execution](./model-execution.md). - -## Tools - -| Operation | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| [Public functions](./execution-tools.md#functions) with text results | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; object-root schemas only | Rejected | -| [Function results with images](./message-content.md#function-results) | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; successful inline PNG or JPEG results only | Rejected | -| [Structured output](./execution-tools.md#structured-output) | Rejected | Verified on all placements | Rejected | -| [Deferred function discovery](./execution-tools.md#deferred-function-discovery) | Rejected | Verified: `none`, self-hosted; admitted: hosted | Rejected | -| [Disabled web search and programmatic tool calling](./execution-tools.md#web-search-and-programmatic-tool-calling) | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | -| Enabled web search or programmatic tool calling | Rejected | Rejected | Rejected | -| [Service-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: `none`; rejected elsewhere | Verified: `none`; rejected elsewhere | Rejected | -| [Environment-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: hosted, self-hosted | Verified: hosted, self-hosted | Verified: hosted, self-hosted; `allowed_tools` null and `required` false only | -| [HTTP MCP bearer credentials](./execution-tools.md#http-mcp) | Verified | Verified | Verified | -| Required MCP initialization | Verified | Verified | Rejected | -| [Subagents](./subagents.md) | Verified: hosted; admitted: `none`, self-hosted | Verified: hosted; admitted: `none`, self-hosted | Verified: hosted; admitted: `none`, self-hosted | -| Subagents together with functions or HTTP MCP | Rejected | Rejected | Rejected | - -Claude structured output requires a single Agent, medium verbosity and no Skills, Plugins, capability directories, MCP or `tool_search`. Claude deferred discovery requires a single Agent, only function tools besides `tool_search` and the disabled controls, no Skills, Plugins or capability directories and no structured output. Claude on a workspace placement needs the packaged bridge features for each operation (`workspace_functions`, `workspace_structured_output`, `workspace_tool_search`, `workspace_mcp_http`). - -## Environment preparation - -These operations need a workspace, so they apply to hosted and self-hosted placements only. - -| Operation | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| [Initial files, setup commands, Skills and Plugins](./environments.md#runtime-capability-preparation) | Verified: hosted, self-hosted | Verified: self-hosted; admitted: hosted | Verified: self-hosted; admitted: hosted | -| Capability directories | Admitted | Admitted | Admitted | -| npm and Python packages | Admitted | Admitted | Admitted | -| `packages.system` | Rejected | Rejected | Rejected | -| Network `disabled` or `restricted` | Rejected | Rejected | Rejected | -| [Plugin MCP over stdio](./environments.md#plugin-mcp) | Verified: hosted, self-hosted | Verified: self-hosted; admitted: hosted | Verified: self-hosted; admitted: hosted | -| Plugin MCP over HTTP | Admitted, with literal headers or HTTPS bearer | Admitted, anonymous or HTTPS bearer | Verified: self-hosted; admitted: hosted; anonymous or HTTPS bearer | diff --git a/contracts/agents-api/harness-catalog.md b/contracts/agents-api/harness-catalog.md index 69a04c911..96548b11d 100644 --- a/contracts/agents-api/harness-catalog.md +++ b/contracts/agents-api/harness-catalog.md @@ -3,10 +3,10 @@ The authored registration list is [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json). Change it and run `make generate-harness-catalog`; `make check-harness-catalog` checks the generated projections. -| Identifier | Display name | Model configuration declaration | Core qualification constructor | -| --- | --- | --- | --- | -| `claude_sdk` | Claude Code | `claudesdk.Configuration` | `claudeProfile` | -| `codex` | Codex | `codex.Configuration` | `codexProfile` | -| `mcode` | MiniMax Code | `mcode.Configuration` | `mcodeProfile` | +| Identifier | Display name | Declaration | +| --- | --- | --- | +| `claude_sdk` | Claude Code | `claudesdk.Configuration` | +| `codex` | Codex | `codex.Configuration` | +| `mcode` | MiniMax Code | `mcode.Configuration` | -Configuration declarations live in `internal/harnessconfig/` and qualification constructors in `services/core/internal/engine`. These registrations describe the build. Which Harnesses a deployment enables is the `core.harnesses` [process setting](../../docs/configuration.md#settings), [Harness capabilities](./harness-capabilities.md) lists what each Harness supports, and a connected Runtime reports its own availability. [Harness onboarding](./harness-onboarding.md) describes the adapter and packaging steps. +Each declaration in `internal/harnessconfig/` states the Harness's model providers and its support, the only source Core and the Runtime admit a selection against. These registrations describe the build. Which Harnesses a deployment enables is the `core.harnesses` [process setting](../../docs/configuration.md#settings), and a connected Runtime reports its own availability. [Harness onboarding](./harness-onboarding.md) describes the adapter and packaging steps. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 00b1e6254..696e3a614 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -2,7 +2,7 @@ title: "Add a Harness" --- -A **Harness** is a native agent engine (Codex, Claude Code, MiniMax Code) that runs the model and tool loop. A **Harness adapter** translates the Runtime's Executor and Turn contract into that engine's SDK or protocol. This document is the Runtime–Harness protocol: the adapter interfaces and their lifecycle obligations, registration, Core qualification and acceptance. [Harness capabilities](./harness-capabilities.md) records what each current Harness supports. +A **Harness** is a native agent engine (Codex, Claude Code, MiniMax Code) that runs the model and tool loop. A **Harness adapter** translates the Runtime's Executor and Turn contract into that engine's SDK or protocol. This document is the Runtime–Harness protocol: the adapter interfaces and their lifecycle obligations, registration, the support declaration and acceptance. Start from two entry points: @@ -29,8 +29,8 @@ Runtime: Executor preparation, reuse, idle expiry, recovery | Runtime | Authenticated connection, shared capability preparation and the common Executor and Turn lifecycle | `apps/daemon/internal/dispatch` | | Adapter | Native configuration, resources, API calls, event translation and restrictions | `apps/daemon/internal/agent/` | | Harness | Native model and tool loop and history | Pinned SDK or executable | -| Service profile | Pure validation of qualified operations and placements | `services/core/internal/engine` | -| Registration | Adapter declarations, installed factories and verified capabilities | `apps/daemon/internal/agent//declaration.go`; static list in `apps/daemon/internal/cli/agent_discovery.go` | +| Declaration | The Harness's support, against which Core and the Runtime admit each selection | `internal/harnessconfig/` | +| Registration | Adapter declarations, installed factories and the installation's narrowed support | `apps/daemon/internal/agent//declaration.go`; static list in `apps/daemon/internal/cli/agent_discovery.go` | An Environment supplies execution resources. Managed E2B, Docker and microsandbox machines and application-owned machines differ in provisioning and connection; the connected Runtime uses this same contract. The daemon runs on Linux, macOS and Windows, managed Providers are Linux-only, and each adapter qualifies its own platforms ([self-hosted platforms](../../docs/getting-started/self-hosted.md#platforms)). Native factories receive capabilities only after the Runtime has loaded the bound installed snapshot ([capability preparation](./environments.md#runtime-capability-preparation)). Model providers supply model communication settings, not Turn scheduling or native process ownership. @@ -38,22 +38,21 @@ An Environment supplies execution resources. Managed E2B, Docker and microsandbo 1. **Pin the native source.** Record the upstream package version and source revision and document the native entry point next to the adapter. 2. **Implement the adapter** in `apps/daemon/internal/agent/`: an `ExecutorFactory`, an `Executor` and a `Turn` ([required interfaces](#required-adapter-interfaces), [lifetimes](#executor-and-turn-lifetimes)). Reuse the shared process, credential, configuration and local workspace helpers. -3. **Declare the kind in the adapter** and add its declaration to the Runtime’s static list in `apps/daemon/internal/cli/agent_discovery.go` ([register the adapter](#register-the-adapter)). -4. **Add the service profile and one catalog entry** ([add the engine to Core](#add-the-engine-to-core)). -5. **Package native prerequisites.** Add a Runtime image under `services/core/deploy/` and, optionally, [native installer participation](#native-installer-participation). -6. **Enable and select the engine** with the `core.harnesses` setting and [Harness selection](./model-execution.md#harness-selection). -7. **Qualify it** ([qualify the adapter](#qualify-the-adapter)) and record the result in [Harness capabilities](./harness-capabilities.md). +3. **Declare its support and register it.** Declare the support in `internal/harnessconfig/` with one catalog entry ([declare support](#declare-support)), then declare the kind in the adapter and add it to the Runtime’s static list in `apps/daemon/internal/cli/agent_discovery.go` ([register the adapter](#register-the-adapter)). +4. **Package native prerequisites.** Add a Runtime image under `services/core/deploy/` and, optionally, [native installer participation](#native-installer-participation). +5. **Enable and select the engine** with the `core.harnesses` setting and [Harness selection](./model-execution.md#harness-selection). +6. **Qualify it** ([qualify the adapter](#qualify-the-adapter)) and record each native difference in the [coverage ledger](./index.md). Implement the mandatory text lifecycle and handle every extension explicitly. Qualify supported extensions one at a time; an unqualified extension returns `agent.ErrUnsupportedOperation` without native effects. A native cancellation may require retirement instead of reuse: `Reusable=false` carries a reason and the caller must confirm `Executor.Close`. Do not force reuse to fit a test helper, and do not copy an adapter's native limitations into the shared Core protocol. ## Architecture rules - Codex, Claude Code and future Harnesses have equal standing. The common Runtime wire protocol and Executor and Turn interfaces own lifecycle, input receipts, cancellation, recovery and resource access; each adapter keeps its native implementation and model and tool loop. -- A new engine supplies an adapter, a qualified profile, registration and an independently verified deployment. It adds no engine-name branches to API handlers, persistence, dispatch, scheduling or Environment providers, and no handler, store table, scheduler, event projector or model loop for capabilities the contract already represents. +- A new engine supplies an adapter, its declaration, registration and an independently verified deployment. It adds no engine-name branches to API handlers, persistence, dispatch, scheduling or Environment providers, and no handler, store table, scheduler, event projector or model loop for capabilities the contract already represents. - Keep required lifecycle declarations, extension interfaces and registration methods in `agent/harness.go`. Result types, errors and Registry storage may stay in focused files. -- Use the existing `proto.SupportedAgentKind` and `AgentKindCapabilities` schema. Do not add a second capability descriptor or a combined optional interface. +- Use the existing `proto.Declaration` and `proto.SupportedAgentKind` schema. Do not add a second capability descriptor or a combined optional interface. - Onboarding does not require feature equality. Harnesses need not match each other's optional features, and MCP, functions, images or verbosity control are not required to register. Verify the common lifecycle obligations and use the same public assertions for each declared operation. An omitted declaration or a missing extension implementation blocks onboarding; a native difference does not. -- The service profile catalog is the qualification boundary. Unknown profiles fail closed, and a Runtime heartbeat cannot authorize new public functionality. Schema validity, service qualification and the available Runtime are independent checks. +- The static declaration is the support boundary. Unknown kinds fail closed, and Core rejects a heartbeat that widens a declaration. Schema validity, the declaration and the available Runtime are independent checks. - Never equate accepted parameters with applied native behavior. ## Required adapter interfaces @@ -84,9 +83,9 @@ The reason is a fixed safe string, never submitted content, a credential or raw The wire request carries no working directory. The Runtime checks `local_environment.workspace_directory` against its binding and gives the Harness its bound workspace directory in `LocalEnvironment.WorkspaceRoot`; run the native Harness there. -Workspace reads, writes, output export and read-only preparation belong to the Session's [Environment owner](../../docs/runtime-protocol.md#session-assignments), not the adapter. An adapter implements none of them and declares `WorkspaceReadPreparation` and `WorkspaceOutputExport` unsupported. It declares `LocalEnvironment` and `EnvironmentNone` as what its Executors run, and `agent.Registry.Register` composes the declaration once with what the Runtime's owner serves (`agent.EnvironmentSupport`): it keeps each only where the owner serves it and sets both export fields to the composed `LocalEnvironment`. One declaration holds for every Executor of the install, including its [view](#run-in-an-agent-host-view). +Workspace reads, writes, output export and read-only preparation belong to the Session's [Environment owner](../../docs/runtime-protocol.md#session-assignments), not the adapter. An adapter implements none of them. Its declaration's `LocalEnvironment` and `EnvironmentNone` state what its Executors run, and `agent.Registry.Register` composes them once with what the Runtime's owner serves (`agent.EnvironmentSupport`), keeping each only where the owner serves it. The composed `LocalEnvironment` also admits the owner's workspace reads, read-only preparation and output export. One declaration holds for every Executor of the install, including its [view](#run-in-an-agent-host-view). -The service profile qualifies public combinations and the Runtime advertises the installed combination; neither replaces schema validation or Project authorization. Native behavior tests must agree with the declarations. An advertised operation that returns Unsupported is a contract violation, never success or grounds for replay. +The declaration states the public combinations the Harness supports and the heartbeat narrows it to the installation; neither replaces schema validation or Project authorization. Native behavior tests must agree with the declarations. An advertised operation that returns Unsupported is a contract violation, never success or grounds for replay. ## Executor and Turn lifetimes @@ -127,17 +126,17 @@ Initial input and steering use ordered `proto.MessageInput`. Keep user-message a ### Required and extension operations -The public text path requires durable Turns, applied input receipts, ordered observations, cancellation and enforcement of disabled execution controls; `execution.Policy.engineCapabilities` holds the exact requirements. An engine without native tools can guarantee their absence; an engine with tools must actually disable them when asked. Accepting a configuration is not proof of enforcement. +The public text path requires durable Turns, applied input receipts, ordered observations, cancellation and enforcement of disabled execution controls. An engine without native tools can guarantee their absence; an engine with tools must actually disable them when asked. Accepting a configuration is not proof of enforcement. -MCP, public functions, deferred function discovery, structured output, image input, verbosity controls and other optional operations need not match another engine. Reject an unqualified combination with Unsupported and record the gap; never advertise a capability to bypass selection. +MCP, public functions, deferred function discovery, structured output, image input, verbosity controls and other optional operations need not match another engine. Declare what is unsupported, a combination as a `Conflicts` pair, and record the gap; never advertise a capability to bypass selection. -- Structured output: consume `ExecutionControls.OutputFormat` and publish confirmed native output through the Message contract ([execution tools](./execution-tools.md#structured-output)). Register the public qualification separately from the Runtime capability. -- Images: register the Runtime's `MessageImages` and qualify the profile's `MessageImages` separately ([message input](./message-content.md)). +- Structured output: consume `ExecutionControls.OutputFormat` and publish confirmed native output through the Message contract ([execution tools](./execution-tools.md#structured-output)). Declare `Binary64OutputSchema` when the native SDK reads JSON numbers as binary64. +- Images: declare `MessageImages` and `FunctionResultImages`, and whether function results admit image URLs and images in a failed result ([message input](./message-content.md)). - Workspace placements additionally need verified preparation, workspace reads and output export and the dedicated Runtime binding with the shared Files helpers. Enable a placement only after its lifecycle behavior is demonstrated. ### MCP origin and native limits -Declare supported public origins in the engine profile's `MCPOrigins` and bearer support in `MCPBearer`. The Runtime advertises its actual HTTP, bearer and required-initialization capabilities. Shared admission validates origin and placement; adapter validation keeps native label, allowlist and initialization limits. +Declare the supported public origins in `MCPOrigins`, HTTP, bearer and required-initialization support in the capabilities, and native limits as data: `MCPAllowedTools`, `ReservedMCPLabels` and the `MCPLabel` and `MCPToolName` patterns. `proto.ValidateSelection` checks them with the origin and placement, and the adapter does not check them again. Consume `agent.ResolveMCPBindings` for public and installed declarations, and keep origin, credential authority, null versus empty allowlists and required startup. Do not copy tokens into native profiles or reinterpret a service request as an Environment request. Reject unsupported native policies instead of dropping them. Follow the [MCP origin contract](./environments.md#public-mcp-connection-origin) and run public-client, failure, cancellation and cold-recovery qualification for each advertised combination. Model capability is separate from Harness transport support; never infer it from model names or silently degrade input. @@ -147,54 +146,35 @@ A Harness that supports the Subagent reads implements the [neutral observation c ## Register the adapter -Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go). The declaration contains the kind and complete capability descriptor, the shared model `Configuration` and a `Discover` function. Discovery receives the profile and diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor, Executor factory and view declaration. Return nil when the adapter is not configured; return an unavailable descriptor without an Executor factory or view when configured prerequisites fail. Keep version gates and factory-selection conditions inside the adapter. +Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go). The declaration contains the kind with the capabilities of the shared model `Configuration`'s declaration, that `Configuration` and a `Discover` function. Discovery receives the profile and diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor, Executor factory and view declaration. Return nil when the adapter is not configured; return an unavailable descriptor without an Executor factory or view when configured prerequisites fail. Keep version gates and factory-selection conditions inside the adapter; they only clear support. [`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) iterates the discovered runtimes and calls `Registry.Register` from `agent/harness.go`. It verifies that discovery retained the declared kind and registers the Runtime in this order: | Order | Method | Registers | | --- | --- | --- | -| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind, availability, version, `AgentKindCapabilities` and the model configuration declaration. It resets the other registrations, so call it first. | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution | +| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind, availability, version, `AgentKindCapabilities` and the model configuration, whose declaration it narrows to those capabilities; it panics on a widening. It resets the other registrations, so call it first. | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution. Its factory runs only for a request whose selection the narrowed declaration admits. | | 3 | `RegisterView(kind, agent.View)` | Optional: the agent-host view declaration from `Runtime.View`. It panics with `ErrInvalidView` when `View.Validate` fails. Its Executor factory validates the model configuration like `RegisterExecutor` and enforces the [gateway rule](#endpoints-and-proxy). | `Runtime.View` declares how the Harness runs in an agent-host Session view, described in [Run in an agent-host view](#run-in-an-agent-host-view). Every adapter sets it explicitly; `View: nil` means the agent host rejects the kind, and `Registry.ResolveView` returns an error wrapping `ErrUnsupportedOperation`. `TestPublicHarnessContractDeclarations` requires the field in each declaration. -Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySupported` or `proto.CapabilityUnsupported`, even for an unavailable Harness. `proto.CapabilityUnspecified` is invalid: zero values and omitted fields never mean Unsupported. An installation probe may set an individual field with `proto.CapabilityFromBool`; it must not populate unmentioned or future fields. Availability stays separate in `SupportedAgentKind.Available`. Registration validates the complete declaration before changing the registry, and the wire carries an explicit boolean for every field, so omitted and null fields are invalid. A new field requires a decision in every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion verifies implementation, never support. Every declaration must match the behavior verified for that installation; the [Core–Runtime protocol](../../docs/runtime-protocol.md#capability-declarations) owns how declarations travel and are frozen. +Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySupported` or `proto.CapabilityUnsupported`, even for an unavailable Harness. `proto.CapabilityUnspecified` is invalid: zero values and omitted fields never mean Unsupported. An installation probe may clear an individual field with `proto.CapabilityFromBool`; it never sets support the static declaration lacks. Availability stays separate in `SupportedAgentKind.Available`. Registration validates the complete declaration before changing the registry, and the wire carries an explicit boolean for every field, so omitted and null fields are invalid. A new field requires a decision in every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion verifies implementation, never support. Every declaration must match the behavior verified for that installation; the [Core–Runtime protocol](../../docs/runtime-protocol.md#capability-declarations) owns how declarations travel and are frozen. -Every available Harness implements, without a declaration, the shared Turn lifecycle, including durable `SteerWithReceipt` input and the Turn settlement contract that `contracttest.TextLifecycle` checks, typed `execution_controls` and tool observations. A Harness that cannot meet them on a platform reports `Available` false there. `FunctionTools` admits `SubmitFunctionResult`. `WorkspaceReadPreparation` admits `execution_prepare` with `workspace_read_only`, which the Environment owner readies and serves without calling the Executor factory. Runtime registration does not grant Core qualification; the service profile does. +Every available Harness implements, without a declaration, the shared Turn lifecycle, including durable `SteerWithReceipt` input and the Turn settlement contract that `contracttest.TextLifecycle` checks, typed `execution_controls` and tool observations. A Harness that cannot meet them on a platform reports `Available` false there. `FunctionTools` admits `SubmitFunctionResult`. `LocalEnvironment` admits `execution_prepare` with `workspace_read_only`, which the Environment owner readies and serves without calling the Executor factory. 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 +## Declare support -Core recognizes the [built-in Harness registrations](./harness-catalog.md). Add one entry to `internal/harnessconfig/builtin/catalog.json` with: +Core recognizes the [built-in Harness registrations](./harness-catalog.md). Add one entry to `internal/harnessconfig/builtin/catalog.json` with the public `kind`, the display `label` and the model `configuration` package under `internal/harnessconfig`, then run `make generate-harness-catalog`. It generates the model configuration registry, client identifiers and display names and the registration reference; public input validators read the generated registry. `make openapi` derives Harness enums from the same catalog, so do not add handwritten enums to DTO tags or route annotations. `make check-harness-catalog` rejects stale projections. -- the public `kind` and display `label`; -- the model `configuration` package under `internal/harnessconfig`; -- the `profile` constructor under `services/core/internal/engine`. +The `Declaration` of `Configuration()` in `internal/harnessconfig/` is the Harness's support: a `proto.Declaration` with its `AgentKindCapabilities`, its message, image, MCP and output-schema limits, and in `Conflicts` the feature pairs it supports alone but not together. It states the adapter's maximum support and is the only source: Core reads it through `builtin.Registry()`, and the adapter's Runtime descriptor starts from it. Discovery and the Environment owner only clear support, and Core rejects a heartbeat that widens it. Declare only real differences between Harnesses; a rule that holds for every Harness is a common check in `proto.ValidateSelection`. -Implement the profile constructor, then run `make generate-harness-catalog`. It generates the model configuration registry, Core profile catalog, client identifiers and display names and the registration reference; public input validators read the generated registry. `make openapi` derives Harness enums from the same catalog, so do not add handwritten enums to DTO tags or route annotations. `make check-harness-catalog` rejects stale projections. +`proto.ValidateSelection` is the only check of a declaration. Core applies the static declaration when an Agent with a saved Harness is created or updated, at Session creation and at input and function-result admission, and the Runtime's narrowed declaration at device selection and before it claims a Turn. The Runtime applies it when it admits an `execution_prepare`, before any Executor factory runs. A rejection is 400 `unsupported_or_invalid_configuration` with the configuration path as `param`. Runtime facts, such as a missing binary, native history or filesystem readiness, stay adapter preparation failures. -Each Runtime declaration references the same `internal/harnessconfig/.Configuration()` used by the generated catalog and owns its native factories, probes and installed capability evidence. The catalog cannot declare a machine's availability, and there is no dynamic plugin loader. +Each Runtime declaration references the same `internal/harnessconfig/.Configuration()` and owns its native factories and probes. The catalog cannot declare a machine's availability, and there is no dynamic plugin loader. -The profile is pure: it declares supported placements, public configuration and result limits and required Runtime controls, using existing public and protocol types. Profile callbacks cannot query business data, decrypt credentials or control native processes. Shared dispatch checks capability combinations, not a whitelist of engine names. - -### Explicit service qualification - -`engine.Profile` is the service's qualification declaration, separate from the Runtime's `AgentKindCapabilities`. Its capability fields reuse the small `proto.CapabilitySupport` value type: every field must explicitly select `CapabilitySupported` or `CapabilityUnsupported`. `CapabilityUnspecified`, including an omitted field, is rejected. Sharing this value type does not let a Runtime advertisement grant service authorization. - -Each of `ConfigurationValidation`, `ToolsValidation` and `FunctionResultValidation` chooses one of two strategies: - -- `CommonValidationOnly`: common schema and admission checks are sufficient. The corresponding callback must be nil; no successful placeholder callback is needed. -- `AdditionalValidation`: the matching `ValidateConfiguration`, `ValidateTools` or `ValidateFunctionResult` callback is mandatory and adds pure Harness restrictions. - -An omitted or unknown policy, missing required callback, or callback paired with common-only policy is invalid. Admission follows the declared policy, never method presence. Preserve existing error precedence: configuration restrictions run first; when additional configuration validation is selected, tool-decoding errors precede tool restrictions. With common-only configuration validation, additional tool restrictions retain their existing precedence over a decoding error. Common-only function-result validation adds no native result restriction. - -`engine.NewCatalog` validates every entry before publishing its immutable snapshot and panics with `engine.ErrInvalidDeclaration` for invalid static registrations. Kinds must be nonempty without surrounding whitespace. Placements must explicitly list at least one supported placement; MCP origins must be a non-nil list (an empty list qualifies none). Unknown or duplicate choices, origins without a corresponding placement and bearer support without an MCP origin are rejected. Errors identify authored fields without echoing declaration values. Future profile fields must be classified by the completeness validator and explicitly decided by every profile; there is no production default-filling constructor. - -Run the `engine` and `execution` tests for omission, policy, combination and error precedence coverage, and the public onboarding tests in `services/core/tests/integration` for admission and Runtime dispatch. Test fixtures use `engine/enginetest`, whose exhaustive literal also requires a decision when a field is added; it is not a production profile. - -`execution.Policy` supplies immutable service qualification to HTTP admission, Worker device selection and final dispatch. Custom composition gives the same Policy to `api.Dependencies.Policy` and the Core dispatcher's `Policy`. The zero value uses the built-in profiles; an explicitly empty catalog authorizes none. There is no mutable global registration. +The shared selection fixtures in `internal/harnessconfig/builtin` hold one accepted and one rejected case for every declared rule. Run them, and the public onboarding tests in `services/core/tests/integration` for admission and Runtime dispatch. ## Native model configuration @@ -209,7 +189,7 @@ The declaration's ordered `protocols` list is the only source of accepted protoc Before starting, record the operation set, expected results, exclusions and stopping conditions. A qualification ends when its declared operations pass; it does not expand to match another Harness's feature list. 1. **Contract tests.** Call `agent/contracttest.TextLifecycle` from a test named `TestSharedTextLifecycle` with the adapter's prepared Executor and a deterministic native fixture; `claudesdk/executor_test.go` is the reference. It checks independent Turn streams, native owner and history continuity, durable write and application receipts, stale cancellation and healthy continuation after cancellation. `make check-runtime-contract` runs it together with the shared wire, gateway, transport and dispatcher tests, the declaration completeness check and each adapter's `TestUnsupportedExtensionsHaveNoNativeEffects`. Adapter tests also cover two ordinary Turns sharing one native process or connection and history, cancellation followed by another Turn, stale cancellation and late events, native exit, cleanup failure, input write and application receipts, unknown outcomes and fresh per-Turn usage, function, input and child-observation state. State whether a fixture is controlled or a real provider. -2. **Shared integration.** `TestThirdHarnessPublicOnboarding` runs the synthetic Harness through public Session and input admission, Worker device selection, the real WebSocket gateway, the daemon Registry and Router, neutral events and durable terminal projection. It uses a custom immutable `engine.Catalog` in the same `execution.Policy` given to the API handler and the dispatcher, and checks applied input receipts, saved native identity, continuation, cancellation, unsupported optional requests and missing mandatory Runtime support. The fixture has no workspace, MCP or public functions, and its registration stays local to the test. It proves the integration path, not native execution. +2. **Shared integration.** `TestThirdHarnessPublicOnboarding` runs the synthetic Harness through public Session and input admission, Worker device selection, the real WebSocket gateway, the daemon Registry and Router, neutral events and durable terminal projection. It registers under the `mcode` kind, so Core admits it against MiniMax Code's declaration, and checks applied input receipts, saved native identity, continuation, cancellation, unsupported optional requests and missing mandatory Runtime support. The fixture has no workspace, MCP or public functions, and its registration stays local to the test. It proves the integration path, not native execution. 3. **Real acceptance.** Use the pinned official Python SDK and raw HTTP against Core, a real provider API, the native Harness and a dedicated database. Verify initial execution, a warm follow-up, cancellation and restart with continuation; record native owner identity and same-condition cold and warm timing. For workspace placements also verify Files and Artifacts, workspace identity, that no credentials appear in public responses and that foreign history is rejected. `services/core/tests/official_hosted_functions_native.py` holds the shared function assertions: success and error, native file output and public Artifact bytes, same-history continuation after restart, foreign result rejection and pending-call cancellation. Synthetic or failed runs never count. The opt-in tests below run the pinned-SDK fixtures in `services/core/tests` against a real daemon and model; each runs when `OAC_TEST_OFFICIAL_SDK_PYTHON`, `OAC_TEST_NATIVE_DAEMON_BIN`, `OAC_TEST_NATIVE_PROOF_DIR` and its private options file are set. The options file is a JSON object with exactly `model` and `model_provider` (the fields of `x_agents_core.model_provider`); the test sets it as the deployment default model provider, which the fixtures' `environment: none` Sessions freeze at creation. 4. **Regression.** Existing Harnesses keep working. Run targeted tests, then `make check`; run `make openapi` after API changes and `make sqlc-generate` after query changes. 5. **Review.** Follow the [blind review workflow](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review). diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index 3f3158f0a..d7738cfdf 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -45,7 +45,7 @@ Evidence for a status comes from the pinned official SDK and raw HTTP against th | Files | create, retrieve, list, delete, content | Implemented for `purpose=user_data`; content download is rejected | [Files and Skills](./source-files.md) | | Skills and Skill versions | create, retrieve, update, list, delete, content | Implemented | [Files and Skills](./source-files.md) | -Which operation each Harness supports on each placement is in the [Harness capabilities](./harness-capabilities.md). [Core wire behavior](./wire-semantics.md) holds the rules that apply across resources: requests, errors and lists. +Each Harness declares what it supports once, in `internal/harnessconfig/` ([Declare support](./harness-onboarding.md#declare-support)). [Core wire behavior](./wire-semantics.md) holds the rules that apply across resources: requests, errors and lists. Core's own fields sit inside `x_agents_core` ([Core extensions](../../docs/api/public-agent-api.md#core-extensions-x_agents_core)). The Core administration API (`/core/v1`) and the machine API (`/api/v1`) are not part of the Agents API. @@ -109,7 +109,7 @@ Each item is Core's deliberate or native behavior where the official service beh **Configuration and tools** - Explicit reasoning effort or summary, service tiers other than `auto`, enabled `web_search` and enabled programmatic tool calling are saved but rejected at Session admission. -- Harness support for tools, structured output, deferred discovery, subagents and MCP differs by Harness and placement; see the [Harness capabilities](./harness-capabilities.md). MiniMax Code has no public functions, no service-origin MCP and no image input. +- Harness support differs as each [declaration](./harness-onboarding.md#declare-support) states. Codex has no structured output or `tool_search`. Claude Code takes no whitespace-only text, `medium` verbosity only and function-result images only inline and in successful results; it rejects structured output with Subagents, MCP, installed capabilities or `tool_search`, and `tool_search` with MCP or installed capabilities. MiniMax Code has no public functions, no service-origin MCP, no image input, no whitespace-only text, no required MCP and `medium` verbosity only, and takes `allowed_tools` null only. Each Harness reserves an MCP `server_label`: `codex_apps` for Codex, `functions` for Claude Code and `oac_workspace` for MiniMax Code. Claude Code also requires labels to match `^[a-zA-Z0-9_-]+$` and `allowed_tools` names to match `^[a-zA-Z0-9_.-]+$`. - Model-derived reasoning defaults are not resolved. - MCP tools support the `http` transport only; `stdio` is rejected, and so is an inline `authorization` on a Session MCP transport ([HTTP MCP](./execution-tools.md#http-mcp)). @@ -122,7 +122,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Behind the [credential gateway](./model-execution.md#credential-gateway), pinned Codex compacts history locally and never calls `/responses/compact`. - Claude Code and MiniMax Code report no public usage. - Core gives no crash-safe or exactly-once guarantee for native side effects; claimed work fails on restart without replay. -- Images must be inline PNG or JPEG data URIs; remote URLs, `file_id` and `detail` are rejected. +- Message images must be inline PNG or JPEG data URIs; remote URLs, `file_id` and `detail` are rejected. **Environments and Templates** diff --git a/contracts/agents-api/message-content.md b/contracts/agents-api/message-content.md index de27f670e..7ce89de55 100644 --- a/contracts/agents-api/message-content.md +++ b/contracts/agents-api/message-content.md @@ -25,11 +25,11 @@ An `input_image` part carries `image_url` as an inline data URI: `data:image/png | Claude Code | Accepted on `none`, `openai_hosted` and `self_hosted` | | MiniMax Code | Rejected | -Admission checks the harness before anything is written; an image the harness cannot take returns 400. The Runtime must also report message image support: Core binds a Session whose input carries images only to such a Runtime, and delivery to a Runtime without it fails. Use a model that accepts images. +Admission checks the harness before anything is written; an image the harness cannot take returns 400 `unsupported_or_invalid_configuration`. The Runtime must also report message image support: Core binds a Session whose input carries images only to such a Runtime, and delivery to a Runtime without it fails. Use a model that accepts images. ## Whitespace-only text -Whitespace-only text such as `" "` or `"\n\t"` is valid content and is stored and returned verbatim. Whether a harness can run it is declared in its engine profile: +Whitespace-only text such as `" "` or `"\n\t"` is valid content and is stored and returned verbatim. Whether a harness can run it is part of its [declaration](./harness-onboarding.md#declare-support): | Harness | A message with no image and no non-whitespace text | | --- | --- | @@ -50,7 +50,7 @@ An `agent.session.input.tool_result` event carries `success`, an optional nullab | Harness | Function results | | --- | --- | | Codex | Text and ordered text/image output. Core checks only that each part is well formed and passes image references to the harness unchanged | -| Claude Code | Text output. Images only in successful results and only as inline PNG or JPEG; an image in a failed result or a remote reference returns 400 before anything is stored, and the pending call stays open. The harness may resize or re-encode images in its own history; public Items keep the submitted bytes | +| Claude Code | Text output. Images only in successful results and only as inline PNG or JPEG; an image in a failed result or a remote or malformed reference returns 400 `unsupported_or_invalid_configuration` before anything is stored, and the pending call stays open. The harness may resize or re-encode images in its own history; public Items keep the submitted bytes | | MiniMax Code | No public functions | ### Application receipts @@ -64,7 +64,7 @@ Both adapters wait at most 10 seconds for a receipt. A timeout or native release ## Runtime boundary -The Core–Runtime wire carries messages as `MessageInput` for initial input, prepared start and steering, with the same ordered `InputContent` parts that function results use ([Core–Runtime protocol](../../docs/runtime-protocol.md)). Admission checks the harness's declared profile; binding and delivery check what the Runtime reports. Adapters own native encoding and application receipts. A text-only adapter rejects image parts instead of dropping them. +The Core–Runtime wire carries messages as `MessageInput` for initial input, prepared start and steering, with the same ordered `InputContent` parts that function results use ([Core–Runtime protocol](../../docs/runtime-protocol.md)). Admission checks the Harness's declaration; binding and delivery check it as the Runtime's heartbeat narrows it. Adapters own native encoding and application receipts. A text-only adapter rejects image parts instead of dropping them. - **Codex** flattens a batch into its native input list with a blank-line separator between public messages. Public message boundaries stay in Core's storage; the native history does not keep them. - **Claude Code** sends native image blocks and a UUID per native user message. One public input is applied only after every message in its batch is consumed. Within one native Turn the bridge accepts at most 64 user messages, including the opening prompt; it rejects a steering batch that would exceed the bound before submitting any part of it, which ends the running Turn. The daemon requires bridge protocol 3. diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 8f638c513..b6a4a7636 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -15,7 +15,7 @@ Saved Agents accept `x_agents_core.harness` on create, update and read, and Sess - Omitted: a Session inherits its saved Agent's Harness; an inline Agent uses the deployment default. - Explicit null on the Session's inline extension: resets to the deployment default Harness while keeping inherited provider bundles. On a saved Agent, a null extension clears its Harness and provider. - A selected Harness must be enabled; Core never falls back to another one. -- Session creation resolves the selection after saved-Agent overrides, validates the Harness profile and stores the result as the Session's engine. Session reads report it when the effective Agent includes the extension; other Sessions keep the official Agent shape. Reads never consult the current Agent or deployment default. +- Session creation resolves the selection after saved-Agent overrides, checks the configuration against the Harness's [declaration](./harness-onboarding.md#declare-support) and stores the result as the Session's engine. Session reads report it when the effective Agent includes the extension; other Sessions keep the official Agent shape. Reads never consult the current Agent or deployment default. - Creation retries with an explicit selector keep the caller's intent; changing the selector under an existing Idempotency-Key conflicts. The Session's `environment` and Environment Templates select preparation, not Harnesses or providers. The extension is defined once in `contracts/agents-api/v1`; validators derive from the catalog, and no handler or schema keeps its own list of names. diff --git a/contracts/agents-api/subagents.md b/contracts/agents-api/subagents.md index d0a7a39bb..6d469e4ad 100644 --- a/contracts/agents-api/subagents.md +++ b/contracts/agents-api/subagents.md @@ -2,7 +2,7 @@ title: "Subagents" --- -With `multi_agent.enabled`, a Harness may start native child agents. Core exposes them through the six Subagent read operations of the pinned SDK in [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json) and records them from adapter observations. With `multi_agent.enabled=false`, the Runtime removes native child tools. [Harness capabilities](./harness-capabilities.md) lists which Harness supports Subagents in which combinations. +With `multi_agent.enabled`, a Harness may start native child agents. Core exposes them through the six Subagent read operations of the pinned SDK in [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json) and records them from adapter observations. With `multi_agent.enabled=false`, the Runtime removes native child tools. Every Harness supports Subagents. No Harness runs them with function tools or `agent.tools` MCP, and Claude also rejects them with installed Plugin MCP or structured output ([Declare support](./harness-onboarding.md#declare-support)). ## Public reads @@ -50,8 +50,6 @@ An adapter freezes root output before child settlement, keeps its native owner a ## Native profiles -[Harness capabilities](./harness-capabilities.md#tools) lists rejected tool combinations. - **Codex.** The adapter enables the native `multi_agent` feature with a nesting depth of 64 and maps the concurrency limit to `agents.max_threads`. It disables native hooks, plugins, code mode and `multi_agent_v2`, and refuses to start if the native hook list is not empty or managed requirements force a conflicting feature. Close and reopen facts come from direct tool output correlated with the same call's persisted completion, so they need native persisted receipts. Native Turn times have second precision. Child file work can finish under the same owner after the root Turn finishes. Cancellation continues under the same owner after a caller deadline; a later call can confirm settlement without repeating the native interrupt. **Claude SDK.** The pinned SDK's native Agent and SendMessage calls run the single child type `oac_worker`, which inherits the model and has workspace Bash, Agent and SendMessage; the bridge's `subagent_resources` feature gates it. Children use native Bash with the parent's launching-user permissions. Private child records establish parentage, the first own input time and later own Turns; inherited parent context is excluded. The query owner admits children before start and keeps their history through settlement. Confirmed cancellation writes an immutable effect receipt because a native abort can leave no terminal record. There is no close operation: completed or cancelled children stay active. Messages to running children, background work, other child profiles and per-call model overrides are rejected. The [Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) documents the details. diff --git a/contracts/agents-api/v1/harness_selection.go b/contracts/agents-api/v1/harness_selection.go new file mode 100644 index 000000000..22658e3a2 --- /dev/null +++ b/contracts/agents-api/v1/harness_selection.go @@ -0,0 +1,50 @@ +package v1 + +import ( + "encoding/json" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" +) + +// HarnessSelection projects an Agent and its Environment, if known, onto what +// proto.ValidateSelection checks against a Harness declaration. Callers +// validate the tool declarations themselves. An MCP server with a +// credential_id is authenticated. +func HarnessSelection(agent Agent, environment *Environment) proto.Selection { + selection := proto.Selection{MultiAgent: agent.MultiAgent.Enabled, TextVerbosity: agent.Text.Verbosity} + if agent.Text.Format.Type == "json_schema" { + selection.OutputSchema = agent.Text.Format.Schema + } + if environment != nil { + selection.Environment = "local" + if environment.Type == "none" { + selection.Environment = "none" + } + selection.InstalledCapabilities = len(environment.Skills) > 0 || len(environment.Plugins) > 0 || len(environment.CapabilityDirectories) > 0 + } + for _, raw := range agent.Tools { + var tool struct { + Type string `json:"type"` + DeferLoading bool `json:"defer_loading"` + ServerLabel string `json:"server_label"` + AllowedTools *[]string `json:"allowed_tools"` + ConnectionOrigin string `json:"connection_origin"` + CredentialID *string `json:"credential_id"` + Required bool `json:"required"` + } + if json.Unmarshal(raw, &tool) != nil { + continue + } + switch tool.Type { + case "function": + selection.Functions = true + selection.DeferredFunctions = selection.DeferredFunctions || tool.DeferLoading + case "tool_search": + selection.ToolSearch = true + case "mcp": + selection.MCP = append(selection.MCP, proto.SelectedMCP{Origin: tool.ConnectionOrigin, Label: tool.ServerLabel, + AllowedTools: tool.AllowedTools, Required: tool.Required, Bearer: tool.CredentialID != nil}) + } + } + return selection +} diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index 6fd0a080f..025c769ef 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: 0abc36c1fe157f0ef8e6a9fd18dd6edd659352c269d59e107c1e3bf4d29ce3b5 +source_hash: 6bfd865756dbe0d358c687ce83f23b610ab9f6db9902e86ef775741982920ae5 --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment Template 是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -57,7 +57,7 @@ Core 在 Session 创建事务中创建 Environment 记录;Session upsert 会 ### 托管(`openai_hosted`) {#hosted-openai-hosted} -部署中配置的 Sandbox Provider(E2B、Docker 或 microsandbox,请参阅 [sandbox deployment](sandbox-deployment.md))承载 Environment。[Harness capabilities](harness-capabilities.md) 列出了可在其中运行的 Harnesses。 +部署中配置的 Sandbox Provider(E2B、Docker 或 microsandbox,请参阅 [sandbox deployment](sandbox-deployment.md))承载 Environment。声明支持 `local_environment` 的每个 Harness 都可在其中运行([声明支持](harness-onboarding.md#declare-support))。 - Session 创建时,无论是否包含初始输入,都会在 Worker 配置计算资源之前提交 Session、Environment 和重试身份。若创建在 bootstrap 前中断,可恢复时不会重复执行 Provider 的 Create。 - 置备无需调用方执行任何操作;在 Turn 启动之前,Session 会保持空闲。 @@ -364,9 +364,7 @@ Plugin 可以在 `.codex-plugin/plugin.json` 中通过 `mcpServers: "./.mcp.json stdio 服务器通过 daemon 的 stdio helper 启动;该 helper 会解析已安装的声明,并以 Harness 的权限启动命令。在 Unix 上,helper 会将自身替换为服务器;在 Windows 上,它会在所属进程树内部转发 stdio。已初始化的值会覆盖声明中的变量。进程组和 Windows Jobs 负责取消及后代进程清理,而不负责隔离。 -[Harness capabilities](harness-capabilities.md#environment-preparation) 负责受支持的 Plugin 传输及每个 Harness 的限制。 - -Environment MCP 需要启用的网络。重复的服务器身份会被拒绝。Claude 会拒绝字面量标头,因为固定版本客户端会再次展开这些标头,并将自定义标头跨来源转发。MiniMax ACP HTTP 声明会保留在 Session 本地的原生内存中;令牌绝不会进入原生配置文件或进程参数。无法通过 Plugin 清单设置必需初始化和工具允许列表。 +Environment MCP 需要启用的网络。重复的服务器身份会被拒绝。Claude 会拒绝字面量标头,因为固定版本客户端会再次展开这些标头,并将自定义标头跨来源转发;MiniMax Code 也会拒绝字面量标头。MiniMax ACP HTTP 声明会保留在 Session 本地的原生内存中;令牌绝不会进入原生配置文件或进程参数。无法通过 Plugin 清单设置必需初始化和工具允许列表。 ### 有效绑定 {#effective-bindings} @@ -381,9 +379,7 @@ Agent 的 HTTP MCP 工具([declaration](execution-tools.md#http-mcp))具有 | `service` | Core 的服务端执行主机 | 仅 `none` | | `environment` | Environment 的工作区 | `openai_hosted` 和启用网络的 `self_hosted` | -Core 的 Harness profile 会声明 `MCPOrigins`;接纳和分派会将来源与放置方式以及 Runtime 公布的 HTTP、bearer 和必需初始化能力进行核对,Runtime 会在调用适配器之前再次验证来源。不存在按 Harness 名称或 Provider 分支选择不同路径的逻辑。 - -[Harness capabilities](harness-capabilities.md#tools) 负责每个 Harness 的来源支持和策略限制。 +每个 Harness 都会声明自己支持的来源([声明支持](harness-onboarding.md#declare-support));MiniMax Code 只支持 `environment`。接纳和分派会将来源与放置方式以及 Runtime 声明的 HTTP、bearer 和必需初始化支持进行核对,Runtime 会在调用适配器之前再次验证该选择。不存在按 Harness 名称或 Provider 分支选择不同路径的逻辑。 两种来源都支持匿名 HTTP 和 HTTPS bearer 凭据。所附加 Vault 的选择会冻结凭据身份,包括唯一的隐式 URL 匹配或匿名选择。只有该凭据经 Project 授权后,才会进入瞬时 Runtime 请求;绝不会搜索 Core 默认值或无关 Vault。解密失败或凭据缺失时,执行会失败且不会回退到匿名。公开 Environment MCP 保留 `project_vault` 权限,Plugin 凭据保留 `environment_configuration` 权限;两者都不会覆盖重复的服务器标签。Bearer 令牌绝不会进入持久化的原生配置或进程参数。 diff --git a/contracts/agents-api/zh/execution-tools.md b/contracts/agents-api/zh/execution-tools.md index 022a42ed5..9c4692a52 100644 --- a/contracts/agents-api/zh/execution-tools.md +++ b/contracts/agents-api/zh/execution-tools.md @@ -1,16 +1,16 @@ --- title: "执行工具" source: contracts/agents-api/execution-tools.md -source_hash: eacb0d566f9d787f393c1d462351023a1ca6fe97ffb32773c8f23139833f5762 +source_hash: 21ba998ee2697491899ab001367ab7dd2dea7ef86f4bfc015b95cbbf5beba558 --- -Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 +Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。每个 Harness 的[声明](harness-onboarding.md#declare-support)说明它支持其中哪些内容。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 ## 准入 {#admission} - 已保存的 Agent 将所有固定版本工具声明保留为资源数据。保存不代表通过执行资格验证。 -- 创建 Session 时,执行解析器将保存的引用与内联声明解析为不可变 Session 快照,再根据 `services/core/internal/engine` 中所选 Harness 的配置检查组合。不支持的组合在任何写入之前返回 400 `unsupported_or_invalid_configuration`。重复 `web_search` 或 `tool_search`、非对象 schema 根等协议错误使用官方错误字段([验证](wire-semantics.md#configuration-validation))。 -- 分发之前,所选 Runtime 也必须声明该操作的能力。仅有能力声明不会启用操作。 +- 创建 Session 时,执行解析器将保存的引用与内联声明解析为不可变 Session 快照,再根据所选 Harness 的声明检查组合。不支持的组合在任何写入之前返回 400 `unsupported_or_invalid_configuration`,并以被拒绝的字段作为 `param`。重复 `web_search` 或 `tool_search`、非对象 schema 根等协议错误使用官方错误字段([验证](wire-semantics.md#configuration-validation))。 +- 分发之前,所选 Runtime 的心跳也必须声明该操作。心跳只会收窄静态声明,绝不会启用操作。 - 原生 Harness 运行模型与工具循环。Core 不添加第二个循环、输出修复、schema 强制转换或提示词包装,也不选择原生工具名称。 ## 函数 {#functions} @@ -41,7 +41,7 @@ SSE 仅提供实时事件。重启或流丢失后,读取 Session 的 `required `text.format` 接受 `{type: "json_schema", schema: {...}}`,即 Agents API 形式:不包含 `name`、`strict` 或其他 Responses API 包装字段。schema 被保存,经 Agent 和 Session 解析继承,并冻结于 Session 快照。对所有 Harness,显式非对象根类型在保存和 Session 创建时均为协议错误。Claude 要求 schema 根显式为 `type: "object"`。Claude SDK 将 JSON 数字读为 binary64,因此 Session 准入拒绝数字在转换中会变化的 schema;已保存 Agent 保留原值。 -Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选项的请求要求配置通过结构化输出资格验证,并要求 Runtime 具有 `structured_output` 和消息观察能力。冻结的 schema 在输入之前送达准备阶段,适用于初次和恢复执行;Start 不能替换它。 +Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选项的请求要求 Harness 的声明和 Runtime 的心跳都支持 `structured_output`。冻结的 schema 在输入之前送达准备阶段,适用于初次和恢复执行;Start 不能替换它。 Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。匹配的实时根工具结果和已归属的成功 SDK 结果确认输出。适配器将原生 `result.result` 字符串原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。未验证重试和已取消候选不会成为答案,适配器不会把 `structured_output` 重新序列化为 JSON。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。 @@ -49,7 +49,7 @@ Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `Str `tool_search` 工具仅包含 `type`;仅适用于 Responses 的执行字段被拒绝。函数 `defer_loading` 标记延迟加载的定义。发现要求两者同时存在:没有延迟函数的 `tool_search`,或没有 `tool_search` 的延迟函数均被拒绝。已保存 Agent 的工具联合类型保留 `tool_search`;固定版本 Session 响应联合类型省略它,因此 Session 与 SSE 资源投影移除它,冻结配置仍保留它。固定版本 Item 联合类型没有 tool-search Item,Core 不自行创建。 -Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoading`,并要求配置通过资格验证、Runtime 具备 `tool_search` 能力。原生搜索与 schema 延迟加载属于适配器。Claude 适配器的 MCP 服务器将立即加载定义标记为 `anthropic/alwaysLoad:true`,延迟定义标记为 false,并启用原生 ToolSearch;函数配置仅允许所声明回调、ToolSearch 与选定的工作区工具。工作区 Runtime 从桥接层的 `workspace_tool_search` 功能推导 `tool_search`。原生 Harness 管理模型与提供方策略;已知冲突模式和 beta 设置在适配器中拒绝,不透明策略改变后,SDK 不提供可靠的输入前信号来确认延迟加载是否生效。 +Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoading`,并要求 Harness 的声明和 Runtime 的心跳都支持 `tool_search`。原生搜索与 schema 延迟加载属于适配器。Claude 适配器的 MCP 服务器将立即加载定义标记为 `anthropic/alwaysLoad:true`,延迟定义标记为 false,并启用原生 ToolSearch;函数配置仅允许所声明回调、ToolSearch 与选定的工作区工具。工作区 Runtime 从桥接层的 `workspace_tool_search` 功能推导 `tool_search`。原生 Harness 管理模型与提供方策略;已知冲突模式和 beta 设置在适配器中拒绝,不透明策略改变后,SDK 不提供可靠的输入前信号来确认延迟加载是否生效。 ## Web 搜索与程序化工具调用 {#web-search-and-programmatic-tool-calling} @@ -64,7 +64,7 @@ Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoadi 执行仅允许 `mode: "disabled"` 和 `enabled: false`。启用或省略模式的搜索、启用或省略 `enabled` 的程序化调用,在 Session 准入时被拒绝,除非 Session 替换了保存的工具。省略程序化配置会保留各 Harness 原生行为,这与官方默认启用行为不同。无关的原生实用工具不会被移除。 -`DisableProgrammaticToolCalling` 在初次执行和冷继续执行中携带禁用意图,仅在存在时要求 Runtime 能力。搜索使用已有禁用控制项。 +`DisableProgrammaticToolCalling` 在初次执行和冷继续执行中携带禁用意图。每个 Harness 运行时都禁用原生网络搜索。 | Harness | 原生执行约束 | | --- | --- | @@ -86,10 +86,10 @@ Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoadi ``` - `server_label` 非空且在 Session 中唯一。仅接受 `http` 传输;`server_url` 为不带凭据、查询或片段的绝对 HTTP 或 HTTPS URL。非空 `headers`、`request_metadata` 和内联 `authorization` 被拒绝。 -- [公开 MCP 连接来源](environments.md#public-mcp-connection-origin)定义来源默认值、部署位置和凭据权限;[Harness 能力](harness-capabilities.md#tools)定义各 Harness 支持范围。 +- [公开 MCP 连接来源](environments.md#public-mcp-connection-origin)定义来源默认值、部署位置和凭据权限。 - 省略或 null 的 `allowed_tools` 允许所有服务器工具;`[]` 不允许任何工具。 - `required: true` 使原生线程创建和冷恢复等待服务器初始化;失败会停止执行,不替换保留历史。它要求 Runtime 的 `mcp_http_required` 能力。等待期间公开工作可被接受或排队。 - Bearer 认证使用附加的静态或 OAuth Vault 凭据。[Vault 凭据](vaults.md)定义选择规则,[MCP 凭据权限](environments.md#public-mcp-connection-origin)定义冻结的 Runtime 绑定。认证执行要求 `mcp_http_bearer_auth`。 -- Runtime 必须声明 `mcp_http_tools`。原生 Harness 管理发现、调用和结果;公开 `mcp_call` Item 使用原始服务器与工具名称,保留观察到的原生结果。 +- Harness 的声明和 Runtime 的心跳都必须支持 `mcp_http_tools`。原生 Harness 管理发现、调用和结果;公开 `mcp_call` Item 使用原始服务器与工具名称,保留观察到的原生结果。 Codex 在启动或恢复线程之前验证准确的有效 MCP 配置,排除未声明服务器,禁用原生 apps 和 plugins,拒绝原生保留标签和已存储原生 MCP 凭据。Claude 接受 ASCII 字母、数字、下划线和连字符组成的标签,但不允许 `functions`;工具名称还可包含点,并要求已连接服务器具有静态工具清单。匿名 Claude 请求发送空 Authorization 头以阻止原生 OAuth 注入。不支持原生 OAuth 登录。 diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md deleted file mode 100644 index b947daaf8..000000000 --- a/contracts/agents-api/zh/harness-capabilities.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: "Harness 能力" -source: contracts/agents-api/harness-capabilities.md -source_hash: e1ffc7a260ceaec64ba377f7c0db28f2c371c9d664098b110b740cc110506506 ---- - -本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `services/core/internal/engine` 中 Harness 的引擎配置决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 - -| 状态 | 含义 | -| --- | --- | -| 已验证 | Core 允许准入,且该位置通过固定版本官方客户端的真实模型验收 | -| 已准入 | Core 通过相同 Runtime 路径允许准入,但该位置尚未运行真实模型验收 | -| 已拒绝 | Core 在执行前拒绝请求 | - -部署位置为 `none`(无 Environment)、托管(`openai_hosted`)和自托管(`self_hosted`)。托管验收在 Docker 节点运行;E2B、microsandbox 使用相同 Runtime 与适配器,托管位置的每个已验证单元格在这两者中视为已准入。自托管验收在 Linux 机器运行;[自托管指南](../../../docs/zh/getting-started/self-hosted.md#platforms)列出支持平台。操作通过验证仅代表该操作本身,不代表与其他选项的所有组合;配置拒绝的组合列于对应操作契约。 - -## 执行与输入 {#execution-and-input} - -| 操作 | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| 文本 Turn、活动输入、取消、重启与继续 | 所有位置已验证 | 所有位置已验证 | 所有位置已验证 | -| [文件与 Artifact](environment-files.md) | 已验证:托管、自托管 | 已验证:托管、自托管 | 已验证:托管、自托管 | -| [仅空白消息文本](message-content.md) | 已准入;原样交付 | 已拒绝 | 已拒绝 | -| [内联 PNG、JPEG 消息图像](message-content.md) | 所有位置已验证 | 所有位置已验证 | 已拒绝 | -| 远程图像 URL | 已拒绝 | 已拒绝 | 已拒绝 | -| 显式 `reasoning`;非 `auto` 的 `service_tier` | 已拒绝 | 已拒绝 | 已拒绝 | -| 非 `medium` 的 `text.verbosity` | 已准入;由原生模型决定 | 已拒绝 | 已拒绝 | -| [公开 token 用量](sessions-events.md) | 测量计数器 | Null | Null | - -各 Harness 原生模型参数及提供方协议见[模型执行](model-execution.md)。 - -## 工具 {#tools} - -| 操作 | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| 文本结果的[公开函数](execution-tools.md#functions) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅对象根 schema | 已拒绝 | -| [含图像函数结果](message-content.md#function-results) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅成功结果中的内联 PNG 或 JPEG | 已拒绝 | -| [结构化输出](execution-tools.md#structured-output) | 已拒绝 | 所有位置已验证 | 已拒绝 | -| [延迟函数发现](execution-tools.md#deferred-function-discovery) | 已拒绝 | 已验证:`none`、自托管;已准入:托管 | 已拒绝 | -| [禁用 Web 搜索与程序化工具调用](execution-tools.md#web-search-and-programmatic-tool-calling) | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | -| 启用 Web 搜索或程序化工具调用 | 已拒绝 | 已拒绝 | 已拒绝 | -| [服务端来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:`none`;其他位置拒绝 | 已验证:`none`;其他位置拒绝 | 已拒绝 | -| [Environment 来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:托管、自托管 | 已验证:托管、自托管 | 已验证:托管、自托管;仅 `allowed_tools` null、`required` false | -| [HTTP MCP bearer 凭据](execution-tools.md#http-mcp) | 已验证 | 已验证 | 已验证 | -| 必需 MCP 初始化 | 已验证 | 已验证 | 已拒绝 | -| [Subagent](subagents.md) | 已验证:托管;已准入:`none`、自托管 | 已验证:托管;已准入:`none`、自托管 | 已验证:托管;已准入:`none`、自托管 | -| Subagent 与函数或 HTTP MCP 同时使用 | 已拒绝 | 已拒绝 | 已拒绝 | - -Claude 结构化输出要求单 Agent、medium verbosity,且无 Skill、Plugin、能力目录、MCP 或 `tool_search`。Claude 延迟发现要求单 Agent,除 `tool_search` 与禁用控制项外仅函数工具,无 Skill、Plugin 或能力目录,也无结构化输出。Claude 在工作区位置需要每种操作的打包桥接功能(`workspace_functions`、`workspace_structured_output`、`workspace_tool_search`、`workspace_mcp_http`)。 - -## Environment 准备 {#environment-preparation} - -这些操作要求工作区,因此仅适用于托管与自托管位置。 - -| 操作 | Codex | Claude SDK | MiniMax Code | -| --- | --- | --- | --- | -| [初始文件、设置命令、Skill 与 Plugin](environments.md#runtime-capability-preparation) | 已验证:托管、自托管 | 已验证:自托管;已准入:托管 | 已验证:自托管;已准入:托管 | -| 能力目录 | 已准入 | 已准入 | 已准入 | -| npm 与 Python 包 | 已准入 | 已准入 | 已准入 | -| `packages.system` | 已拒绝 | 已拒绝 | 已拒绝 | -| 网络 `disabled` 或 `restricted` | 已拒绝 | 已拒绝 | 已拒绝 | -| [stdio Plugin MCP](environments.md#plugin-mcp) | 已验证:托管、自托管 | 已验证:自托管;已准入:托管 | 已验证:自托管;已准入:托管 | -| HTTP Plugin MCP | 已准入,字面值头或 HTTPS bearer | 已准入,匿名或 HTTPS bearer | 已验证:自托管;已准入:托管;匿名或 HTTPS bearer | diff --git a/contracts/agents-api/zh/harness-catalog.md b/contracts/agents-api/zh/harness-catalog.md index 115f99bbf..b0652c3eb 100644 --- a/contracts/agents-api/zh/harness-catalog.md +++ b/contracts/agents-api/zh/harness-catalog.md @@ -3,10 +3,10 @@ 注册列表的源文件是 [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json)。修改后运行 `make generate-harness-catalog`;`make check-harness-catalog` 检查生成结果。 -| 标识符 | 显示名称 | 模型配置声明 | Core 验收构造函数 | -| --- | --- | --- | --- | -| `claude_sdk` | Claude Code | `claudesdk.Configuration` | `claudeProfile` | -| `codex` | Codex | `codex.Configuration` | `codexProfile` | -| `mcode` | MiniMax Code | `mcode.Configuration` | `mcodeProfile` | +| 标识符 | 显示名称 | 声明 | +| --- | --- | --- | +| `claude_sdk` | Claude Code | `claudesdk.Configuration` | +| `codex` | Codex | `codex.Configuration` | +| `mcode` | MiniMax Code | `mcode.Configuration` | -配置声明位于 `internal/harnessconfig/`,验收构造函数位于 `services/core/internal/engine`。这些注册描述当前构建。部署启用的 Harness 由 `core.harnesses` [进程设置](../../../docs/zh/configuration.md#settings)决定;[Harness 能力](./harness-capabilities.md)列出各 Harness 的支持范围,连接的 Runtime 报告自身可用性。[Harness 接入](./harness-onboarding.md)介绍适配器和打包步骤。 +`internal/harnessconfig/` 中的声明给出 Harness 的模型提供方及其支持范围,是 Core 和 Runtime 准入选择的唯一依据。这些注册描述当前构建。部署启用的 Harness 由 `core.harnesses` [进程设置](../../../docs/zh/configuration.md#settings)决定,连接的 Runtime 报告自身可用性。[Harness 接入](./harness-onboarding.md)介绍适配器和打包步骤。 diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index a48b76f52..6cf38129b 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,10 +1,10 @@ --- title: "添加 Harness" source: contracts/agents-api/harness-onboarding.md -source_hash: 1951ed3ead52e659e96b8b341dd0d6bbacb32d8eb564425591f8f4307b038b67 +source_hash: f5d10dc8f734dae33713d262079a0a0906fd881ca059b769390fc817086273c0 --- -**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 +**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。 从两个入口开始: @@ -31,8 +31,8 @@ Runtime: Executor preparation, reuse, idle expiry, recovery | Runtime | 经身份验证的连接、共享能力准备以及通用 Executor 和 Turn 生命周期 | `apps/daemon/internal/dispatch` | | Adapter | 原生配置、资源、API 调用、事件转换和限制 | `apps/daemon/internal/agent/` | | Harness | 原生模型和工具循环以及历史记录 | 锁定版本的 SDK 或可执行文件 | -| 服务 profile | 对已认定合格的操作和放置位置进行纯验证 | `services/core/internal/engine` | -| 注册 | 适配器声明、已安装工厂和已验证能力 | `apps/daemon/internal/agent//declaration.go`;`apps/daemon/internal/cli/agent_discovery.go` 中的静态列表 | +| 声明 | Harness 的支持范围,Core 和 Runtime 据此准入每个选择 | `internal/harnessconfig/` | +| 注册 | 适配器声明、已安装工厂和该安装收窄后的支持 | `apps/daemon/internal/agent//declaration.go`;`apps/daemon/internal/cli/agent_discovery.go` 中的静态列表 | Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以及应用自有机器在预配和连接方式上有所不同;已连接的 Runtime 使用同一契约。daemon 运行于 Linux、macOS 和 Windows,受管 Provider 仅支持 Linux,并且每个适配器自行认定其支持的平台([self-hosted platforms](../../../docs/zh/getting-started/self-hosted.md#platforms))。只有在 Runtime 加载绑定的已安装快照后,原生工厂才会收到能力([capability preparation](environments.md#runtime-capability-preparation))。模型 Provider 提供模型通信设置,而不负责 Turn 调度或原生进程所有权。 @@ -40,22 +40,21 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 1. **锁定原生来源。** 记录上游包版本和源修订版本,并在适配器旁记录原生入口点。 2. **实现适配器**,位置为 `apps/daemon/internal/agent/`:实现 `ExecutorFactory`、`Executor` 和 `Turn`([required interfaces](#required-adapter-interfaces)、[lifetimes](#executor-and-turn-lifetimes))。复用共享的进程、凭据、配置和本地工作区辅助函数。 -3. **在适配器中声明 kind**,并将其声明添加到 `apps/daemon/internal/cli/agent_discovery.go` 中 Runtime 的静态列表([register the adapter](#register-the-adapter))。 -4. **添加服务 profile 和一个目录条目**([add the engine to Core](#add-the-engine-to-core))。 -5. **打包原生先决条件。** 在 `services/core/deploy/` 下添加 Runtime 镜像,并可选添加 [native installer participation](#native-installer-participation)。 -6. **启用并选择引擎**,通过 `core.harnesses` 设置和 [Harness selection](model-execution.md#harness-selection) 完成。 -7. **认定其资格**([qualify the adapter](#qualify-the-adapter)),并将结果记录到 [Harness capabilities](harness-capabilities.md)。 +3. **声明支持并注册。** 在 `internal/harnessconfig/` 中声明支持并添加一个目录条目([declare support](#declare-support)),然后在适配器中声明 kind,并将其添加到 `apps/daemon/internal/cli/agent_discovery.go` 中 Runtime 的静态列表([register the adapter](#register-the-adapter))。 +4. **打包原生先决条件。** 在 `services/core/deploy/` 下添加 Runtime 镜像,并可选添加 [native installer participation](#native-installer-participation)。 +5. **启用并选择引擎**,通过 `core.harnesses` 设置和 [Harness selection](model-execution.md#harness-selection) 完成。 +6. **认定其资格**([qualify the adapter](#qualify-the-adapter)),并将每项原生差异记录到[覆盖台账](index.md)。 实现强制的文本生命周期,并明确处理每一种扩展。逐个认定受支持扩展的资格;未认定资格的扩展返回 `agent.ErrUnsupportedOperation`,且不会产生原生副作用。原生取消可能要求退役而非复用:`Reusable=false` 会携带原因,调用方必须确认 `Executor.Close`。不要为了适配测试辅助函数而强制复用,也不要将适配器的原生限制复制到共享 Core 协议中。 ## 架构规则 {#architecture-rules} - Codex、Claude Code 和未来的 Harness 地位平等。通用 Runtime 线协议以及 Executor 和 Turn 接口负责生命周期、输入回执、取消、恢复和资源访问;每个适配器保留其原生实现以及模型和工具循环。 -- 新引擎需要提供适配器、已认定合格的 profile、注册以及经过独立验证的部署。它不得在 API 处理程序、持久化、调度、调度器或 Environment Provider 中添加按引擎名称分支的实现,也不得为契约已经涵盖的能力添加处理程序、存储表、调度器、事件投影器或模型循环。 +- 新引擎需要提供适配器、其声明、注册以及经过独立验证的部署。它不得在 API 处理程序、持久化、调度、调度器或 Environment Provider 中添加按引擎名称分支的实现,也不得为契约已经涵盖的能力添加处理程序、存储表、调度器、事件投影器或模型循环。 - 将必需的生命周期声明、扩展接口和注册方法保留在 `agent/harness.go` 中。结果类型、错误和 Registry 存储可以保留在聚焦的文件中。 -- 使用现有的 `proto.SupportedAgentKind` 和 `AgentKindCapabilities` schema。不要添加第二套能力描述符或组合式可选接口。 +- 使用现有的 `proto.Declaration` 和 `proto.SupportedAgentKind` schema。不要添加第二套能力描述符或组合式可选接口。 - 接入不要求功能完全一致。Harness 不必匹配彼此的可选功能,并且注册时不强制要求 MCP、函数、图像或详细程度控制。验证通用生命周期义务,并对每个声明的操作使用相同的公共断言。缺少声明或扩展实现会阻止接入;原生差异不会。 -- 服务 profile 目录是资格认定边界。未知 profile 以关闭方式失败,并且 Runtime 心跳无法授权新的公共功能。Schema 有效性、服务资格认定和可用 Runtime 是相互独立的检查。 +- 静态声明是支持边界。未知 kind 以关闭方式失败,Core 拒绝扩大声明的心跳。Schema 有效性、声明和可用 Runtime 是相互独立的检查。 - 绝不能将已接受的参数等同于已实际应用的原生行为。 ## 必需的适配器接口 {#required-adapter-interfaces} @@ -86,9 +85,9 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl 线协议请求不携带工作目录。Runtime 将 `local_environment.workspace_directory` 与其绑定进行核对,并通过 `LocalEnvironment.WorkspaceRoot` 向 Harness 提供其绑定的工作区目录;必须在该目录中运行原生 Harness。 -工作区读取、写入、输出导出和只读 preparation 属于 Session 的 [Environment owner](../../../docs/zh/runtime-protocol.md#session-assignments),不属于 adapter。adapter 不实现其中任何操作,并将 `WorkspaceReadPreparation` 和 `WorkspaceOutputExport` 声明为不支持。它将 `LocalEnvironment` 和 `EnvironmentNone` 声明为其 Executor 能运行的内容,`agent.Registry.Register` 将该声明与 Runtime 的 owner 所提供的内容(`agent.EnvironmentSupport`)组合一次:仅在 owner 提供时保留二者,并将两个导出字段设为组合后的 `LocalEnvironment`。一份声明适用于该安装的每个 Executor,包括其[视图](#run-in-an-agent-host-view)。 +工作区读取、写入、输出导出和只读 preparation 属于 Session 的 [Environment owner](../../../docs/zh/runtime-protocol.md#session-assignments),不属于 adapter。adapter 不实现其中任何操作。其声明中的 `LocalEnvironment` 和 `EnvironmentNone` 表示其 Executor 能运行的内容,`agent.Registry.Register` 将二者与 Runtime 的 owner 所提供的内容(`agent.EnvironmentSupport`)组合一次,仅在 owner 提供时保留。组合后的 `LocalEnvironment` 同时准入 owner 的工作区读取、只读 preparation 和输出导出。一份声明适用于该安装的每个 Executor,包括其[视图](#run-in-an-agent-host-view)。 -服务 profile 对公共组合进行资格认定,Runtime 宣称已安装的组合;二者都不能替代 schema 验证或 Project 授权。原生行为测试必须与声明一致。已宣称但返回 Unsupported 的操作属于契约违规,既不是成功,也不能作为重放的依据。 +声明说明 Harness 支持的公共组合,心跳将其收窄到该安装;二者都不能替代 schema 验证或 Project 授权。原生行为测试必须与声明一致。已宣称但返回 Unsupported 的操作属于契约违规,既不是成功,也不能作为重放的依据。 ## Executor 和 Turn 生命周期 {#executor-and-turn-lifetimes} @@ -129,17 +128,17 @@ Session 在其已连接的 Runtime 中拥有一个可复用的 Executor;Turn ### 必需操作和扩展操作 {#required-and-extension-operations} -公共文本路径要求持久化 Turn、已应用输入回执、有序观察、取消,以及执行已禁用的执行控制;`execution.Policy.engineCapabilities` 保存精确要求。没有原生工具的引擎可以保证这些工具不存在;具有工具的引擎在收到要求时必须实际禁用它们。接受某项配置不能证明其已得到执行。 +公共文本路径要求持久化 Turn、已应用输入回执、有序观察、取消,以及执行已禁用的执行控制。没有原生工具的引擎可以保证这些工具不存在;具有工具的引擎在收到要求时必须实际禁用它们。接受某项配置不能证明其已得到执行。 -MCP、公共函数、延迟函数发现、结构化输出、图像输入、详细程度控制和其他可选操作不必匹配另一个引擎。使用 Unsupported 拒绝未认定资格的组合并记录差距;绝不能宣称某项能力来绕过选择。 +MCP、公共函数、延迟函数发现、结构化输出、图像输入、详细程度控制和其他可选操作不必匹配另一个引擎。声明不支持的内容(组合声明为 `Conflicts` 中的一对)并记录差距;绝不能宣称某项能力来绕过选择。 -- 结构化输出:读取 `ExecutionControls.OutputFormat`,并通过 Message 契约发布已确认的原生输出([execution tools](execution-tools.md#structured-output))。公共资格认定与 Runtime 能力分别注册。 -- 图像:分别注册 Runtime 的 `MessageImages` 并认定 profile 的 `MessageImages` 资格([message input](message-content.md))。 +- 结构化输出:读取 `ExecutionControls.OutputFormat`,并通过 Message 契约发布已确认的原生输出([execution tools](execution-tools.md#structured-output))。原生 SDK 以 binary64 读取 JSON 数字时,声明 `Binary64OutputSchema`。 +- 图像:声明 `MessageImages` 和 `FunctionResultImages`,以及函数结果是否准入图像 URL 和失败结果中的图像([message input](message-content.md))。 - 工作区放置方式还需要经过验证的准备过程、工作区读取和输出导出,以及使用共享 Files 辅助函数的专用 Runtime 绑定。只有在展示其生命周期行为后才能启用放置方式。 ### MCP 来源和原生限制 {#mcp-origin-and-native-limits} -在引擎 profile 的 `MCPOrigins` 中声明受支持的公共来源,并在 `MCPBearer` 中声明 bearer 支持。Runtime 宣称其实际 HTTP、bearer 和必需初始化能力。共享准入负责验证来源和放置位置;适配器验证负责保留原生标签、允许列表和初始化限制。 +在 `MCPOrigins` 中声明受支持的公共来源,在能力中声明 HTTP、bearer 和必需初始化支持,并将原生限制声明为数据:`MCPAllowedTools`、`ReservedMCPLabels` 以及 `MCPLabel` 和 `MCPToolName` 模式。`proto.ValidateSelection` 将它们与来源和放置位置一起检查,适配器不再重复检查。 使用 `agent.ResolveMCPBindings` 处理公共声明和已安装声明,并保留来源、凭据权限、`null` 与空允许列表之间的区别以及必需启动过程。不要将令牌复制到原生 profile 中,也不要将服务请求重新解释为 Environment 请求。拒绝不受支持的原生策略,而不是将其丢弃。遵循 [MCP origin contract](environments.md#public-mcp-connection-origin),并对每个宣称的组合执行公共客户端、失败、取消和冷恢复资格认定。模型能力与 Harness 传输支持相互独立;绝不能从模型名称推断模型能力,也绝不能静默降低输入质量。 @@ -149,54 +148,35 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 ## 注册适配器 {#register-the-adapter} -注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go) 的 `harnessDeclarations` 中。声明包含 kind、完整能力描述符、共享模型 `Configuration` 和 `Discover` 函数。发现过程接收 profile 和诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符、Executor 工厂和视图声明。未配置适配器时返回 nil;已配置的前置条件失败时,返回不带 Executor 工厂和视图的不可用描述符。将版本门控和工厂选择条件保留在适配器内部。 +注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go) 的 `harnessDeclarations` 中。声明包含 kind 及共享模型 `Configuration` 的声明中的能力、该 `Configuration` 和 `Discover` 函数。发现过程接收 profile 和诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符、Executor 工厂和视图声明。未配置适配器时返回 nil;已配置的前置条件失败时,返回不带 Executor 工厂和视图的不可用描述符。将版本门控和工厂选择条件保留在适配器内部;它们只能清除支持。 [`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) 遍历已发现的 Runtime,并调用 `agent/harness.go` 中的 `Registry.Register`。它验证发现过程是否保留了声明的 kind,并按以下顺序注册该 Runtime: | 顺序 | 方法 | 注册内容 | | --- | --- | --- | -| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind、可用性、版本、`AgentKindCapabilities` 和模型配置声明。它会重置其他注册项,因此必须首先调用。 | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期 | +| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind、可用性、版本、`AgentKindCapabilities` 和模型配置;它将模型配置的声明收窄到这些能力,遇到扩大时 panic。它会重置其他注册项,因此必须首先调用。 | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期。其工厂只为收窄后的声明所准入的请求运行。 | | 3 | `RegisterView(kind, agent.View)` | 可选:来自 `Runtime.View` 的 agent-host 视图声明。`View.Validate` 失败时以 `ErrInvalidView` panic。其 Executor 工厂像 `RegisterExecutor` 一样验证模型配置,并执行[网关规则](#endpoints-and-proxy)。 | `Runtime.View` 声明 Harness 如何在 agent-host Session 视图中运行,详见[在 agent-host 视图中运行](#run-in-an-agent-host-view)。每个适配器都显式设置它;`View: nil` 表示 agent host 拒绝该 kind,`Registry.ResolveView` 返回包装 `ErrUnsupportedOperation` 的错误。`TestPublicHarnessContractDeclarations` 要求每个声明都包含该字段。 -每个 `proto.AgentKindCapabilities` 字段都必须显式设为 `proto.CapabilitySupported` 或 `proto.CapabilityUnsupported`,即使 Harness 不可用也是如此。`proto.CapabilityUnspecified` 无效:零值和省略字段绝不表示 Unsupported。安装探测可以使用 `proto.CapabilityFromBool` 设置单个字段;但不得填充未提及字段或未来字段。可用性通过 `SupportedAgentKind.Available` 单独表示。注册会在更改 registry 之前验证完整声明;线协议会为每个字段携带显式布尔值,因此省略字段和 null 字段均无效。添加新字段时,每个生产声明都必须作出决定。Runtime 使用者应调用 `IsSupported()`,并在原生操作前拒绝不受支持的请求;接口断言用于验证实现,绝不表示支持。每个声明都必须与针对该安装验证的行为一致;[Core–Runtime protocol](../../../docs/zh/runtime-protocol.md#capability-declarations) 负责声明的传输方式和冻结方式。 +每个 `proto.AgentKindCapabilities` 字段都必须显式设为 `proto.CapabilitySupported` 或 `proto.CapabilityUnsupported`,即使 Harness 不可用也是如此。`proto.CapabilityUnspecified` 无效:零值和省略字段绝不表示 Unsupported。安装探测可以使用 `proto.CapabilityFromBool` 清除单个字段;绝不设置静态声明不具备的支持。可用性通过 `SupportedAgentKind.Available` 单独表示。注册会在更改 registry 之前验证完整声明;线协议会为每个字段携带显式布尔值,因此省略字段和 null 字段均无效。添加新字段时,每个生产声明都必须作出决定。Runtime 使用者应调用 `IsSupported()`,并在原生操作前拒绝不受支持的请求;接口断言用于验证实现,绝不表示支持。每个声明都必须与针对该安装验证的行为一致;[Core–Runtime protocol](../../../docs/zh/runtime-protocol.md#capability-declarations) 负责声明的传输方式和冻结方式。 -每个可用 Harness 都无需声明即实现共享 Turn 生命周期(包括持久的 `SteerWithReceipt` 输入和由 `contracttest.TextLifecycle` 检查的 Turn 结算契约)、类型化的 `execution_controls` 和工具观测。在某个平台上无法满足这些要求的 Harness 在该平台报告 `Available` 为 false。`FunctionTools` 准入 `SubmitFunctionResult`。`WorkspaceReadPreparation` 准入带 `workspace_read_only` 的 `execution_prepare`,由 Environment owner 就绪并提供读取,不调用 Executor 工厂。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 +每个可用 Harness 都无需声明即实现共享 Turn 生命周期(包括持久的 `SteerWithReceipt` 输入和由 `contracttest.TextLifecycle` 检查的 Turn 结算契约)、类型化的 `execution_controls` 和工具观测。在某个平台上无法满足这些要求的 Harness 在该平台报告 `Available` 为 false。`FunctionTools` 准入 `SubmitFunctionResult`。`LocalEnvironment` 准入带 `workspace_read_only` 的 `execution_prepare`,由 Environment owner 就绪并提供读取,不调用 Executor 工厂。 可运行的仅测试示例 [`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} +## 声明支持 {#declare-support} -Core 会识别[内置 Harness 注册项](harness-catalog.md)。向 `internal/harnessconfig/builtin/catalog.json` 添加一个条目,其中包含: +Core 会识别[内置 Harness 注册项](harness-catalog.md)。向 `internal/harnessconfig/builtin/catalog.json` 添加一个条目,包含公共 `kind`、显示 `label` 和 `internal/harnessconfig` 下的模型 `configuration` 包,然后运行 `make generate-harness-catalog`。它会生成模型配置 registry、客户端标识符和显示名称以及注册参考;公共输入验证器读取生成的 registry。`make openapi` 从同一目录派生 Harness 枚举,因此不要在 DTO 标签或路由注解中添加手写枚举。`make check-harness-catalog` 会拒绝过时的投影。 -- 公共 `kind` 和显示 `label`; -- `internal/harnessconfig` 下的模型 `configuration` 包; -- `services/core/internal/engine` 下的 `profile` 构造函数。 +`internal/harnessconfig/` 中 `Configuration()` 的 `Declaration` 就是 Harness 的支持范围:一个 `proto.Declaration`,包含其 `AgentKindCapabilities`、消息、图像、MCP 和输出 schema 限制,以及 `Conflicts` 中它能单独支持但不能同时支持的功能对。它说明适配器的最大支持范围,并且是唯一来源:Core 通过 `builtin.Registry()` 读取它,适配器的 Runtime 描述符也从它开始。发现过程和 Environment owner 只能清除支持,Core 拒绝扩大该声明的心跳。只声明 Harness 之间的真实差异;对每个 Harness 都成立的规则属于 `proto.ValidateSelection` 中的通用检查。 -实现 profile 构造函数,然后运行 `make generate-harness-catalog`。它会生成模型配置 registry、Core profile 目录、客户端标识符和显示名称以及注册参考;公共输入验证器读取生成的 registry。`make openapi` 从同一目录派生 Harness 枚举,因此不要在 DTO 标签或路由注解中添加手写枚举。`make check-harness-catalog` 会拒绝过时的投影。 +`proto.ValidateSelection` 是对声明的唯一检查。Core 在创建或更新已保存 Harness 的 Agent、创建 Session 以及准入输入和函数结果时应用静态声明,在设备选择和认领 Turn 之前应用 Runtime 收窄后的声明。Runtime 在准入 `execution_prepare` 时应用它,早于任何 Executor 工厂运行。拒绝返回 400 `unsupported_or_invalid_configuration`,并以配置路径作为 `param`。Runtime 事实(例如缺少二进制、原生历史或文件系统就绪状态)仍是适配器准备失败。 -每个 Runtime 声明都引用生成的目录所使用的同一个 `internal/harnessconfig/.Configuration()`,并负责其原生工厂、探测和已安装能力证据。目录不能声明某台机器的可用性,也不存在动态插件加载器。 +每个 Runtime 声明都引用同一个 `internal/harnessconfig/.Configuration()`,并负责其原生工厂和探测。目录不能声明某台机器的可用性,也不存在动态插件加载器。 -profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受支持的放置方式、公共配置、结果限制和必需的 Runtime 控制。Profile 回调不能查询业务数据、解密凭据或控制原生进程。共享调度检查能力组合,而不是引擎名称允许列表。 - -### 显式服务资格认定 {#explicit-service-qualification} - -`engine.Profile` 是服务的资格声明,独立于 Runtime 的 `AgentKindCapabilities`。其能力字段复用小型 `proto.CapabilitySupport` 值类型:每个字段都必须显式选择 `CapabilitySupported` 或 `CapabilityUnsupported`。`CapabilityUnspecified`(包括省略字段)会被拒绝。复用此值类型并不意味着 Runtime 的宣称可以授予服务授权。 - -`ConfigurationValidation`、`ToolsValidation` 和 `FunctionResultValidation` 分别选择以下两种策略之一: - -- `CommonValidationOnly`:通用 schema 和准入检查已足够。相应回调必须为 nil;不需要提供成功的占位回调。 -- `AdditionalValidation`:相应的 `ValidateConfiguration`、`ValidateTools` 或 `ValidateFunctionResult` 回调为必需项,并添加纯 Harness 限制。 - -省略或未知策略、缺少必需回调,或者将回调与仅通用策略搭配,均无效。准入遵循声明的策略,而不依据方法是否存在。保留现有错误优先级:配置限制最先执行;选择额外配置验证时,工具解码错误先于工具限制。对于仅通用的配置验证,额外工具限制仍保持其相对于解码错误的现有优先级。仅通用的函数结果验证不会添加原生结果限制。 - -`engine.NewCatalog` 会在发布不可变快照之前验证每个条目,并对无效静态注册触发 `engine.ErrInvalidDeclaration` panic。Kind 必须非空且前后不得包含空白字符。放置方式必须显式列出至少一个受支持的放置方式;MCP 来源必须是非 nil 列表(空列表表示不认定任何来源合格)。未知或重复选项、没有对应放置方式的来源,以及没有 MCP 来源的 bearer 支持均会被拒绝。错误应标识已编写的字段,但不回显声明值。未来 profile 字段必须由完整性验证器分类,并由每个 profile 显式决定;不存在生产用默认填充构造函数。 - -运行 `engine` 和 `execution` 测试以覆盖遗漏、策略、组合和错误优先级,并运行 `services/core/tests/integration` 中的公共接入测试以覆盖准入和 Runtime 调度。测试夹具使用 `engine/enginetest`,其穷尽式字面量在添加字段时也要求作出决定;它不是生产 profile。 - -`execution.Policy` 向 HTTP 准入、Worker 设备选择和最终调度提供不可变服务资格认定。自定义组合将同一个 Policy 提供给 `api.Dependencies.Policy` 和 Core 调度器的 `Policy`。零值使用内置 profile;显式空目录不授权任何内容。不存在可变全局注册。 +`internal/harnessconfig/builtin` 中的共享选择夹具为每条声明规则各保存一个接受用例和一个拒绝用例。运行这些夹具,并运行 `services/core/tests/integration` 中的公共接入测试以覆盖准入和 Runtime 调度。 ## 原生模型配置 {#native-model-configuration} @@ -211,7 +191,7 @@ profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受 开始前,记录操作集、预期结果、排除项和停止条件。当其声明的操作通过时,资格认定即结束;它不会扩展为匹配另一个 Harness 的功能列表。 1. **契约测试。** 在名为 `TestSharedTextLifecycle` 的测试中,使用适配器准备好的 Executor 和确定性的原生夹具调用 `agent/contracttest.TextLifecycle`;`claudesdk/executor_test.go` 是参考实现。它检查独立的 Turn 流、原生所有者和历史连续性、持久化写入与应用回执、过期取消,以及取消后的健康继续执行。`make check-runtime-contract` 会将它与共享线协议、gateway、传输层和调度器测试、声明完整性检查以及每个适配器的 `TestUnsupportedExtensionsHaveNoNativeEffects` 一起运行。适配器测试还覆盖两个普通 Turn 共享一个原生进程或连接和历史、取消后执行另一个 Turn、过期取消和迟到事件、原生退出、清理失败、输入写入与应用回执、未知结果,以及每 Turn 新鲜的 usage、函数、输入和子项观察状态。必须说明夹具是受控夹具还是真实 Provider。 -2. **共享集成。** `TestThirdHarnessPublicOnboarding` 让合成 Harness 通过公共 Session 和输入准入、Worker 设备选择、真实 WebSocket gateway、daemon Registry 和 Router、中立事件以及持久化终态投影运行。它在与 API 处理程序和调度器相同的 `execution.Policy` 中使用自定义不可变 `engine.Catalog`,并检查已应用输入回执、已保存原生身份、继续执行、取消、不受支持的可选请求以及缺少强制 Runtime 支持。该夹具没有工作区、MCP 或公共函数,其注册仅保留在测试本地。它证明的是集成路径,而不是原生执行。 +2. **共享集成。** `TestThirdHarnessPublicOnboarding` 让合成 Harness 通过公共 Session 和输入准入、Worker 设备选择、真实 WebSocket gateway、daemon Registry 和 Router、中立事件以及持久化终态投影运行。它以 `mcode` kind 注册,因此 Core 按 MiniMax Code 的声明准入它,并检查已应用输入回执、已保存原生身份、继续执行、取消、不受支持的可选请求以及缺少强制 Runtime 支持。该夹具没有工作区、MCP 或公共函数,其注册仅保留在测试本地。它证明的是集成路径,而不是原生执行。 3. **真实验收。** 使用锁定的官方 Python SDK 和针对 Core 的原始 HTTP、真实 Provider API、原生 Harness 以及专用数据库。验证初始执行、热后续执行、取消以及带继续执行的重启;记录原生所有者身份以及相同条件下的冷启动和热运行时间。对于工作区放置方式,还要验证 Files 和 Artifacts、工作区身份、公开响应中未出现凭据,以及外部历史会被拒绝。`services/core/tests/official_hosted_functions_native.py` 保存共享函数断言:成功和错误、原生文件输出和公共 Artifact 字节、重启后的同历史继续执行、外部结果拒绝以及待处理调用取消。合成运行或失败运行绝不计入。下面的选择性测试会在 `services/core/tests` 中针对真实 daemon 和模型运行锁定 SDK 夹具;设置 `OAC_TEST_OFFICIAL_SDK_PYTHON`、`OAC_TEST_NATIVE_DAEMON_BIN`、`OAC_TEST_NATIVE_PROOF_DIR` 及其私有选项文件后,每项测试才会运行。选项文件是一个 JSON 对象,恰好包含 `model` 和 `model_provider`(即 `x_agents_core.model_provider` 的字段);测试会将其设置为部署默认模型 Provider,而夹具的 `environment: none` Session 会在创建时将其冻结。 4. **回归。** 现有 Harness 必须继续正常工作。先运行定向测试,然后运行 `make check`;API 更改后运行 `make openapi`,查询更改后运行 `make sqlc-generate`。 5. **审查。** 遵循 [blind review workflow](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review)。 diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 85e24eac5..2fd794cfd 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "Agents API 覆盖台账" source: contracts/agents-api/index.md -source_hash: fe62502746c71dd9444acab51d5bf9e6929107b5e905d0348078376c635faacc +source_hash: 3e6abc5b98cd47c332ff6f5c12fc8676a35428da02a6caae2648dc8bab6bc486 --- Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([public API rule](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#public-api))。本台账记录 Core 对各项资源实现了哪些内容、哪些契约保存其详细信息,并列出相对于 OpenAI 服务的所有已知差异和所有未解决缺口。[API namespaces and credentials](../../../docs/zh/api/index.md) 说明谁调用哪些 API;[Agents API guide](../../../docs/zh/api/public-agent-api.md) 介绍使用方法。 @@ -48,7 +48,7 @@ Go 输入投影排除 `packages.system`,保留下方记录的明确拒绝行 | Files | create, retrieve, list, delete, content | 已支持 `purpose=user_data`;内容下载会被拒绝 | [Files and Skills](source-files.md) | | Skills and Skill versions | create, retrieve, update, list, delete, content | 已实现 | [Files and Skills](source-files.md) | -各 Harness 在不同部署位置支持哪些操作,请参阅 [Harness capabilities](harness-capabilities.md)。[Core wire behavior](wire-semantics.md) 包含适用于各项资源的通用规则:请求、错误和列表。 +每个 Harness 只在 `internal/harnessconfig/` 中声明一次自己的支持范围([声明支持](harness-onboarding.md#declare-support))。[Core wire behavior](wire-semantics.md) 包含适用于各项资源的通用规则:请求、错误和列表。 Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh/api/public-agent-api.md#core-extensions-x-agents-core))。Core 管理 API(`/core/v1`)和机器 API(`/api/v1`)不属于 Agents API。 @@ -112,7 +112,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh **配置和工具** - 显式指定推理强度或摘要、使用 `auto` 之外的服务层级、启用 `web_search` 或启用程序化工具调用,这些设置都会被保存,但在 Session 准入时会被拒绝。 -- Harness 对工具、结构化输出、延迟发现、subagents 和 MCP 的支持因 Harness 和部署位置而异;请参阅 [Harness capabilities](harness-capabilities.md)。MiniMax Code 不提供公共 functions、没有服务源 MCP,也不支持图像输入。 +- 各 Harness 的支持差异以其[声明](harness-onboarding.md#declare-support)为准。Codex 不支持结构化输出或 `tool_search`。Claude Code 不接受仅含空白的文本,只支持 `medium` 详细程度,函数结果图像只能内联且只能出现在成功结果中;它拒绝结构化输出与子智能体、MCP、已安装能力或 `tool_search` 同时使用,也拒绝 `tool_search` 与 MCP 或已安装能力同时使用。MiniMax Code 不提供公共 functions,没有服务源 MCP,不支持图像输入、仅含空白的文本和必需 MCP,只支持 `medium` 详细程度,且只接受值为 null 的 `allowed_tools`。每个 Harness 都保留一个 MCP `server_label`:Codex 保留 `codex_apps`,Claude Code 保留 `functions`,MiniMax Code 保留 `oac_workspace`。Claude Code 还要求标签匹配 `^[a-zA-Z0-9_-]+$`,`allowed_tools` 中的名称匹配 `^[a-zA-Z0-9_.-]+$`。 - 由模型推导出的推理默认值不会被解析确定。 - MCP 工具仅支持 `http` 传输,`stdio` 会被拒绝,Session MCP 传输中的内联 `authorization` 也会被拒绝([HTTP MCP](execution-tools.md#http-mcp))。 @@ -125,7 +125,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - 在[凭据网关](./model-execution.md#credential-gateway)之后,固定版本的 Codex 在本地压缩历史,从不调用 `/responses/compact`。 - Claude Code 和 MiniMax Code 都不报告公共用量。 - 对于原生副作用,Core 不提供崩溃安全或恰好一次保证;已认领的工作若不重放,会在重启后失败。 -- 图像必须是内嵌的 PNG 或 JPEG data URI;远程 URL、`file_id` 和 `detail` 会被拒绝。 +- 消息图像必须是内嵌的 PNG 或 JPEG data URI;远程 URL、`file_id` 和 `detail` 会被拒绝。 **Environments 和 Templates** diff --git a/contracts/agents-api/zh/message-content.md b/contracts/agents-api/zh/message-content.md index 6e330bb8f..41738fc70 100644 --- a/contracts/agents-api/zh/message-content.md +++ b/contracts/agents-api/zh/message-content.md @@ -1,7 +1,7 @@ --- title: "消息内容" source: contracts/agents-api/message-content.md -source_hash: 085ade22b792243b8fea4fb798f1de3cbed872d27752afa4f30b93f251b829a8 +source_hash: 8758bbcce858298277343026e303e6d7939c849e1995b8a6cf5fc59017cda49c --- 用户消息和函数结果共享同一内容模型:由 `input_text` 与 `input_image` 部分组成的有序列表。Core 按发送形式准确存储消息边界、部分顺序和图像引用,并在用户 Item 中原样返回。不下载、转码或修复媒体。Session 创建的 `input` 与 `events.create` 消息共享验证和准入;[Session、事件与历史](sessions-events.md#send-input)定义准入、请求限制和错误。 @@ -27,11 +27,11 @@ source_hash: 085ade22b792243b8fea4fb798f1de3cbed872d27752afa4f30b93f251b829a8 | Claude Code | 在 `none`、`openai_hosted`、`self_hosted` 接受 | | MiniMax Code | 拒绝 | -任何写入之前,准入检查 Harness;Harness 无法接受的图像返回 400。Runtime 也必须报告消息图像支持:Core 仅将输入含图像的 Session 绑定到此类 Runtime,交付给不支持的 Runtime 会失败。应使用接受图像的模型。 +任何写入之前,准入检查 Harness;Harness 无法接受的图像返回 400 `unsupported_or_invalid_configuration`。Runtime 也必须报告消息图像支持:Core 仅将输入含图像的 Session 绑定到此类 Runtime,交付给不支持的 Runtime 会失败。应使用接受图像的模型。 ## 仅空白文本 {#whitespace-only-text} -`" "` 或 `"\n\t"` 等仅含空白的文本为有效内容,原样存储和返回。Harness 能否运行由引擎配置声明: +`" "` 或 `"\n\t"` 等仅含空白的文本为有效内容,原样存储和返回。Harness 能否运行属于其[声明](harness-onboarding.md#declare-support): | Harness | 无图像且无非空白文本的消息 | | --- | --- | @@ -52,7 +52,7 @@ source_hash: 085ade22b792243b8fea4fb798f1de3cbed872d27752afa4f30b93f251b829a8 | Harness | 函数结果 | | --- | --- | | Codex | 文本与有序文本/图像输出。Core 仅检查各部分格式正确,并将图像引用原样传给 Harness | -| Claude Code | 文本输出。仅成功结果可含内联 PNG 或 JPEG 图像;失败结果图像或远程引用在任何存储前返回 400,待处理调用保持开放。Harness 可在原生历史中调整图像尺寸或重新编码;公开 Item 保留提交字节 | +| Claude Code | 文本输出。仅成功结果可含内联 PNG 或 JPEG 图像;失败结果图像、远程引用或格式错误的引用在任何存储前返回 400 `unsupported_or_invalid_configuration`,待处理调用保持开放。Harness 可在原生历史中调整图像尺寸或重新编码;公开 Item 保留提交字节 | | MiniMax Code | 无公开函数 | ### 应用回执 {#application-receipts} @@ -66,7 +66,7 @@ source_hash: 085ade22b792243b8fea4fb798f1de3cbed872d27752afa4f30b93f251b829a8 ## Runtime 边界 {#runtime-boundary} -Core–Runtime wire 在初始输入、准备后启动和引导中将消息作为 `MessageInput` 携带,使用与函数结果相同的有序 `InputContent` 部分([Core–Runtime 协议](../../../docs/zh/runtime-protocol.md))。准入检查 Harness 声明配置;绑定和交付检查 Runtime 报告。适配器负责原生编码和应用回执。仅文本适配器拒绝图像部分,不丢弃它们。 +Core–Runtime wire 在初始输入、准备后启动和引导中将消息作为 `MessageInput` 携带,使用与函数结果相同的有序 `InputContent` 部分([Core–Runtime 协议](../../../docs/zh/runtime-protocol.md))。准入检查 Harness 的声明;绑定和交付按 Runtime 心跳收窄后的声明检查。适配器负责原生编码和应用回执。仅文本适配器拒绝图像部分,不丢弃它们。 - **Codex** 将批次展平为原生输入列表,在公开消息之间插入空行分隔。公开消息边界保留于 Core 存储,原生历史不保留。 - **Claude Code** 发送原生图像块及每条原生用户消息的 UUID。一个公开输入仅在批次所有消息消费后视为已应用。单个原生 Turn 内桥接层最多接受 64 条用户消息(含开场提示);超过界限的引导批次在提交任何部分前被拒绝,并结束运行 Turn。daemon 要求桥接协议 3。 diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 41650bc31..01c2d0508 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: e0d23ec03ffdfa296dcfb02fb595164ed4170183a89f1c6c209bdc8928e5c595 +source_hash: 2f4cbc16107fe0aeb26f911c4e1275803e992de90d26b9a4295d39472b2c9d3f --- 每个 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) 定义 Core 和 Runtime 共同应用的[提供商规则](#session-override),并声明[凭据网关](#credential-gateway)转发的内容,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -17,7 +17,7 @@ source_hash: e0d23ec03ffdfa296dcfb02fb595164ed4170183a89f1c6c209bdc8928e5c595 - 省略:Session 继承其已保存 Agent 的 Harness;内联 Agent 使用部署默认 Harness。 - Session 的内联扩展显式为 null:重置为部署默认 Harness,同时保留继承的提供商配置包。对于已保存 Agent,null 扩展会清除其 Harness 和提供商。 - 所选 Harness 必须已启用;Core 绝不会回退到其他 Harness。 -- 创建 Session 时,Core 在处理已保存 Agent 的覆盖值后解析该选择,验证 Harness 配置文件,并将结果存储为 Session 的引擎。当生效的 Agent 包含该扩展时,Session 读取结果会报告它;其他 Session 保持官方 Agent 结构。读取操作从不查询当前 Agent 或部署默认值。 +- 创建 Session 时,Core 在处理已保存 Agent 的覆盖值后解析该选择,根据 Harness 的[声明](harness-onboarding.md#declare-support)检查配置,并将结果存储为 Session 的引擎。当生效的 Agent 包含该扩展时,Session 读取结果会报告它;其他 Session 保持官方 Agent 结构。读取操作从不查询当前 Agent 或部署默认值。 - 使用显式选择器重试创建时会保留调用方意图;在已有 Idempotency-Key 下更改选择器会产生冲突。 Session 的 `environment` 和 Environment Templates 用于选择准备流程,而不是 Harness 或提供商。该扩展仅在 `contracts/agents-api/v1` 中定义一次;校验器从目录派生,任何处理程序或 schema 都不会维护自己的名称列表。 diff --git a/contracts/agents-api/zh/subagents.md b/contracts/agents-api/zh/subagents.md index 5c6128be2..7260e2553 100644 --- a/contracts/agents-api/zh/subagents.md +++ b/contracts/agents-api/zh/subagents.md @@ -1,10 +1,10 @@ --- title: "子智能体" source: contracts/agents-api/subagents.md -source_hash: 8fed8b73a8403d2ccf80354b2a981f11240eba3c10d5a0257206a39d048b92a7 +source_hash: 7160c78c2f564b86154594d3c9735383300a692d9cdbc4569f628cb449771bc1 --- -启用 `multi_agent.enabled` 后,Harness 可以启动原生子智能体。Core 通过锁定版本的 SDK(见 [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))中的六项 Subagent 读取操作公开它们,并根据适配器观察结果记录它们。当 `multi_agent.enabled=false` 时,Runtime 会移除原生子智能体工具。[Harness capabilities](harness-capabilities.md) 列出各 Harness 在哪些组合中支持子智能体。 +启用 `multi_agent.enabled` 后,Harness 可以启动原生子智能体。Core 通过锁定版本的 SDK(见 [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))中的六项 Subagent 读取操作公开它们,并根据适配器观察结果记录它们。当 `multi_agent.enabled=false` 时,Runtime 会移除原生子智能体工具。每个 Harness 都支持子智能体。任何 Harness 都不会在启用函数工具或 `agent.tools` MCP 时运行子智能体,Claude 还会拒绝已安装的 Plugin MCP 或结构化输出与子智能体同时使用([声明支持](harness-onboarding.md#declare-support))。 ## 公开读取 {#public-reads} @@ -52,8 +52,6 @@ Core 根据已授权的 Session 绑定关系分配合公开 ID 和所有权。 ## 原生配置 {#native-profiles} -[Harness capabilities](harness-capabilities.md#tools) 列出被拒绝的工具组合。 - **Codex.** 适配器会启用原生 `multi_agent` 特性,将嵌套深度设为 64,并把并发限制映射到 `agents.max_threads`。它会禁用原生钩子、插件、代码模式和 `multi_agent_v2`;如果原生钩子列表非空,或托管配置要求强制启用冲突特性,则拒绝启动。关闭和重新打开的事实来自直接工具输出,并与同一次调用的持久化完成记录相关联,因此需要原生持久化回执。原生 Turn 时间具有秒级精度。根级 Turn 完成后,子级文件工作可以在同一所有者下完成。超过调用方截止时间后,取消操作仍会在同一所有者下继续;后续调用可以确认已经收敛,而无需重复执行原生中断。 **Claude SDK.** 锁定版本 SDK 的原生 Agent 和 SendMessage 调用会运行唯一的子级类型 `oac_worker`;该类型继承模型,并可使用工作区中的 Bash、Agent 和 SendMessage;bridge 的 `subagent_resources` 特性控制是否启用它。子级使用原生 Bash,权限继承自父级的启动用户。私有子级记录用于确定父子关系、首次自身输入时间及后续自身 Turns;继承的父级上下文会被排除。查询所有者会在启动前接纳子级,并将子级历史保留至工作收敛。已确认的取消操作会写入不可变的操作回执,因为原生中止可能不会留下终态记录。不存在关闭操作:已完成或已取消的子级保持 active 状态。向运行中的子级发送消息、使用后台工作、采用其他子级配置以及按次覆盖模型都会被拒绝。[Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) 记录了详细信息。 diff --git a/docs.json b/docs.json index e75e5295b..112a68a59 100644 --- a/docs.json +++ b/docs.json @@ -27,7 +27,7 @@ "docs/api/public-agent-api", "contracts/agents-api/admin-api", "contracts/agents-api/machine-api", - "contracts/agents-api/harness-capabilities" + "contracts/agents-api/index" ] }, { diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index 869b0a4c4..5c3d1a8c9 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -158,7 +158,7 @@ The harness is the agent program that runs a Session: Codex (`codex`), Claude Co - **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). -Not every combination of harness, placement and operation is supported; the [Harness capabilities](../../contracts/agents-api/harness-capabilities.md) lists them. +Not every combination of harness, placement and operation is supported. An unsupported one returns 400 `unsupported_or_invalid_configuration` with the rejected field in `param`; [known gaps](../../contracts/agents-api/index.md#known-gaps) lists each harness's differences. ## Agents @@ -535,7 +535,7 @@ The HTTP path is `/vaults`, with the Beta header. A Session selects credentials 1. Read the Session's `status` and `error`, and the latest Turn's `error`. A failed Turn reports only a generic `internal_error`. 2. Check that the Environment is connected and its harness is available. -3. Check the harness, model and tool combination in [Harness capabilities](../../contracts/agents-api/harness-capabilities.md). +3. Check the harness, model and tool combination against [known gaps](../../contracts/agents-api/index.md#known-gaps). 4. Ask the administrator for the Session's [diagnostics](../../contracts/agents-api/session-diagnostics.md), which name the failure category, and to check [troubleshooting](../getting-started/operations.md#troubleshooting) for service logs, credentials and node readiness. A 401 usually means a key from another namespace; see [API namespaces and credentials](./index.md). diff --git a/docs/development.md b/docs/development.md index 71d3c842a..f11c40757 100644 --- a/docs/development.md +++ b/docs/development.md @@ -70,7 +70,7 @@ For frontend development, run `pnpm dev:web` using the fixture or Core connectio | `services/core/internal/persistence`, `services/core/internal/db` and `services/core/migrations` | Core's PostgreSQL adapters, transactions, queries and migrations | [Service guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#database) | | `services/core/tests/integration` | Tests that drive the HTTP boundaries, the execution Worker and the PostgreSQL adapters together | [Service guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#tests) | | `services/core/internal/execution` | Durable Turn dispatch and scheduling | [Runtime protocol](./runtime-protocol.md) | -| `services/core/internal/engine` | Pure qualification of harness operations and placements | [Harness onboarding](../contracts/agents-api/harness-onboarding.md) | +| `internal/harnessconfig` | Built-in Harness registrations and their support declarations | [Declare support](../contracts/agents-api/harness-onboarding.md#declare-support) | | `internal/agentdaemon/proto` | Core–Runtime wire types and validators | [Runtime protocol](./runtime-protocol.md) | | `internal/runtimebootstrap` | Provider-to-Runtime startup input | [Runtime bootstrap](./runtime-bootstrap.md) | | `internal/sandboxwire` | Frame header, primitive encoding and request ID sequence shared by the sandbox I/O protocols | [Framing](./sandbox-link-protocol.md#framing) | diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 9fca31b63..857c81f81 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -24,29 +24,26 @@ Each physical connection has fresh routing, admission handles and transfer state ## Capability declarations -`AgentKindCapabilities` in [`inbound.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) describes one composed Runtime and Harness, independently of `available` and of Core's engine profile. Every field is a `CapabilitySupport`: supported or unsupported. The zero value is unspecified and invalid, even for an unavailable Harness. Registration validates the complete declaration before changing the registry; there is no implicit basic descriptor. +`AgentKindCapabilities` in [`inbound.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) describes one composed Runtime and Harness, independently of `available`. It is the part of the Harness's [declaration](../contracts/agents-api/harness-onboarding.md#declare-support) that an installation narrows. Every field is a `CapabilitySupport`: supported or unsupported. The zero value is unspecified and invalid, even for an unavailable Harness. Registration validates the complete declaration before changing the registry; there is no implicit basic descriptor. On the wire each field is a JSON boolean, and every field is present, including `false`. Encoding an incomplete declaration fails. Decoding rejects omitted, null, invalid and unknown fields, and a missing capability object. An invalid heartbeat clears the connection's admission snapshot and closes its transport; that establishes no native completion or cancellation result. -Each admitted Executor and Turn keeps the declaration it was admitted with. A later heartbeat cannot add operations to an existing owner. Optional operations check this snapshot before any native call; the presence of a Go interface never grants support. A declared operation that returns `agent.ErrUnsupportedOperation` is a contract violation, distinct from unavailability, a failed native call or an uncertain write. Uncertain operations keep their receipts and ownership and are never replayed automatically. A Runtime declares `local_environment` and `environment_none` only where both the Harness and its [Environment owner](#session-assignments) support them, and `workspace_read_preparation` and `workspace_output_export` exactly where it declares `local_environment`, never from a Harness adapter. +Each admitted Executor and Turn keeps the declaration it was admitted with. A later heartbeat cannot add operations to an existing owner. Optional operations check this snapshot before any native call; the presence of a Go interface never grants support. A declared operation that returns `agent.ErrUnsupportedOperation` is a contract violation, distinct from unavailability, a failed native call or an uncertain write. Uncertain operations keep their receipts and ownership and are never replayed automatically. A Runtime declares `local_environment` and `environment_none` only where both the Harness and its [Environment owner](#session-assignments) support them; `local_environment` also covers the owner's workspace reads, read-only preparation and output export. A new field requires an explicit decision in every production declaration. Contract tests enumerate every field for registration and wire round trips; the shared test fixture lists fields individually and supplies no defaults for future ones. [Harness onboarding](../contracts/agents-api/harness-onboarding.md) owns the adapter side of each declaration. -A declaration describes what the Runtime can do. Core admits a public feature only when the Harness's engine profile also qualifies it. During device selection and again at the final check before it claims a Turn, Core requires the selected device to report the Harness `available` and checks its declaration: +A heartbeat only narrows the Harness's static declaration. Core treats a heartbeat as invalid when a kind has no built-in declaration or advertises support its static declaration lacks. During device selection and again at the final check before it claims a Turn, Core requires the selected device to report the Harness `available` and checks the Session against the static declaration narrowed to the heartbeat with `proto.ValidateSelection`, which requires: | Capability | Core requires it when | | --- | --- | | `environment_none` | The Environment type is `none` | -| `local_environment`, `workspace_read_preparation`, `workspace_output_export` | The Environment type is `openai_hosted` or `self_hosted` | -| `workspace_read_preparation` | An idle Files directory read needs a read-only preparation | +| `local_environment` | The Environment type is `openai_hosted` or `self_hosted`, or an idle Files directory read needs a read-only preparation | | `native_session_recovery` | A Session with a started Turn has no recorded native Session ID | -| `web_search_control`, `text_verbosity` | The Harness's engine profile declares that control | +| `text_verbosity` | The Agent requests a verbosity other than `medium` | | `structured_output` | The Agent requests `json_schema` output | | `subagent_observations` | `multi_agent.enabled` is true | -| `subagent_control` | `multi_agent.enabled` is false | | `tool_search` | The Agent enables tool search or defers function loading | -| `programmatic_tool_calling_disable` | The Agent explicitly disables programmatic tool calling | -| `function_tools` | The Agent declares function tools | +| `function_tools` | The Agent declares function tools, or a function result is delivered | | `message_images`, `function_result_images` | A message, or a function result, carries an image | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | The Agent declares HTTP MCP servers; one is `required`; a Vault credential is selected for one | @@ -57,7 +54,7 @@ The `execution_prepare` configuration carries the Session's model configuration | Field | Set by Core | | --- | --- | | `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 | +| `execution_controls` | Always: 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_subagent_identities`, `disable_subagents` | From the Agent's `multi_agent.enabled` | | `disable_execution_environment` | For an Environment of type `none` | | `local_environment` | For `openai_hosted` and `self_hosted`, with the exact Environment binding. The request carries no working directory; the Runtime checks `workspace_directory` against its binding | @@ -197,7 +194,7 @@ Core stores accepted values in the Turn outcome as `engine_error_code` and `engi ## Workspace operations -A workspace read that needs no running Turn uses the read-only preparation profile: `execution_prepare` with `workspace_read_only`, which requires the `workspace_read_preparation` capability. It accepts only the bound Environment and resource identity; execution options, model and MCP credentials, native Session continuation and model or tool input are excluded, and the owner rejects `execution_start`. The Session's Environment owner serves it without starting a Harness process. The profile publishes `released` after the Runtime drops the preparation's ownership; a stale status snapshot never publishes success. A release request, HTTP disconnect or remote socket closure alone does not confirm the release. +A workspace read that needs no running Turn uses the read-only preparation profile: `execution_prepare` with `workspace_read_only`, which requires the `local_environment` capability. It accepts only the bound Environment and resource identity; execution options, model and MCP credentials, native Session continuation and model or tool input are excluded, and the owner rejects `execution_start`. The Session's Environment owner serves it without starting a Harness process. The profile publishes `released` after the Runtime drops the preparation's ownership; a stale status snapshot never publishes success. A release request, HTTP disconnect or remote socket closure alone does not confirm the release. `workspace_read` lists one workspace-relative directory (an empty path selects the root) of an existing preparation handle, or the Run it was transferred to, on the same authenticated device connection, with the exact frozen Environment identity; callers cannot supply sockets, credentials or workspace roots. A result carries at most `max_entries` (1 to 1024) single-component UTF-8 names of at most 255 bytes each, the entry kind, regular-file sizes and explicit truncation, and is returned only after directory access and handle cleanup settle. There is no snapshot, recursion or pagination at this layer. diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 121358a74..f7703954d 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: 07bcf81c1f4d131d55e38bf1158805a24e53bae13c79205857dc8fbe25001123 +source_hash: 78f630fdbc9a1ff51ebcbe4de53708f79a7ad8a75546ad19d14baa9baafa0ddb --- Core 在 `/v1` 提供 [OpenAI Agents API](https://platform.openai.com/docs/api-reference)。可以使用官方 OpenAI SDK 或普通 HTTP。本指南针对每项常见操作同时展示这两种方式,并说明 Core 与 OpenAI 存在差异的地方。 @@ -160,7 +160,7 @@ harness 是运行 Session 的 Agent 程序:Codex(`codex`)、Claude Code( - **提供商。** 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)。 -并非每种 harness、部署位置和操作组合都受支持;[Harness capabilities](../../../contracts/agents-api/zh/harness-capabilities.md) 列出了受支持的组合。 +并非每种 harness、部署位置和操作组合都受支持。不受支持的组合返回 400 `unsupported_or_invalid_configuration`,并在 `param` 中给出被拒绝的字段;[已知缺口](../../../contracts/agents-api/zh/index.md#known-gaps)列出各 harness 的差异。 ## Agents(智能体) {#agents} @@ -537,7 +537,7 @@ HTTP 路径为 `/vaults`,需要 Beta 请求头。Session 从其 `vault_ids` 1. 读取 Session 的 `status` 和 `error`,以及最新 Turn 的 `error`。失败的 Turn 只会报告通用的 `internal_error`。 2. 检查 Environment 是否已连接,以及其 harness 是否可用。 -3. 在 [Harness capabilities](../../../contracts/agents-api/zh/harness-capabilities.md) 中检查 harness、模型和工具的组合。 +3. 对照[已知缺口](../../../contracts/agents-api/zh/index.md#known-gaps)检查 harness、模型和工具的组合。 4. 向管理员索取 Session 的[诊断信息](../../../contracts/agents-api/zh/session-diagnostics.md),其中会指出故障类别;同时请查看[故障排除](../getting-started/operations.md#troubleshooting),了解服务日志、凭据和节点就绪状态。 401 通常表示使用了其他命名空间中的密钥;请参阅 [API 命名空间与凭据](index.md)。 diff --git a/docs/zh/development.md b/docs/zh/development.md index 2ddd14279..fdc1aef19 100644 --- a/docs/zh/development.md +++ b/docs/zh/development.md @@ -1,7 +1,7 @@ --- title: "开发 OpenAgentCore" source: docs/development.md -source_hash: f0e5430d71b2d1f5f34569f9e4d0b623b9983175758bb217fa5f208913118da8 +source_hash: f3e891be155a821f011ebc75916a6b5c9a68984d1ffe29753fdce59162577c51 --- 准备工作副本,构建组件并验证修改。如需使用已安装的实例,从[入门指南](getting-started/index.md)开始。修改代码前阅读[贡献者规则](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md)。 @@ -72,7 +72,7 @@ Core 构建产物和输出目录设置见[独立 Core 构建](maintainers.md#sta | `services/core/internal/persistence`、`services/core/internal/db` 和 `services/core/migrations` | Core 的 PostgreSQL adapter、事务、查询和迁移 | [服务指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#database) | | `services/core/tests/integration` | 共同驱动 HTTP 边界、执行 Worker 和 PostgreSQL adapter 的测试 | [服务指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#tests) | | `services/core/internal/execution` | 持久化 Turn 分发与调度 | [Runtime 协议](runtime-protocol.md) | -| `services/core/internal/engine` | 对 harness 操作与执行位置进行纯资格验证 | [Harness 接入](../../contracts/agents-api/zh/harness-onboarding.md) | +| `internal/harnessconfig` | 内置 Harness 注册及其支持声明 | [声明支持](../../contracts/agents-api/zh/harness-onboarding.md#declare-support) | | `internal/agentdaemon/proto` | Core–Runtime wire 类型与验证器 | [Runtime 协议](runtime-protocol.md) | | `internal/runtimebootstrap` | Provider 到 Runtime 的启动输入 | [Runtime 引导](runtime-bootstrap.md) | | `internal/sandboxwire` | 各沙箱 I/O 协议共享的帧头、基本类型编码和 request ID 序列 | [帧格式](sandbox-link-protocol.md#framing) | diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 854fcc55e..767f188c1 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: 87a7ecf9bfe2dbf03725c69ac68ae4abf4ee7b25206e49e72a4eb45fc160ca35 +source_hash: 9d9f7867b0d5d88d0212ffaa9ef55ddf4aa5624a53717f2be92e0453f2049f01 --- 此协议在 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)。 @@ -26,29 +26,26 @@ wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blo ## 能力声明 {#capability-declarations} -[`inbound.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) 中的 `AgentKindCapabilities` 描述一个 Runtime 与 Harness 组合,独立于 `available` 和 Core 的 engine profile。每个字段都是 `CapabilitySupport`:支持或不支持。零值表示未指定且无效,即使 Harness 不可用也如此。注册在修改 registry 前验证完整声明;不存在隐含的基础 descriptor。 +[`inbound.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) 中的 `AgentKindCapabilities` 描述一个 Runtime 与 Harness 组合,独立于 `available`。它是 Harness [声明](../../contracts/agents-api/zh/harness-onboarding.md#declare-support)中由安装收窄的部分。每个字段都是 `CapabilitySupport`:支持或不支持。零值表示未指定且无效,即使 Harness 不可用也如此。注册在修改 registry 前验证完整声明;不存在隐含的基础 descriptor。 wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 `false`。不完整声明编码失败。解码拒绝省略、null、无效和未知字段,以及缺失的 capability 对象。无效 heartbeat 会清空连接的 admission snapshot 并关闭 transport;这不证明原生完成或取消结果。 -每个已准入的 Executor 和 Turn 保留准入时的声明。后续 heartbeat 不能给已有 owner 增加操作。可选操作在任何原生调用前检查此快照;存在 Go interface 不代表支持。已声明操作返回 `agent.ErrUnsupportedOperation` 属于契约违规,与不可用、原生调用失败或不确定写入不同。不确定操作保留回执与所有权,绝不自动重放。Runtime 仅在 Harness 及其 [Environment owner](#session-assignments) 都支持时声明 `local_environment` 和 `environment_none`,并恰好在声明 `local_environment` 时声明 `workspace_read_preparation` 和 `workspace_output_export`,从不由 Harness adapter 声明后两者。 +每个已准入的 Executor 和 Turn 保留准入时的声明。后续 heartbeat 不能给已有 owner 增加操作。可选操作在任何原生调用前检查此快照;存在 Go interface 不代表支持。已声明操作返回 `agent.ErrUnsupportedOperation` 属于契约违规,与不可用、原生调用失败或不确定写入不同。不确定操作保留回执与所有权,绝不自动重放。Runtime 仅在 Harness 及其 [Environment owner](#session-assignments) 都支持时声明 `local_environment` 和 `environment_none`;`local_environment` 同时涵盖 owner 的工作区读取、只读 preparation 和输出导出。 新增字段要求每个生产声明都作出明确决定。契约测试为注册和 wire 往返逐一枚举字段;共享测试 fixture 单独列出字段,不为未来字段提供默认值。[Harness 接入](../../contracts/agents-api/zh/harness-onboarding.md)负责各声明的 adapter 侧规则。 -声明描述 Runtime 能做什么。Core 仅在 Harness 的 engine profile 也通过资格验证时准入公开功能。在设备选择及领取 Turn 前的最终检查中,Core 要求选定设备报告该 Harness 为 `available`,并检查其声明: +heartbeat 只能收窄 Harness 的静态声明。某个 kind 没有内置声明,或宣称了其静态声明不具备的支持时,Core 将该 heartbeat 视为无效。在设备选择及领取 Turn 前的最终检查中,Core 要求选定设备报告该 Harness 为 `available`,并用 `proto.ValidateSelection` 按收窄到该 heartbeat 的静态声明检查 Session,它要求: | 能力 | Core 何时要求 | | --- | --- | | `environment_none` | Environment 类型为 `none` | -| `local_environment`, `workspace_read_preparation`, `workspace_output_export` | Environment 类型为 `openai_hosted` 或 `self_hosted` | -| `workspace_read_preparation` | 空闲 Files 目录读取需要只读 preparation | +| `local_environment` | Environment 类型为 `openai_hosted` 或 `self_hosted`,或空闲 Files 目录读取需要只读 preparation | | `native_session_recovery` | Session 已启动过 Turn,但未记录原生 Session ID | -| `web_search_control`, `text_verbosity` | Harness 的 engine profile 声明该控制 | +| `text_verbosity` | Agent 请求 `medium` 以外的 verbosity | | `structured_output` | Agent 请求 `json_schema` 输出 | | `subagent_observations` | `multi_agent.enabled` 为 true | -| `subagent_control` | `multi_agent.enabled` 为 false | | `tool_search` | Agent 启用 tool search 或延迟 function 加载 | -| `programmatic_tool_calling_disable` | Agent 明确禁用 programmatic tool calling | -| `function_tools` | Agent 声明 function tool | +| `function_tools` | Agent 声明 function tool,或交付 function result | | `message_images`, `function_result_images` | 消息或 function result 携带图像 | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | Agent 声明 HTTP MCP server;其中一个为 `required`;其中一个选用了 Vault 凭据 | @@ -59,7 +56,7 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` | 字段 | 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 在产生任何原生效果之前验证它们 | -| `execution_controls` | 始终设置:web search 为 `disabled`、解析后的 text verbosity(默认 `medium`)、明确禁用 programmatic tool calling,以及任何 `json_schema` 输出格式。原生选项名称由 adapter 负责 | +| `execution_controls` | 始终设置:解析后的 text verbosity(默认 `medium`)、明确禁用 programmatic tool calling,以及任何 `json_schema` 输出格式。原生选项名称由 adapter 负责 | | `observe_subagent_identities`, `disable_subagents` | 根据 Agent 的 `multi_agent.enabled` 设置 | | `disable_execution_environment` | Environment 类型为 `none` 时设置 | | `local_environment` | 为 `openai_hosted` 和 `self_hosted` 设置,包含精确的 Environment 绑定。请求不携带 working directory;Runtime 按自身绑定检查 `workspace_directory` | @@ -199,7 +196,7 @@ Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engin ## 工作区操作 {#workspace-operations} -无需运行 Turn 的工作区读取使用只读 preparation profile:带 `workspace_read_only` 的 `execution_prepare`,要求 `workspace_read_preparation` 能力。仅接受绑定的 Environment 和 resource 身份;不包含 execution option、model 与 MCP 凭据、原生 Session continuation、model 或 tool 输入,owner 拒绝 `execution_start`。Session 的 Environment owner 提供读取,不启动 Harness 进程。profile 在 Runtime 放弃该 preparation 的所有权后发布 `released`;旧 status snapshot 不发布成功。release 请求、HTTP 断连或远端 socket 关闭本身都不确认释放。 +无需运行 Turn 的工作区读取使用只读 preparation profile:带 `workspace_read_only` 的 `execution_prepare`,要求 `local_environment` 能力。仅接受绑定的 Environment 和 resource 身份;不包含 execution option、model 与 MCP 凭据、原生 Session continuation、model 或 tool 输入,owner 拒绝 `execution_start`。Session 的 Environment owner 提供读取,不启动 Harness 进程。profile 在 Runtime 放弃该 preparation 的所有权后发布 `released`;旧 status snapshot 不发布成功。release 请求、HTTP 断连或远端 socket 关闭本身都不确认释放。 `workspace_read` 在同一已认证设备连接上,针对现有 preparation handle 或它已转移给的 Run,使用精确冻结的 Environment 身份,列出一个 workspace 相对目录(空路径选择根目录);调用方不能提供 socket、凭据或 workspace root。结果最多携带 `max_entries`(1 到 1024)个单路径组件 UTF-8 名称,每个最多 255 字节,并包含 entry kind、普通文件大小和明确截断信息;仅在目录访问与 handle 清理结算后返回。此层没有快照、递归或分页。 diff --git a/internal/agentdaemon/proto/capabilities.go b/internal/agentdaemon/proto/capabilities.go index 22d79c8fc..e6cf657c9 100644 --- a/internal/agentdaemon/proto/capabilities.go +++ b/internal/agentdaemon/proto/capabilities.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "reflect" + "regexp" + "slices" ) // CapabilitySupport requires a deliberate decision. Its zero value is invalid; @@ -90,6 +92,78 @@ func (c *AgentKindCapabilities) UnmarshalJSON(data []byte) error { return nil } +// Declaration is one Harness's support, authored once in +// internal/harnessconfig/. Capabilities is the maximum a Runtime may +// advertise; discovery and the Environment owner only narrow it. The other +// fields never change at runtime and are never sent: Core reads them from the +// built-in declaration. ValidateSelection is the only reader. +type Declaration struct { + Capabilities AgentKindCapabilities + // WhitespaceOnlyText admits a message without an image or any + // non-whitespace text. + WhitespaceOnlyText CapabilitySupport + // FunctionResultImageURLs admits remote image references in function + // results; otherwise their images are inline PNG or JPEG. + FunctionResultImageURLs CapabilitySupport + // FailedFunctionResultImages admits images in a failed function result. + FailedFunctionResultImages CapabilitySupport + // MCPAllowedTools admits an MCP allowed_tools list; otherwise it is null. + MCPAllowedTools CapabilitySupport + // MCPOrigins lists the MCP connection origins the Harness serves. + MCPOrigins []string + // ReservedMCPLabels are MCP server labels the adapter uses itself. + ReservedMCPLabels []string + // MCPLabel and MCPToolName, when set, restrict MCP server labels and + // allowed tool names. + MCPLabel, MCPToolName *regexp.Regexp + // Binary64OutputSchema requires a json_schema output schema with an + // object root and numbers that survive binary64. + Binary64OutputSchema bool + // Conflicts lists pairs of features the Harness supports alone but not + // together. + Conflicts [][2]Feature +} + +// ValidateDeclaration rejects an incomplete or ambiguous static declaration. +func (d Declaration) ValidateDeclaration() error { + if err := d.Capabilities.ValidateDeclaration(); err != nil { + return err + } + for name, s := range map[string]CapabilitySupport{"WhitespaceOnlyText": d.WhitespaceOnlyText, "FunctionResultImageURLs": d.FunctionResultImageURLs, + "FailedFunctionResultImages": d.FailedFunctionResultImages, "MCPAllowedTools": d.MCPAllowedTools} { + if s != CapabilitySupported && s != CapabilityUnsupported { + return fmt.Errorf("declaration %s must be explicitly declared", name) + } + } + for i, origin := range d.MCPOrigins { + if (origin != "service" && origin != "environment") || slices.Contains(d.MCPOrigins[:i], origin) { + return errors.New("declaration MCPOrigins must list distinct service and environment origins") + } + } + for _, pair := range d.Conflicts { + if !pair[0].valid() || !pair[1].valid() || pair[0] == pair[1] { + return errors.New("declaration Conflicts must pair two distinct features") + } + } + return nil +} + +// Narrow returns d with the capabilities a Runtime advertises, which may +// only clear support that d declares. +func (d Declaration) Narrow(advertised AgentKindCapabilities) (Declaration, error) { + if err := advertised.ValidateDeclaration(); err != nil { + return Declaration{}, err + } + declared, narrowed := reflect.ValueOf(d.Capabilities), reflect.ValueOf(advertised) + for i := 0; i < declared.NumField(); i++ { + if narrowed.Field(i).Interface() == CapabilitySupported && declared.Field(i).Interface() != CapabilitySupported { + return Declaration{}, fmt.Errorf("capability %s widens the Harness declaration", declared.Type().Field(i).Name) + } + } + d.Capabilities = advertised + return d, nil +} + func (k SupportedAgentKind) ValidateDeclaration() error { if k.Kind == "" { return errors.New("agent kind is required") diff --git a/internal/agentdaemon/proto/execution_controls.go b/internal/agentdaemon/proto/execution_controls.go deleted file mode 100644 index 5ccd11220..000000000 --- a/internal/agentdaemon/proto/execution_controls.go +++ /dev/null @@ -1,12 +0,0 @@ -package proto - -import "errors" - -// ValidateProgrammaticToolCallingDisable applies only to explicit tool disabling. -// Omission preserves the adapter's ordinary native behavior. -func (r PromptRequestPayload) ValidateProgrammaticToolCallingDisable(supported bool) error { - if r.ExecutionControls != nil && r.ExecutionControls.DisableProgrammaticToolCalling && !supported { - return errors.New("engine does not support disabling programmatic tool calling") - } - return nil -} diff --git a/internal/agentdaemon/proto/functions.go b/internal/agentdaemon/proto/functions.go index b678c135c..c16b5ebb4 100644 --- a/internal/agentdaemon/proto/functions.go +++ b/internal/agentdaemon/proto/functions.go @@ -17,25 +17,6 @@ type FunctionTool struct { DeferLoading bool `json:"defer_loading,omitempty"` } -// ValidateToolSearch checks only the requested function discovery operation. -// Native search configuration and discovery stay inside the adapter. -func (r PromptRequestPayload) ValidateToolSearch(supported bool) error { - deferred := false - for _, tool := range r.FunctionTools { - deferred = deferred || tool.DeferLoading - } - if !r.ToolSearch && !deferred { - return nil - } - if !supported { - return errors.New("engine does not support deferred function discovery") - } - if !r.ToolSearch || !deferred { - return errors.New("qualified discovery requires tool search and deferred functions") - } - return nil -} - // FunctionCallPayload belongs to the Run identified by Envelope.ID. type FunctionCallPayload struct { CallID string `json:"call_id"` diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 9a24ad85e..7d34d204f 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -157,31 +157,27 @@ const ( DoneMetaAgentSessionType = "agent_session_type" ) -// AgentKindCapabilities describes what a daemon-side agent_kind can -// do inside one prompt session. Every field requires an explicit support -// decision, including for unavailable engines. The Runtime lifecycle -// (streaming, durable Turns and input receipts, preparation) is mandatory for -// every Harness and is not declared. +// AgentKindCapabilities is the part of a Harness's Declaration that a Runtime +// advertises in its heartbeat. The heartbeat only narrows the static +// declaration. Every field requires an explicit support decision, including +// for unavailable engines. The Runtime lifecycle (streaming, durable Turns and +// input receipts, preparation) is mandatory for every Harness and is not +// declared. type AgentKindCapabilities struct { SubagentObservations CapabilitySupport `json:"subagent_observations"` NativeSessionRecovery CapabilitySupport `json:"native_session_recovery"` - EnvironmentNone CapabilitySupport `json:"environment_none"` - LocalEnvironment CapabilitySupport `json:"local_environment"` - WorkspaceReadPreparation CapabilitySupport `json:"workspace_read_preparation"` - WorkspaceOutputExport CapabilitySupport `json:"workspace_output_export"` - ProgrammaticToolCallingDisable CapabilitySupport `json:"programmatic_tool_calling_disable"` - WebSearchControl CapabilitySupport `json:"web_search_control"` - TextVerbosity CapabilitySupport `json:"text_verbosity"` - StructuredOutput CapabilitySupport `json:"structured_output"` - ToolSearch CapabilitySupport `json:"tool_search"` - MessageImages CapabilitySupport `json:"message_images"` - FunctionResultImages CapabilitySupport `json:"function_result_images"` - SubagentControl CapabilitySupport `json:"subagent_control"` - FunctionTools CapabilitySupport `json:"function_tools"` - MCPHTTPTools CapabilitySupport `json:"mcp_http_tools"` - MCPHTTPRequired CapabilitySupport `json:"mcp_http_required"` - MCPHTTPBearerAuth CapabilitySupport `json:"mcp_http_bearer_auth"` + EnvironmentNone CapabilitySupport `json:"environment_none"` + LocalEnvironment CapabilitySupport `json:"local_environment"` + TextVerbosity CapabilitySupport `json:"text_verbosity"` + StructuredOutput CapabilitySupport `json:"structured_output"` + ToolSearch CapabilitySupport `json:"tool_search"` + MessageImages CapabilitySupport `json:"message_images"` + FunctionResultImages CapabilitySupport `json:"function_result_images"` + FunctionTools CapabilitySupport `json:"function_tools"` + MCPHTTPTools CapabilitySupport `json:"mcp_http_tools"` + MCPHTTPRequired CapabilitySupport `json:"mcp_http_required"` + MCPHTTPBearerAuth CapabilitySupport `json:"mcp_http_bearer_auth"` } // SupportedAgentKind is one daemon-advertised agent engine. Daemons diff --git a/internal/agentdaemon/proto/outbound.go b/internal/agentdaemon/proto/outbound.go index 6320cdcf2..cdafb9e00 100644 --- a/internal/agentdaemon/proto/outbound.go +++ b/internal/agentdaemon/proto/outbound.go @@ -71,11 +71,10 @@ type PromptCancelPayload struct { DeliveryID string `json:"delivery_id,omitempty"` } -// ExecutionControls requires both values when supplied; omitting the block keeps native defaults. -// Send only to a peer advertising execution_controls. +// ExecutionControls requires text_verbosity when supplied; omitting the block +// keeps native defaults. Native web search is always disabled. type ExecutionControls struct { DisableProgrammaticToolCalling bool `json:"disable_programmatic_tool_calling,omitempty"` - WebSearch string `json:"web_search"` TextVerbosity string `json:"text_verbosity"` OutputFormat *OutputFormat `json:"output_format,omitempty"` } diff --git a/internal/agentdaemon/proto/prototest/capabilities.go b/internal/agentdaemon/proto/prototest/capabilities.go index 28129ff88..abe0641d8 100644 --- a/internal/agentdaemon/proto/prototest/capabilities.go +++ b/internal/agentdaemon/proto/prototest/capabilities.go @@ -12,24 +12,19 @@ import ( // fixture is deliberately updated, so declaration validation fails on omission. func Capabilities(overrides proto.AgentKindCapabilities) proto.AgentKindCapabilities { c := proto.AgentKindCapabilities{ - SubagentObservations: proto.CapabilityUnsupported, - NativeSessionRecovery: proto.CapabilityUnsupported, - EnvironmentNone: proto.CapabilityUnsupported, - LocalEnvironment: proto.CapabilityUnsupported, - WorkspaceReadPreparation: proto.CapabilityUnsupported, - WorkspaceOutputExport: proto.CapabilityUnsupported, - ProgrammaticToolCallingDisable: proto.CapabilityUnsupported, - WebSearchControl: proto.CapabilityUnsupported, - TextVerbosity: proto.CapabilityUnsupported, - StructuredOutput: proto.CapabilityUnsupported, - ToolSearch: proto.CapabilityUnsupported, - MessageImages: proto.CapabilityUnsupported, - FunctionResultImages: proto.CapabilityUnsupported, - SubagentControl: proto.CapabilityUnsupported, - FunctionTools: proto.CapabilityUnsupported, - MCPHTTPTools: proto.CapabilityUnsupported, - MCPHTTPRequired: proto.CapabilityUnsupported, - MCPHTTPBearerAuth: proto.CapabilityUnsupported, + SubagentObservations: proto.CapabilityUnsupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + EnvironmentNone: proto.CapabilityUnsupported, + LocalEnvironment: proto.CapabilityUnsupported, + TextVerbosity: proto.CapabilityUnsupported, + StructuredOutput: proto.CapabilityUnsupported, + ToolSearch: proto.CapabilityUnsupported, + MessageImages: proto.CapabilityUnsupported, + FunctionResultImages: proto.CapabilityUnsupported, + FunctionTools: proto.CapabilityUnsupported, + MCPHTTPTools: proto.CapabilityUnsupported, + MCPHTTPRequired: proto.CapabilityUnsupported, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, } dst, src := reflect.ValueOf(&c).Elem(), reflect.ValueOf(overrides) for i := 0; i < src.NumField(); i++ { diff --git a/internal/agentdaemon/proto/prototest/model.go b/internal/agentdaemon/proto/prototest/model.go index 5e49b1008..c5f4bb569 100644 --- a/internal/agentdaemon/proto/prototest/model.go +++ b/internal/agentdaemon/proto/prototest/model.go @@ -1,6 +1,8 @@ package prototest import ( + "reflect" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" @@ -8,11 +10,20 @@ import ( // 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. +// provider's protocol and full support for a fixture Harness, so each fixture's +// advertised capabilities narrow it, and WithModel gives a request the fixture +// model and provider. func ModelConfiguration() harnessconfig.Configuration { - return harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: string(modelprovider.Responses)}}} + supported := proto.CapabilitySupported + var capabilities proto.AgentKindCapabilities + fields := reflect.ValueOf(&capabilities).Elem() + for i := 0; i < fields.NumField(); i++ { + fields.Field(i).Set(reflect.ValueOf(supported)) + } + return harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: string(modelprovider.Responses)}}, Declaration: proto.Declaration{ + Capabilities: capabilities, WhitespaceOnlyText: supported, FunctionResultImageURLs: supported, FailedFunctionResultImages: supported, + MCPAllowedTools: supported, MCPOrigins: []string{"service", "environment"}}} } func WithModel(req proto.PromptRequestPayload) proto.PromptRequestPayload { diff --git a/internal/agentdaemon/proto/selection.go b/internal/agentdaemon/proto/selection.go new file mode 100644 index 000000000..6f62e8589 --- /dev/null +++ b/internal/agentdaemon/proto/selection.go @@ -0,0 +1,246 @@ +package proto + +import ( + "encoding/json" + "slices" + "strings" +) + +// Feature names one part of a Selection that a Declaration's Conflicts pair. +type Feature string + +const ( + FeatureMultiAgent Feature = "multi_agent" + FeatureMCP Feature = "mcp" + FeatureToolSearch Feature = "tool_search" + FeatureJSONSchema Feature = "json_schema" + FeatureInstalledCapabilities Feature = "installed_capabilities" +) + +func (f Feature) valid() bool { + switch f { + case FeatureMultiAgent, FeatureMCP, FeatureToolSearch, FeatureJSONSchema, FeatureInstalledCapabilities: + return true + } + return false +} + +// Selection is the credential-free projection of one execution request that +// ValidateSelection checks against a Declaration. Core builds it from the +// frozen Session configuration and input, the Runtime from the prepared +// request. Zero fields select nothing. +type Selection struct { + // Environment is "none" or "local", or empty before an Environment is + // selected. + Environment string + InstalledCapabilities bool + MultiAgent bool + Functions bool + DeferredFunctions bool + ToolSearch bool + // OutputSchema is the schema of a json_schema output format. + OutputSchema json.RawMessage + TextVerbosity string + MCP []SelectedMCP + Messages MessageInput + FunctionResult *FunctionResultPayload +} + +// SelectedMCP is one MCP server of a Selection. An Installed server comes +// from the Environment's installed capabilities rather than agent.tools, so +// only the declared label rules and Conflicts apply to it. +type SelectedMCP struct { + Origin, Label string + AllowedTools *[]string + Required, Bearer, Installed bool +} + +// SelectionError rejects a Selection. Param is the Session configuration path +// it rejects, such as agent.tools; an input rejection has none. +type SelectionError struct { + Param, Message string +} + +func (e *SelectionError) Error() string { return e.Message } + +// WhitespaceOnlyTextMessage reports a message that a Harness without +// WhitespaceOnlyText cannot take. +const WhitespaceOnlyTextMessage = "This Session's harness does not accept a message whose text is only whitespace. Include non-whitespace text or an image, or use a harness that supports whitespace-only text." + +func (s Selection) has(f Feature) bool { + switch f { + case FeatureMultiAgent: + return s.MultiAgent + case FeatureMCP: + return len(s.MCP) > 0 + case FeatureToolSearch: + return s.ToolSearch + case FeatureJSONSchema: + return s.OutputSchema != nil + case FeatureInstalledCapabilities: + return s.InstalledCapabilities + } + return false +} + +var featureParams = map[Feature]string{FeatureMultiAgent: "agent.multi_agent", FeatureMCP: "agent.tools", FeatureToolSearch: "agent.tools", + FeatureJSONSchema: "agent.text.format", FeatureInstalledCapabilities: "environment"} + +// ValidateSelection is the only check of a Harness's declared support, and +// of the common rules that hold for every Harness. Core applies it with the +// static declaration before a Runtime is chosen and with the Runtime's +// narrowed declaration afterwards; the Runtime applies it before the +// Executor factory. +func ValidateSelection(d Declaration, s Selection) error { + c := d.Capabilities + reject := func(param, message string) error { return &SelectionError{Param: param, Message: message} } + selectedMCP := slices.ContainsFunc(s.MCP, func(server SelectedMCP) bool { return !server.Installed }) + switch { + case s.Environment == "none" && !c.EnvironmentNone.IsSupported(): + return reject("environment", "The harness does not support environment none.") + case s.Environment == "local" && !c.LocalEnvironment.IsSupported(): + return reject("environment", "The harness does not support this execution environment.") + case s.MultiAgent && !c.SubagentObservations.IsSupported(): + return reject("agent.multi_agent", "The harness does not support multi-agent execution.") + case s.MultiAgent && (s.Functions || selectedMCP): + return reject("agent.multi_agent", "Multi-agent execution does not support function or MCP tools.") + case s.Functions && !c.FunctionTools.IsSupported(): + return reject("agent.tools", "The harness does not support function tools.") + case (s.ToolSearch || s.DeferredFunctions) && !c.ToolSearch.IsSupported(): + return reject("agent.tools", "The harness does not support tool search.") + case s.ToolSearch != s.DeferredFunctions: + return reject("agent.tools", "Tool search requires deferred functions, and deferred functions require tool search.") + case s.OutputSchema != nil && !c.StructuredOutput.IsSupported(): + return reject("agent.text.format", "The harness does not support json_schema output.") + case s.TextVerbosity != "" && s.TextVerbosity != "medium" && !c.TextVerbosity.IsSupported(): + return reject("agent.text.verbosity", "The harness supports medium text verbosity only.") + } + if s.OutputSchema != nil && d.Binary64OutputSchema { + if err := ValidateBinary64Schema(s.OutputSchema); err != nil { + return reject("agent.text.format", err.Error()) + } + } + for _, server := range s.MCP { + if err := validateMCP(d, s.Environment, server); err != "" { + if server.Installed { + return reject("environment", err) + } + return reject("agent.tools", err) + } + } + for _, pair := range d.Conflicts { + if s.has(pair[0]) && s.has(pair[1]) { + return reject(featureParams[pair[0]], "The harness does not support "+string(pair[0])+" with "+string(pair[1])+".") + } + } + if s.Messages.HasImages() && !c.MessageImages.IsSupported() { + return reject("", "The harness does not support message images.") + } + if !d.WhitespaceOnlyText.IsSupported() { + for _, message := range s.Messages { + if !message.meaningful() { + return reject("", WhitespaceOnlyTextMessage) + } + } + } + if result := s.FunctionResult; result != nil { + images := MessageInput{{Content: result.Content}} + switch { + case !c.FunctionTools.IsSupported(): + return reject("", "The harness does not support function results.") + case !images.HasImages(): + case !c.FunctionResultImages.IsSupported(): + return reject("", "The harness does not support function result images.") + case !result.Success && !d.FailedFunctionResultImages.IsSupported(): + return reject("", "The harness does not support images in a failed function result.") + case !d.FunctionResultImageURLs.IsSupported() && images.ValidateInlineImages() != nil: + return reject("", "The harness supports only inline PNG or JPEG function result images.") + } + } + return nil +} + +func validateMCP(d Declaration, environment string, server SelectedMCP) string { + if slices.Contains(d.ReservedMCPLabels, server.Label) || (d.MCPLabel != nil && !d.MCPLabel.MatchString(server.Label)) { + return "The harness does not support this MCP server label." + } + if server.Installed { + return "" + } + switch { + case server.Origin == "service" && environment != "" && environment != "none": + return "Service-origin MCP requires environment none." + case server.Origin == "environment" && environment != "" && environment != "local": + return "Environment-origin MCP requires a managed or self-hosted execution environment." + case !d.Capabilities.MCPHTTPTools.IsSupported(): + return "The harness does not support MCP tools." + case !slices.Contains(d.MCPOrigins, server.Origin): + return "The harness does not support this MCP connection origin." + case server.AllowedTools != nil && !d.MCPAllowedTools.IsSupported(): + return "The harness requires allowed_tools null." + case server.Required && !d.Capabilities.MCPHTTPRequired.IsSupported(): + return "The harness does not support required MCP servers." + case server.Bearer && !d.Capabilities.MCPHTTPBearerAuth.IsSupported(): + return "The harness does not support authenticated MCP servers." + } + if server.AllowedTools != nil && d.MCPToolName != nil { + for _, name := range *server.AllowedTools { + if !d.MCPToolName.MatchString(name) { + return "The harness does not support this MCP tool name." + } + } + } + return "" +} + +// meaningful reports an image or text with a character other than +// whitespace. +func (m InputMessage) meaningful() bool { + for _, part := range m.Content { + if part.Type == "input_image" || (part.Text != nil && strings.TrimFunc(*part.Text, blankTextRune) != "") { + return true + } + } + return false +} + +// blankTextRune is the explicit union of Go unicode.IsSpace and ECMAScript +// String.prototype.trim (WhiteSpace and LineTerminator), so admission rejects +// every message a JavaScript Harness would treat as blank. +func blankTextRune(r rune) bool { + switch r { + case '\t', '\n', '\v', '\f', '\r', ' ', '\u0085', '\u00a0', '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000', '\ufeff': + return true + } + return r >= '\u2000' && r <= '\u200a' +} + +// Selection projects a request after its Environment owner configured or +// prepared it; installed MCP servers appear only after preparation. +func (r PromptRequestPayload) Selection() Selection { + s := Selection{MultiAgent: r.ObserveSubagentIdentities, Functions: len(r.FunctionTools) > 0, ToolSearch: r.ToolSearch, Messages: r.Input} + for _, tool := range r.FunctionTools { + s.DeferredFunctions = s.DeferredFunctions || tool.DeferLoading + } + if r.DisableExecutionEnvironment { + s.Environment = "none" + } + if local := r.LocalEnvironment; local != nil { + s.Environment, s.InstalledCapabilities = "local", local.Capabilities + for _, installed := range local.MCP { + s.MCP = append(s.MCP, SelectedMCP{Origin: "environment", Label: installed.Server.Name, Installed: true}) + } + } + if c := r.ExecutionControls; c != nil { + s.TextVerbosity = c.TextVerbosity + if c.OutputFormat != nil { + s.OutputSchema = c.OutputFormat.Schema + } + } + if r.MCPHTTPServers != nil { + for _, server := range *r.MCPHTTPServers { + s.MCP = append(s.MCP, SelectedMCP{Origin: server.ConnectionOrigin, Label: server.ServerLabel, AllowedTools: server.AllowedTools, Required: server.Required, Bearer: server.BearerToken != nil}) + } + } + return s +} diff --git a/services/core/internal/execution/blank_text_test.go b/internal/agentdaemon/proto/selection_test.go similarity index 69% rename from services/core/internal/execution/blank_text_test.go rename to internal/agentdaemon/proto/selection_test.go index 05587b054..be1040e6b 100644 --- a/services/core/internal/execution/blank_text_test.go +++ b/internal/agentdaemon/proto/selection_test.go @@ -1,4 +1,4 @@ -package execution +package proto import ( "encoding/json" @@ -8,15 +8,12 @@ import ( "strings" "testing" "unicode" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" ) // The Claude bridge test reads the same table, so both sides must agree on // every member and non-member. func TestBlankTextMatchesClaudeBridgeTable(t *testing.T) { - raw, err := os.ReadFile("../../../../packages/claude-sdk-adapter/tests/blank-text.json") + raw, err := os.ReadFile("../../../packages/claude-sdk-adapter/tests/blank-text.json") if err != nil { t.Fatal(err) } @@ -50,20 +47,24 @@ func TestBlankTextMatchesClaudeBridgeTable(t *testing.T) { t.Fatalf("U+%04X is Go whitespace but not in the table", r) } } - claude, _ := (engine.Catalog{}).Lookup("claude_sdk") + blank := Declaration{WhitespaceOnlyText: CapabilityUnsupported} + rejects := func(text string) bool { + var err *SelectionError + return errors.As(ValidateSelection(blank, Selection{Messages: TextInput(text)}), &err) && err.Message == WhitespaceOnlyTextMessage + } var all strings.Builder for r := range members { all.WriteRune(r) - if !errors.Is(validateMessageTextProfile(claude, proto.TextInput(string(r))), ErrWhitespaceOnlyText) { + if !rejects(string(r)) { t.Fatalf("U+%04X admitted", r) } } - if !errors.Is(validateMessageTextProfile(claude, proto.TextInput(all.String())), ErrWhitespaceOnlyText) { + if !rejects(all.String()) { t.Fatal("all members admitted") } for r := range nonMembers { - if err := validateMessageTextProfile(claude, proto.TextInput(" "+string(r)+"\ufeff")); err != nil { - t.Fatalf("U+%04X rejected: %v", r, err) + if rejects(" " + string(r) + "\ufeff") { + t.Fatalf("U+%04X rejected", r) } } } diff --git a/internal/harnessconfig/builtin/catalog.json b/internal/harnessconfig/builtin/catalog.json index 9328ac7d7..12f547cc2 100644 --- a/internal/harnessconfig/builtin/catalog.json +++ b/internal/harnessconfig/builtin/catalog.json @@ -1,5 +1,5 @@ [ - {"kind": "claude_sdk", "label": "Claude Code", "configuration": "claudesdk", "profile": "claudeProfile"}, - {"kind": "codex", "label": "Codex", "configuration": "codex", "profile": "codexProfile"}, - {"kind": "mcode", "label": "MiniMax Code", "configuration": "mcode", "profile": "mcodeProfile"} + {"kind": "claude_sdk", "label": "Claude Code", "configuration": "claudesdk"}, + {"kind": "codex", "label": "Codex", "configuration": "codex"}, + {"kind": "mcode", "label": "MiniMax Code", "configuration": "mcode"} ] diff --git a/internal/harnessconfig/builtin/selection_test.go b/internal/harnessconfig/builtin/selection_test.go new file mode 100644 index 000000000..13c213a8f --- /dev/null +++ b/internal/harnessconfig/builtin/selection_test.go @@ -0,0 +1,104 @@ +package builtin + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" +) + +// Each declared rule has a selection that one Harness admits and another +// rejects, or that a Runtime narrowing every capability rejects. Core and the +// Runtime admit selections through this one validator and these declarations. +func TestSelectionsAgainstEachDeclaration(t *testing.T) { + text, blank, inline, remote := "inspect", " \t", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=", "https://example.test/image.png" + image := func(url string) []proto.InputContent { + return []proto.InputContent{{Type: "input_image", ImageURL: &url}} + } + result := func(success bool, content []proto.InputContent) *proto.FunctionResultPayload { + return &proto.FunctionResultPayload{CallID: "call", Success: success, Content: content} + } + tools, wildcard := []string{"search"}, []string{"*"} + service := func(label string) []proto.SelectedMCP { return []proto.SelectedMCP{{Origin: "service", Label: label}} } + environment := func(server proto.SelectedMCP) []proto.SelectedMCP { + server.Origin = "environment" + if server.Label == "" { + server.Label = "docs" + } + return []proto.SelectedMCP{server} + } + schema, lossy := json.RawMessage(`{"type":"object","properties":{}}`), json.RawMessage(`{"type":"object","const":9007199254740993}`) + search := proto.Selection{Environment: "none", Functions: true, DeferredFunctions: true, ToolSearch: true} + with := func(s proto.Selection, change func(*proto.Selection)) proto.Selection { change(&s); return s } + const tool, format, multi = "agent.tools", "agent.text.format", "agent.multi_agent" + var none proto.AgentKindCapabilities + for i, fields := 0, reflect.ValueOf(&none).Elem(); i < fields.NumField(); i++ { + fields.Field(i).Set(reflect.ValueOf(proto.CapabilityUnsupported)) + } + for _, c := range []struct { + name string + selection proto.Selection + rejects map[string]string // Harness to the rejection's param + capability bool // a Runtime that advertises no capability rejects it + }{ + {"environment none", proto.Selection{Environment: "none"}, nil, true}, + {"local environment", proto.Selection{Environment: "local"}, nil, true}, + {"installed capabilities", proto.Selection{Environment: "local", InstalledCapabilities: true}, nil, true}, + {"multi-agent", proto.Selection{Environment: "none", MultiAgent: true}, nil, true}, + {"function tools", proto.Selection{Environment: "none", Functions: true}, map[string]string{"mcode": tool}, true}, + {"tool search", search, map[string]string{"codex": tool, "mcode": tool}, true}, + {"json_schema output", proto.Selection{Environment: "none", OutputSchema: schema}, map[string]string{"codex": format, "mcode": format}, true}, + {"lossy json_schema output", proto.Selection{Environment: "none", OutputSchema: lossy}, map[string]string{"codex": format, "claude_sdk": format, "mcode": format}, false}, + {"medium text verbosity", proto.Selection{Environment: "none", TextVerbosity: "medium"}, nil, false}, + {"high text verbosity", proto.Selection{Environment: "none", TextVerbosity: "high"}, map[string]string{"claude_sdk": "agent.text.verbosity", "mcode": "agent.text.verbosity"}, true}, + {"text message", proto.Selection{Messages: proto.TextInput(text)}, nil, false}, + {"whitespace-only message", proto.Selection{Messages: proto.TextInput(blank)}, map[string]string{"claude_sdk": "", "mcode": ""}, false}, + {"message image", proto.Selection{Messages: proto.MessageInput{{Content: image(inline)}}}, map[string]string{"mcode": ""}, true}, + {"function result", proto.Selection{FunctionResult: result(true, []proto.InputContent{{Type: "input_text", Text: &text}})}, map[string]string{"mcode": ""}, true}, + {"function result image", proto.Selection{FunctionResult: result(true, image(inline))}, map[string]string{"mcode": ""}, true}, + {"function result image URL", proto.Selection{FunctionResult: result(true, image(remote))}, map[string]string{"claude_sdk": "", "mcode": ""}, false}, + {"failed function result image", proto.Selection{FunctionResult: result(false, image(inline))}, map[string]string{"claude_sdk": "", "mcode": ""}, false}, + {"service MCP", proto.Selection{Environment: "none", MCP: service("docs")}, map[string]string{"mcode": tool}, true}, + {"environment MCP", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{})}, nil, true}, + {"MCP allowlist", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{AllowedTools: &tools})}, map[string]string{"mcode": tool}, false}, + {"required MCP", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Required: true})}, map[string]string{"mcode": tool}, true}, + {"authenticated MCP", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Bearer: true})}, nil, true}, + {"MCP label codex_apps", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Label: "codex_apps"})}, map[string]string{"codex": tool}, false}, + {"MCP label functions", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Label: "functions"})}, map[string]string{"claude_sdk": tool}, false}, + {"MCP label oac_workspace", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Label: "oac_workspace"})}, map[string]string{"mcode": tool}, false}, + {"MCP label outside the pattern", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Label: "docs.v1"})}, map[string]string{"claude_sdk": tool}, false}, + {"MCP tool name outside the pattern", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{AllowedTools: &wildcard})}, map[string]string{"claude_sdk": tool, "mcode": tool}, false}, + {"installed MCP label", proto.Selection{Environment: "local", MCP: environment(proto.SelectedMCP{Label: "functions", Installed: true})}, map[string]string{"claude_sdk": "environment"}, false}, + {"multi-agent with installed MCP", proto.Selection{Environment: "local", MultiAgent: true, MCP: environment(proto.SelectedMCP{Installed: true})}, map[string]string{"claude_sdk": multi}, false}, + {"json_schema with multi-agent", proto.Selection{Environment: "none", OutputSchema: schema, MultiAgent: true}, map[string]string{"codex": format, "claude_sdk": format, "mcode": format}, false}, + {"json_schema with MCP", proto.Selection{Environment: "none", OutputSchema: schema, MCP: service("docs")}, map[string]string{"codex": format, "claude_sdk": format, "mcode": format}, false}, + {"json_schema with installed capabilities", proto.Selection{Environment: "local", InstalledCapabilities: true, OutputSchema: schema}, map[string]string{"codex": format, "claude_sdk": format, "mcode": format}, false}, + {"json_schema with tool search", with(search, func(s *proto.Selection) { s.OutputSchema = schema }), map[string]string{"codex": tool, "claude_sdk": format, "mcode": tool}, false}, + {"tool search with MCP", with(search, func(s *proto.Selection) { s.MCP = service("docs") }), map[string]string{"codex": tool, "claude_sdk": tool, "mcode": tool}, false}, + {"tool search with installed capabilities", with(search, func(s *proto.Selection) { s.Environment, s.InstalledCapabilities = "local", true }), map[string]string{"codex": tool, "claude_sdk": tool, "mcode": tool}, false}, + } { + t.Run(c.name, func(t *testing.T) { + for _, kind := range Kinds() { + declaration := Configuration(kind).Declaration + err := proto.ValidateSelection(declaration, c.selection) + param, rejected := c.rejects[kind] + var selectionErr *proto.SelectionError + if rejected != (err != nil) || rejected && (!errors.As(err, &selectionErr) || selectionErr.Param != param) { + t.Errorf("%s: %v, want rejected=%v with param %q", kind, err, rejected, param) + } + if !c.capability || rejected { + continue + } + narrowed, err := declaration.Narrow(none) + if err != nil { + t.Fatal(err) + } + if proto.ValidateSelection(narrowed, c.selection) == nil { + t.Errorf("%s: a Runtime without the capability admitted it", kind) + } + } + }) + } +} diff --git a/internal/harnessconfig/claudesdk/configuration.go b/internal/harnessconfig/claudesdk/configuration.go index 4b3608162..cbe4aa464 100644 --- a/internal/harnessconfig/claudesdk/configuration.go +++ b/internal/harnessconfig/claudesdk/configuration.go @@ -1,10 +1,39 @@ // Package claudesdk owns the qualified Claude SDK provider declaration. package claudesdk -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" +import ( + "regexp" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" +) func Configuration() harnessconfig.Configuration { + s, u := proto.CapabilitySupported, proto.CapabilityUnsupported return harnessconfig.Configuration{ValidateNativeConfig: validateNativeConfig, Providers: []harnessconfig.Provider{ {Protocol: "anthropic"}, + }, Declaration: proto.Declaration{ + Capabilities: proto.AgentKindCapabilities{ + SubagentObservations: s, NativeSessionRecovery: s, EnvironmentNone: s, LocalEnvironment: s, + TextVerbosity: u, StructuredOutput: s, ToolSearch: s, MessageImages: s, FunctionResultImages: s, + FunctionTools: s, MCPHTTPTools: s, MCPHTTPRequired: s, MCPHTTPBearerAuth: s, + }, + // The bridge and Anthropic-compatible providers reject text without a + // non-whitespace character, and tool results carry only inline images. + WhitespaceOnlyText: u, FunctionResultImageURLs: u, FailedFunctionResultImages: u, MCPAllowedTools: s, + MCPOrigins: []string{"service", "environment"}, + ReservedMCPLabels: []string{"functions"}, + // The SDK names MCP tools mcp__