diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b9300ef5..7a079a2d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ This guide owns how to work in the repository: documentation ownership, the repo | Built-in Harness identifiers, configuration/profile bindings and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) | | Effective MCP bindings and credential authority | [Environment MCP](contracts/agents-api/environments.md#skills-plugins-and-environment-mcp) and `apps/daemon/internal/agent/mcp_binding.go` | | Harness qualification and acceptance | [Harness integration](contracts/agents-api/harnesses.md) | +| Harness service qualification declarations and registration | [Explicit service qualification](contracts/agents-api/harness-onboarding.md#explicit-service-qualification) and `services/core/internal/engine/profile.go` | | Harness selection and Agent defaults | [Harness selection](contracts/agents-api/harness-selection.md) | | Provider selection, sandbox deployment and E2B setup | [Sandbox deployment](contracts/agents-api/sandbox-deployment.md) | | Hosted sandbox nodes | [Nodes guide](docs/getting-started/nodes.md) and [sandbox deployment contract](contracts/agents-api/sandbox-deployment.md) | diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 901e7f777..81bf61861 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -319,6 +319,21 @@ actual Runtime capabilities remain separate checks; Runtime advertisements alone never enable public operations. 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/store tests 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.WithExecutionPolicy` and `Dispatcher.Policy`. The zero value uses diff --git a/services/core/internal/engine/claude.go b/services/core/internal/engine/claude.go index 9eb99c198..28529f66d 100644 --- a/services/core/internal/engine/claude.go +++ b/services/core/internal/engine/claude.go @@ -13,14 +13,21 @@ import ( // providers reject text without non-whitespace characters. func claudeProfile() Profile { return Profile{ - ProgrammaticToolCallingDisable: true, + ProgrammaticToolCallingDisable: proto.CapabilitySupported, MCPOrigins: []string{"service", "environment"}, - StructuredOutput: true, - ToolSearch: true, - MessageImages: true, - Placements: []string{"none", "openai_hosted", "self_hosted"}, MCPBearer: true, - ValidateConfiguration: validateClaudeConfiguration, - ValidateTools: validateClaudeTools, + Placements: []string{"none", "openai_hosted", "self_hosted"}, + StructuredOutput: proto.CapabilitySupported, + ToolSearch: proto.CapabilitySupported, + MessageImages: proto.CapabilitySupported, + WhitespaceOnlyText: proto.CapabilityUnsupported, + WebSearchControl: proto.CapabilityUnsupported, + TextVerbosity: proto.CapabilityUnsupported, + MCPBearer: proto.CapabilitySupported, + ConfigurationValidation: AdditionalValidation, + ToolsValidation: AdditionalValidation, + FunctionResultValidation: AdditionalValidation, + ValidateConfiguration: validateClaudeConfiguration, + ValidateTools: validateClaudeTools, ValidateFunctionResult: func(_ string, result proto.FunctionResultPayload) error { for _, part := range result.Content { if part.Type == "input_image" && !result.Success { diff --git a/services/core/internal/engine/codex.go b/services/core/internal/engine/codex.go index 8ce736a37..e59882009 100644 --- a/services/core/internal/engine/codex.go +++ b/services/core/internal/engine/codex.go @@ -2,16 +2,24 @@ package engine import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func codexProfile() Profile { return Profile{ - ProgrammaticToolCallingDisable: true, + ProgrammaticToolCallingDisable: proto.CapabilitySupported, Placements: []string{"none", "self_hosted", "openai_hosted"}, MCPOrigins: []string{"service", "environment"}, - MessageImages: true, - WhitespaceOnlyText: true, - WebSearchControl: true, TextVerbosity: true, MCPBearer: true, + MessageImages: proto.CapabilitySupported, + WhitespaceOnlyText: proto.CapabilitySupported, + WebSearchControl: proto.CapabilitySupported, + TextVerbosity: proto.CapabilitySupported, + MCPBearer: proto.CapabilitySupported, + StructuredOutput: proto.CapabilityUnsupported, + ToolSearch: proto.CapabilityUnsupported, + ConfigurationValidation: AdditionalValidation, + ToolsValidation: CommonValidationOnly, + FunctionResultValidation: CommonValidationOnly, ValidateConfiguration: func(agent v1.Agent, _ *v1.Environment, _ bool) error { return rejectSubagentTools(agent, "function", "mcp") }, diff --git a/services/core/internal/engine/declaration_test.go b/services/core/internal/engine/declaration_test.go new file mode 100644 index 000000000..33c3a8362 --- /dev/null +++ b/services/core/internal/engine/declaration_test.go @@ -0,0 +1,139 @@ +package engine_test + +import ( + "errors" + "reflect" + "strconv" + "strings" + "testing" + + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" +) + +func requireDeclarationRejection(t *testing.T, p engine.Profile) { + t.Helper() + if err := p.ValidateDeclaration(); !errors.Is(err, engine.ErrInvalidDeclaration) { + t.Fatalf("expected declaration error, got %v", err) + } + defer func() { + err, ok := recover().(error) + if !ok || !errors.Is(err, engine.ErrInvalidDeclaration) { + t.Fatalf("catalog accepted invalid declaration: %v", err) + } + }() + engine.NewCatalog(map[string]engine.Profile{"fixture": p}) +} + +func TestEveryProfileFieldRequiresAnExplicitDecision(t *testing.T) { + // Claude exercises all three additional validators. Zeroing any field, even + // a future one, must fail before the profile enters an immutable catalog. + original, _ := (engine.Catalog{}).Lookup("claude_sdk") + typ := reflect.TypeOf(original) + for i := 0; i < typ.NumField(); i++ { + t.Run(typ.Field(i).Name, func(t *testing.T) { + p := original + reflect.ValueOf(&p).Elem().Field(i).SetZero() + requireDeclarationRejection(t, p) + }) + } +} + +func TestEveryServiceCapabilityRejectsInvalidValues(t *testing.T) { + for _, kind := range (engine.Catalog{}).Kinds() { + original, _ := (engine.Catalog{}).Lookup(kind) + if err := original.ValidateDeclaration(); err != nil { + t.Fatal(kind, err) + } + typ := reflect.TypeOf(original) + for i := 0; i < typ.NumField(); i++ { + if typ.Field(i).Type != reflect.TypeOf(proto.CapabilitySupport(0)) { + continue + } + for _, invalid := range []proto.CapabilitySupport{proto.CapabilityUnspecified, 3, 255} { + t.Run(kind+"/"+typ.Field(i).Name+"/"+strconv.Itoa(int(invalid)), func(t *testing.T) { + p := original + reflect.ValueOf(&p).Elem().Field(i).Set(reflect.ValueOf(invalid)) + requireDeclarationRejection(t, p) + }) + } + } + } +} + +func TestValidationPoliciesMustMatchCallbacks(t *testing.T) { + original, _ := (engine.Catalog{}).Lookup("claude_sdk") + for _, field := range []string{"ConfigurationValidation", "ToolsValidation", "FunctionResultValidation"} { + for _, invalid := range []engine.ValidationPolicy{engine.ValidationUnspecified, 3, engine.CommonValidationOnly} { + t.Run(field+"/"+strconv.Itoa(int(invalid)), func(t *testing.T) { + p := original + reflect.ValueOf(&p).Elem().FieldByName(field).Set(reflect.ValueOf(invalid)) + requireDeclarationRejection(t, p) + }) + } + t.Run(field+"/missing callback", func(t *testing.T) { + p := enginetest.Profile(nil) + reflect.ValueOf(&p).Elem().FieldByName(field).Set(reflect.ValueOf(engine.AdditionalValidation)) + requireDeclarationRejection(t, p) + }) + } + // Common validation is sufficient when explicitly chosen, with no pretend + // successful callbacks. An empty MCP list is an explicit refusal of all origins. + if err := enginetest.Profile(nil).ValidateDeclaration(); err != nil { + t.Fatal(err) + } +} + +func TestCatalogRejectsInvalidCombinationsBeforeCallbacks(t *testing.T) { + cases := map[string]func(*engine.Profile){ + "empty placements": func(p *engine.Profile) { p.Placements = []string{} }, + "unknown placement": func(p *engine.Profile) { p.Placements = []string{"private-fixture-value"} }, + "duplicate placement": func(p *engine.Profile) { p.Placements = []string{"none", "none"} }, + "unknown origin": func(p *engine.Profile) { p.MCPOrigins = []string{"private-fixture-value"} }, + "duplicate origin": func(p *engine.Profile) { p.MCPOrigins = []string{"service", "service"} }, + "service without placement": func(p *engine.Profile) { p.Placements = []string{"self_hosted"}; p.MCPOrigins = []string{"service"} }, + "environment without placement": func(p *engine.Profile) { p.MCPOrigins = []string{"environment"} }, + "bearer without MCP": func(p *engine.Profile) { p.MCPBearer = proto.CapabilitySupported }, + } + for name, change := range cases { + t.Run(name, func(t *testing.T) { + p := enginetest.Profile(change) + p.ConfigurationValidation = engine.AdditionalValidation + p.ValidateConfiguration = func(v1.Agent, *v1.Environment, bool) error { + t.Fatal("registration invoked request validator") + return nil + } + if err := p.ValidateDeclaration(); err != nil && strings.Contains(err.Error(), "private-fixture-value") { + t.Fatal("declaration error disclosed input") + } + requireDeclarationRejection(t, p) + }) + } +} + +func TestCatalogRejectsEmptyProfileAndKind(t *testing.T) { + requireDeclarationRejection(t, engine.Profile{}) + for _, kind := range []string{"", " ", " fixture"} { + t.Run("kind/"+kind, func(t *testing.T) { + defer func() { + if err, ok := recover().(error); !ok || !errors.Is(err, engine.ErrInvalidDeclaration) { + t.Fatal("invalid kind accepted", err) + } + }() + engine.NewCatalog(map[string]engine.Profile{kind: enginetest.Profile(nil)}) + }) + } + for _, profiles := range []map[string]engine.Profile{nil, {}} { + c := engine.NewCatalog(profiles) + if len(c.Kinds()) != 0 { + t.Fatal("empty catalog inherited builtins") + } + for _, kind := range (engine.Catalog{}).Kinds() { + if _, ok := c.Lookup(kind); ok { + t.Fatal("empty catalog authorized", kind) + } + } + } +} diff --git a/services/core/internal/engine/enginetest/profile.go b/services/core/internal/engine/enginetest/profile.go new file mode 100644 index 000000000..2984c5e93 --- /dev/null +++ b/services/core/internal/engine/enginetest/profile.go @@ -0,0 +1,32 @@ +// Package enginetest supplies explicit declarations for admission fixtures. +// Production profiles must decide every field in their own constructors. +package enginetest + +import ( + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" +) + +// Profile qualifies only common text admission by default. The literal is +// intentionally exhaustive; new fields stay unspecified until decided here. +func Profile(change func(*engine.Profile)) engine.Profile { + p := engine.Profile{ + ProgrammaticToolCallingDisable: proto.CapabilityUnsupported, + WebSearchControl: proto.CapabilityUnsupported, + TextVerbosity: proto.CapabilityUnsupported, + MCPBearer: proto.CapabilityUnsupported, + StructuredOutput: proto.CapabilityUnsupported, + ToolSearch: proto.CapabilityUnsupported, + MessageImages: proto.CapabilityUnsupported, + WhitespaceOnlyText: proto.CapabilityUnsupported, + Placements: []string{"none"}, + MCPOrigins: []string{}, + ConfigurationValidation: engine.CommonValidationOnly, + ToolsValidation: engine.CommonValidationOnly, + FunctionResultValidation: engine.CommonValidationOnly, + } + if change != nil { + change(&p) + } + return p +} diff --git a/services/core/internal/engine/mcode.go b/services/core/internal/engine/mcode.go index 6b6e851a7..bbd1aeca6 100644 --- a/services/core/internal/engine/mcode.go +++ b/services/core/internal/engine/mcode.go @@ -11,20 +11,34 @@ import ( // WhitespaceOnlyText stays unqualified: the native runtime refuses such prompts // with "Local message content or attachments are required.". func mcodeProfile() Profile { - return Profile{MCPOrigins: []string{"environment"}, MCPBearer: true, ProgrammaticToolCallingDisable: true, Placements: []string{"none", "openai_hosted", "self_hosted"}, ValidateConfiguration: func(a v1.Agent, e *v1.Environment, daemon bool) error { - if e == nil || (e.Type != "none" && e.Type != "openai_hosted" && e.Type != "self_hosted") || daemon || strings.TrimSpace(a.Model) == "" || a.Reasoning.Effort != nil || a.Reasoning.Summary != nil || (a.ServiceTier != "" && a.ServiceTier != "auto") || (a.Text.Format.Type != "" && a.Text.Format.Type != "text") || (a.Text.Verbosity != "" && a.Text.Verbosity != "medium") { - return ErrInvalidInput - } - return rejectSubagentTools(a, "mcp") - }, ValidateTools: func(_ *v1.Environment, _ bool, functions []proto.FunctionTool, mcp []proto.MCPHTTPServer) error { - if len(functions) > 0 { - return errors.New("The configured engine does not support public functions.") - } - for _, server := range mcp { - if server.ServerLabel == "oac_workspace" || server.AllowedTools != nil || server.Required { - return errors.New("The configured engine requires an unreserved MCP label, allowed_tools=null and required=false.") + return Profile{ + MCPOrigins: []string{"environment"}, + Placements: []string{"none", "openai_hosted", "self_hosted"}, + MCPBearer: proto.CapabilitySupported, + ProgrammaticToolCallingDisable: proto.CapabilitySupported, + StructuredOutput: proto.CapabilityUnsupported, + ToolSearch: proto.CapabilityUnsupported, + MessageImages: proto.CapabilityUnsupported, + WhitespaceOnlyText: proto.CapabilityUnsupported, + WebSearchControl: proto.CapabilityUnsupported, + TextVerbosity: proto.CapabilityUnsupported, + ConfigurationValidation: AdditionalValidation, + ToolsValidation: AdditionalValidation, + FunctionResultValidation: CommonValidationOnly, + ValidateConfiguration: func(a v1.Agent, e *v1.Environment, daemon bool) error { + if e == nil || (e.Type != "none" && e.Type != "openai_hosted" && e.Type != "self_hosted") || daemon || strings.TrimSpace(a.Model) == "" || a.Reasoning.Effort != nil || a.Reasoning.Summary != nil || (a.ServiceTier != "" && a.ServiceTier != "auto") || (a.Text.Format.Type != "" && a.Text.Format.Type != "text") || (a.Text.Verbosity != "" && a.Text.Verbosity != "medium") { + return ErrInvalidInput } - } - return nil - }} + return rejectSubagentTools(a, "mcp") + }, ValidateTools: func(_ *v1.Environment, _ bool, functions []proto.FunctionTool, mcp []proto.MCPHTTPServer) error { + if len(functions) > 0 { + return errors.New("The configured engine does not support public functions.") + } + for _, server := range mcp { + if server.ServerLabel == "oac_workspace" || server.AllowedTools != nil || server.Required { + return errors.New("The configured engine requires an unreserved MCP label, allowed_tools=null and required=false.") + } + } + return nil + }} } diff --git a/services/core/internal/engine/mcp_test.go b/services/core/internal/engine/mcp_test.go index e5e0b9801..0976107d1 100644 --- a/services/core/internal/engine/mcp_test.go +++ b/services/core/internal/engine/mcp_test.go @@ -1,14 +1,17 @@ -package engine +package engine_test import ( + "testing" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "testing" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" ) func TestMCPOriginQualification(t *testing.T) { for _, kind := range []string{"codex", "claude_sdk", "mcode"} { - profile, ok := (Catalog{}).Lookup(kind) + profile, ok := (engine.Catalog{}).Lookup(kind) if !ok { t.Fatal(kind) } @@ -27,7 +30,7 @@ func TestMCPOriginQualification(t *testing.T) { } } func TestMiniMaxMCPPoliciesRejectInsteadOfDropping(t *testing.T) { - p, _ := (Catalog{}).Lookup("mcode") + p, _ := (engine.Catalog{}).Lookup("mcode") empty := []string{} named := []string{"proof"} for _, allowed := range []*[]string{nil, &empty, &named} { @@ -41,8 +44,11 @@ func TestMiniMaxMCPPoliciesRejectInsteadOfDropping(t *testing.T) { } } func TestMCPOriginCatalogIsImmutable(t *testing.T) { - p := Profile{MCPOrigins: []string{"environment"}} - c := NewCatalog(map[string]Profile{"fixture": p}) + p := enginetest.Profile(func(p *engine.Profile) { + p.MCPOrigins = []string{"environment"} + p.Placements = []string{"self_hosted"} + }) + c := engine.NewCatalog(map[string]engine.Profile{"fixture": p}) p.MCPOrigins[0] = "service" first, _ := c.Lookup("fixture") if first.MCPOrigins[0] != "environment" { diff --git a/services/core/internal/engine/profile.go b/services/core/internal/engine/profile.go index aa678ef22..dce064762 100644 --- a/services/core/internal/engine/profile.go +++ b/services/core/internal/engine/profile.go @@ -2,8 +2,11 @@ package engine import ( "errors" + "fmt" "maps" + "reflect" "slices" + "strings" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -13,20 +16,23 @@ var ErrInvalidInput = errors.New("invalid engine configuration") // Profile records qualified public behavior, independently of Runtime advertisements. type Profile struct { - ProgrammaticToolCallingDisable bool + ProgrammaticToolCallingDisable proto.CapabilitySupport Placements []string MCPOrigins []string - WebSearchControl, TextVerbosity, MCPBearer bool - StructuredOutput bool - ToolSearch bool - MessageImages bool + WebSearchControl, TextVerbosity, MCPBearer proto.CapabilitySupport + StructuredOutput proto.CapabilitySupport + ToolSearch proto.CapabilitySupport + MessageImages proto.CapabilitySupport + ConfigurationValidation ValidationPolicy + ToolsValidation ValidationPolicy + FunctionResultValidation ValidationPolicy ValidateConfiguration func(agent v1.Agent, environment *v1.Environment, hasDaemon bool) error ValidateTools func(environment *v1.Environment, hasDaemon bool, functions []proto.FunctionTool, mcp []proto.MCPHTTPServer) error ValidateFunctionResult func(placement string, result proto.FunctionResultPayload) error // WhitespaceOnlyText qualifies messages without an image or non-whitespace // text. Unqualified harnesses reject them at admission; Core never trims or // pads model input to fit a harness. - WhitespaceOnlyText bool + WhitespaceOnlyText proto.CapabilitySupport } func (p Profile) Accepts(placement string) bool { @@ -39,9 +45,17 @@ type Catalog struct { profiles map[string]Profile } +// NewCatalog rejects invalid static registrations with ErrInvalidDeclaration. +// Nil and empty maps both explicitly authorize no engines. func NewCatalog(profiles map[string]Profile) Catalog { c := Catalog{profiles: make(map[string]Profile, len(profiles))} for kind, profile := range profiles { + if kind == "" || kind != strings.TrimSpace(kind) { + panic(ErrInvalidDeclaration) + } + if err := profile.ValidateDeclaration(); err != nil { + panic(err) + } profile.Placements = slices.Clone(profile.Placements) profile.MCPOrigins = slices.Clone(profile.MCPOrigins) c.profiles[kind] = profile @@ -68,3 +82,84 @@ func (c Catalog) Kinds() []string { slices.Sort(kinds) return kinds } + +var ErrInvalidDeclaration = errors.New("invalid engine profile declaration") + +// ValidationPolicy distinguishes an explicit reliance on common validation from +// additional pure Harness restrictions. Neither is an execution implementation. +type ValidationPolicy uint8 + +const ( + ValidationUnspecified ValidationPolicy = iota + CommonValidationOnly + AdditionalValidation +) + +// ValidateDeclaration runs before a Catalog can authorize any operation. Every +// future field must be classified here; no field receives an inferred default. +func (p Profile) ValidateDeclaration() error { + v := reflect.ValueOf(p) + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i).Name + if support, ok := v.Field(i).Interface().(proto.CapabilitySupport); ok { + if support != proto.CapabilitySupported && support != proto.CapabilityUnsupported { + return declarationError(field) + } + continue + } + switch field { + case "Placements", "MCPOrigins": + if v.Field(i).IsNil() { + return declarationError(field) + } + case "ConfigurationValidation", "ToolsValidation", "FunctionResultValidation": + policy, ok := v.Field(i).Interface().(ValidationPolicy) + if !ok || (policy != CommonValidationOnly && policy != AdditionalValidation) { + return declarationError(field) + } + case "ValidateConfiguration", "ValidateTools", "ValidateFunctionResult": + // Paired with the explicit policy below, including legitimate nil. + default: + return declarationError(field) + } + } + for _, strategy := range []struct { + field string + policy ValidationPolicy + custom bool + }{ + {"ConfigurationValidation", p.ConfigurationValidation, p.ValidateConfiguration != nil}, + {"ToolsValidation", p.ToolsValidation, p.ValidateTools != nil}, + {"FunctionResultValidation", p.FunctionResultValidation, p.ValidateFunctionResult != nil}, + } { + if (strategy.policy == AdditionalValidation) != strategy.custom { + return declarationError(strategy.field) + } + } + if len(p.Placements) == 0 || !validChoices(p.Placements, "none", "openai_hosted", "self_hosted") { + return declarationError("Placements") + } + if !validChoices(p.MCPOrigins, "service", "environment") || + (slices.Contains(p.MCPOrigins, "service") && !p.Accepts("none")) || + (slices.Contains(p.MCPOrigins, "environment") && !p.Accepts("openai_hosted") && !p.Accepts("self_hosted")) || + (p.MCPBearer.IsSupported() && len(p.MCPOrigins) == 0) { + return declarationError("MCPOrigins") + } + return nil +} + +func declarationError(field string) error { + // Only authored field names are exposed, never declaration values. + return fmt.Errorf("%w: %s", ErrInvalidDeclaration, field) +} + +func validChoices(values []string, allowed ...string) bool { + seen := make(map[string]bool, len(values)) + for _, value := range values { + if !slices.Contains(allowed, value) || seen[value] { + return false + } + seen[value] = true + } + return true +} diff --git a/services/core/internal/engine/profile_test.go b/services/core/internal/engine/profile_test.go index d95ea84d6..8cc844ca3 100644 --- a/services/core/internal/engine/profile_test.go +++ b/services/core/internal/engine/profile_test.go @@ -1,37 +1,41 @@ -package engine +package engine_test import ( "reflect" "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" ) func TestCatalogOwnsQualification(t *testing.T) { placements := []string{"none"} - profiles := map[string]Profile{"fixture": {Placements: placements}} - catalog := NewCatalog(profiles) + profiles := map[string]engine.Profile{"fixture": enginetest.Profile(func(p *engine.Profile) { p.Placements = placements })} + catalog := engine.NewCatalog(profiles) placements[0] = "openai_hosted" - profiles["fixture"] = Profile{MCPBearer: true} - profiles["unqualified"] = Profile{Placements: []string{"none"}} + profiles["fixture"] = engine.Profile{MCPBearer: proto.CapabilitySupported} + profiles["unqualified"] = enginetest.Profile(nil) profile, ok := catalog.Lookup("fixture") - if !ok || !profile.Accepts("none") || profile.Accepts("openai_hosted") || profile.MCPBearer { + if !ok || !profile.Accepts("none") || profile.Accepts("openai_hosted") || profile.MCPBearer.IsSupported() { t.Fatal("caller changed catalog qualification") } profile.Placements[0] = "self_hosted" - profile.MCPBearer = true + profile.MCPBearer = proto.CapabilitySupported again, _ := catalog.Lookup("fixture") - if !again.Accepts("none") || again.MCPBearer { + if !again.Accepts("none") || again.MCPBearer.IsSupported() { t.Fatal("lookup exposed mutable qualification") } if _, ok := catalog.Lookup("unqualified"); ok { t.Fatal("caller registered an engine after catalog construction") } - if _, ok := (Catalog{}).Lookup("fixture"); ok { + if _, ok := (engine.Catalog{}).Lookup("fixture"); ok { t.Fatal("fixture widened the service catalog") } } func TestCatalogKindsAreStableAndDefensive(t *testing.T) { - catalog := NewCatalog(map[string]Profile{"zeta": {}, "alpha": {}}) + catalog := engine.NewCatalog(map[string]engine.Profile{"zeta": enginetest.Profile(nil), "alpha": enginetest.Profile(nil)}) got := catalog.Kinds() if !reflect.DeepEqual(got, []string{"alpha", "zeta"}) { t.Fatalf("Kinds = %#v", got) @@ -43,12 +47,12 @@ func TestCatalogKindsAreStableAndDefensive(t *testing.T) { } func TestExplicitCatalogDoesNotInheritBuiltins(t *testing.T) { - for _, catalog := range []Catalog{NewCatalog(nil), NewCatalog(map[string]Profile{"fixture": {Placements: []string{"none"}}})} { + for _, catalog := range []engine.Catalog{engine.NewCatalog(nil), engine.NewCatalog(map[string]engine.Profile{"fixture": enginetest.Profile(nil)})} { if _, ok := catalog.Lookup("codex"); ok { t.Fatal("explicit catalog inherited built-in qualification") } } - if _, ok := (Catalog{}).Lookup("codex"); !ok { + if _, ok := (engine.Catalog{}).Lookup("codex"); !ok { t.Fatal("zero-value catalog lost built-in qualification") } } diff --git a/services/core/internal/execution/disabled_tools_test.go b/services/core/internal/execution/disabled_tools_test.go index 05c5adedf..7bf0f0b06 100644 --- a/services/core/internal/execution/disabled_tools_test.go +++ b/services/core/internal/execution/disabled_tools_test.go @@ -6,7 +6,9 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/device" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -18,7 +20,7 @@ func TestDisabledToolsUseCommonOperationQualification(t *testing.T) { } } for _, qualified := range []bool{false, true} { - policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": {Placements: []string{"none"}, ProgrammaticToolCallingDisable: qualified}})} + policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": enginetest.Profile(func(p *engine.Profile) { p.ProgrammaticToolCallingDisable = proto.CapabilityFromBool(qualified) })})} if err := policy.ValidateSessionConfiguration("new_harness", raw); (err == nil) != qualified { t.Fatal("qualification differs", qualified, err) } diff --git a/services/core/internal/execution/engine_profile.go b/services/core/internal/execution/engine_profile.go index f7416f9aa..0554cd227 100644 --- a/services/core/internal/execution/engine_profile.go +++ b/services/core/internal/execution/engine_profile.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -18,32 +17,32 @@ func profileError(err error) error { } func validateProfileConfiguration(profile engine.Profile, snapshot Snapshot) error { - if snapshot.Agent.Text.Format.Type == "json_schema" && !profile.StructuredOutput { + if snapshot.Agent.Text.Format.Type == "json_schema" && !profile.StructuredOutput.IsSupported() { return errors.New("Structured output is not qualified for this engine.") } - if profile.ValidateConfiguration != nil { + if profile.ConfigurationValidation == engine.AdditionalValidation { if err := profile.ValidateConfiguration(snapshot.Agent, snapshot.Environment, snapshot.Daemon != nil); err != nil { return profileError(err) } } tools, err := executionTools(snapshot.Agent.Tools) // Preserve each profile's admission error precedence when tool decoding fails. - if profile.ValidateConfiguration != nil && err != nil { + if profile.ConfigurationValidation == engine.AdditionalValidation && err != nil { return err } if err == nil { if err := profile.ValidateMCPOrigins(snapshot.Environment, snapshot.Daemon != nil, tools.MCP); err != nil { return err } - if tools.DisableProgrammatic && !profile.ProgrammaticToolCallingDisable { + if tools.DisableProgrammatic && !profile.ProgrammaticToolCallingDisable.IsSupported() { return errors.New("Disabling programmatic tool calling is not qualified for this engine.") } request := proto.PromptRequestPayload{ToolSearch: tools.Search, FunctionTools: tools.Functions} - if err := request.ValidateToolSearch(profile.ToolSearch); err != nil { + if err := request.ValidateToolSearch(profile.ToolSearch.IsSupported()); err != nil { return err } } - if profile.ValidateTools != nil { + if profile.ToolsValidation == engine.AdditionalValidation { if validationErr := profile.ValidateTools(snapshot.Environment, snapshot.Daemon != nil, tools.Functions, tools.MCP); validationErr != nil { return profileError(validationErr) } @@ -66,7 +65,7 @@ func validateProfileInputs(profile engine.Profile, placement string, inputs []st } continue } - if input.Kind != "tool_result" || profile.ValidateFunctionResult == nil { + if input.Kind != "tool_result" || profile.FunctionResultValidation == engine.CommonValidationOnly { continue } var value store.FunctionResultInput diff --git a/services/core/internal/execution/engine_profile_test.go b/services/core/internal/execution/engine_profile_test.go index 16ea78f22..a17ba31e4 100644 --- a/services/core/internal/execution/engine_profile_test.go +++ b/services/core/internal/execution/engine_profile_test.go @@ -3,11 +3,13 @@ package execution import ( "encoding/json" "errors" + "fmt" "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -29,31 +31,33 @@ func TestAcceptedEnginePlacements(t *testing.T) { func TestAdditionalProfileUsesCommonAdmission(t *testing.T) { configurationChecked, toolsChecked, resultChecked := false, false, false - catalog := engine.NewCatalog(map[string]engine.Profile{"fixture": { - Placements: []string{"none"}, - ValidateConfiguration: func(agent v1.Agent, environment *v1.Environment, hasDaemon bool) error { - configurationChecked = true - if agent.Model != "fixture" || environment == nil || hasDaemon { - return engine.ErrInvalidInput - } - return nil - }, - ValidateTools: func(_ *v1.Environment, _ bool, tools []proto.FunctionTool, mcp []proto.MCPHTTPServer) error { - toolsChecked = true - if len(tools) != 1 || tools[0].Name != "echo" || len(mcp) != 0 { - t.Fatal("common tool decoding did not reach profile") - } - return nil - }, - ValidateFunctionResult: func(placement string, result proto.FunctionResultPayload) error { - content := result.Content - resultChecked = true - if placement != "none" || !result.Success || len(content) != 1 || content[0].Text == nil || *content[0].Text != "response" { - t.Fatal("common result decoding did not reach profile") - } + profile := enginetest.Profile(nil) + profile.ConfigurationValidation = engine.AdditionalValidation + profile.ToolsValidation = engine.AdditionalValidation + profile.FunctionResultValidation = engine.AdditionalValidation + profile.ValidateConfiguration = func(agent v1.Agent, environment *v1.Environment, hasDaemon bool) error { + configurationChecked = true + if agent.Model != "fixture" || environment == nil || hasDaemon { return engine.ErrInvalidInput - }, - }}) + } + return nil + } + profile.ValidateTools = func(_ *v1.Environment, _ bool, tools []proto.FunctionTool, mcp []proto.MCPHTTPServer) error { + toolsChecked = true + if len(tools) != 1 || tools[0].Name != "echo" || len(mcp) != 0 { + t.Fatal("common tool decoding did not reach profile") + } + return nil + } + profile.ValidateFunctionResult = func(placement string, result proto.FunctionResultPayload) error { + content := result.Content + resultChecked = true + if placement != "none" || !result.Success || len(content) != 1 || content[0].Text == nil || *content[0].Text != "response" { + t.Fatal("common result decoding did not reach profile") + } + return engine.ErrInvalidInput + } + catalog := engine.NewCatalog(map[string]engine.Profile{"fixture": profile}) profile, ok := catalog.Lookup("fixture") if !ok || !profile.Accepts("none") || profile.Accepts("openai_hosted") { t.Fatal("fixture qualification was not selected") @@ -82,3 +86,66 @@ func TestClaudeHostedToolsRequireSeparateQualification(t *testing.T) { } } } + +func TestExplicitValidationPoliciesPreserveErrorPrecedence(t *testing.T) { + snapshot := Snapshot{Agent: v1.Agent{Tools: []json.RawMessage{json.RawMessage(`{"type":"unknown"}`)}}} + _, decodeErr := executionTools(snapshot.Agent.Tools) + if decodeErr == nil { + t.Fatal("fixture must fail common tool decoding") + } + configurationErr := errors.New("configuration restriction") + toolsErr := errors.New("tools restriction") + for _, configuration := range []engine.ValidationPolicy{engine.CommonValidationOnly, engine.AdditionalValidation} { + for _, rejectConfiguration := range []bool{false, true} { + if configuration == engine.CommonValidationOnly && rejectConfiguration { + continue + } + t.Run(fmt.Sprintf("configuration=%d/reject=%t", configuration, rejectConfiguration), func(t *testing.T) { + toolsCalled := false + profile := enginetest.Profile(func(p *engine.Profile) { + p.ConfigurationValidation = configuration + if configuration == engine.AdditionalValidation { + p.ValidateConfiguration = func(v1.Agent, *v1.Environment, bool) error { + if rejectConfiguration { + return configurationErr + } + return nil + } + } + p.ToolsValidation = engine.AdditionalValidation + p.ValidateTools = func(*v1.Environment, bool, []proto.FunctionTool, []proto.MCPHTTPServer) error { + toolsCalled = true + return toolsErr + } + }) + catalog := engine.NewCatalog(map[string]engine.Profile{"fixture": profile}) + profile, _ = catalog.Lookup("fixture") + err := validateProfileConfiguration(profile, snapshot) + switch { + case rejectConfiguration: + if !errors.Is(err, configurationErr) || toolsCalled { + t.Fatal("configuration lost precedence", err) + } + case configuration == engine.AdditionalValidation: + if err == nil || err.Error() != decodeErr.Error() || toolsCalled { + t.Fatal("decoding lost precedence", err) + } + default: + if !errors.Is(err, toolsErr) || !toolsCalled { + t.Fatal("common-only configuration changed tool error precedence", err) + } + } + }) + } + } +} + +func TestCommonOnlyValidationPreservesFunctionResults(t *testing.T) { + profile := enginetest.Profile(nil) + catalog := engine.NewCatalog(map[string]engine.Profile{"fixture": profile}) + profile, _ = catalog.Lookup("fixture") + inputs := []store.Input{{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":"response"}}`)}} + if err := validateProfileInputs(profile, "none", inputs); err != nil { + t.Fatal("common-only result acquired a native restriction", err) + } +} diff --git a/services/core/internal/execution/mcp_support.go b/services/core/internal/execution/mcp_support.go index 78bf1687f..1b0791c49 100644 --- a/services/core/internal/execution/mcp_support.go +++ b/services/core/internal/execution/mcp_support.go @@ -14,7 +14,7 @@ func (p Policy) mcpCredentialBindings(engine string, snapshot Snapshot) (map[str return nil, err } profile, qualified := p.Engines.Lookup(engine) - if len(selected) > 0 && (!qualified || !profile.MCPBearer) { + if len(selected) > 0 && (!qualified || !profile.MCPBearer.IsSupported()) { return nil, errors.New("The configured engine currently supports anonymous HTTP MCP only.") } return selected, nil diff --git a/services/core/internal/execution/message_support.go b/services/core/internal/execution/message_support.go index 2e685a10f..c05dcfde3 100644 --- a/services/core/internal/execution/message_support.go +++ b/services/core/internal/execution/message_support.go @@ -17,7 +17,7 @@ var ErrWhitespaceOnlyText = errors.New("whitespace-only message text is not supp // validateMessageTextProfile rejects a message without an image or any // non-whitespace text when the harness has not qualified such input. func validateMessageTextProfile(profile engine.Profile, input proto.MessageInput) error { - if profile.WhitespaceOnlyText { + if profile.WhitespaceOnlyText.IsSupported() { return nil } for _, message := range input { @@ -50,7 +50,7 @@ func validateMessageImageProfile(profile engine.Profile, _ string, input proto.M if !input.HasImages() { return nil } - if !profile.MessageImages || input.ValidateInlineImages() != nil { + if !profile.MessageImages.IsSupported() || input.ValidateInlineImages() != nil { return store.ErrInvalidInput } return nil diff --git a/services/core/internal/execution/message_support_test.go b/services/core/internal/execution/message_support_test.go index c83712f09..112f4a11d 100644 --- a/services/core/internal/execution/message_support_test.go +++ b/services/core/internal/execution/message_support_test.go @@ -7,20 +7,21 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { url := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" input := proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image", ImageURL: &url}}}} - profile := engine.Profile{MessageImages: true} + profile := enginetest.Profile(func(p *engine.Profile) { p.MessageImages = proto.CapabilitySupported }) if err := validateMessageImageProfile(profile, "none", input); err != nil { t.Fatal(err) } if err := validateMessageImageProfile(profile, "self_hosted", input); err != nil { t.Fatal("qualified user machine rejected", err) } - if err := validateMessageImageProfile(engine.Profile{}, "none", input); !errors.Is(err, store.ErrInvalidInput) { + if err := validateMessageImageProfile(enginetest.Profile(nil), "none", input); !errors.Is(err, store.ErrInvalidInput) { t.Fatal("unqualified profile accepted", err) } // Text admission and dispatch must not gain an online/image requirement. @@ -33,7 +34,7 @@ func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { // Message validation applies even when no function-result validator exists. raw, _ := json.Marshal(map[string]any{"input": []any{map[string]any{"role": "user", "content": input[0].Content}}}) batch := []store.Input{{Kind: "message", Payload: json.RawMessage(`{"text":"valid first"}`)}, {Kind: "message", Payload: raw}} - if err := validateProfileInputs(engine.Profile{}, "none", batch); !errors.Is(err, store.ErrInvalidInput) { + if err := validateProfileInputs(enginetest.Profile(nil), "none", batch); !errors.Is(err, store.ErrInvalidInput) { t.Fatal("image escaped profile validation", err) } if err := validateProfileInputs(profile, "none", batch); err != nil { diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index 5d32261fb..916c8b64a 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -7,7 +7,9 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/device" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -19,7 +21,7 @@ func TestStructuredOutputNeedsOperationQualification(t *testing.T) { } } for _, qualified := range []bool{false, true} { - policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": {Placements: []string{"none"}, StructuredOutput: qualified}})} + policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": enginetest.Profile(func(p *engine.Profile) { p.StructuredOutput = proto.CapabilityFromBool(qualified) })})} if err := policy.ValidateSessionConfiguration("new_harness", raw); (err == nil) != qualified { t.Fatal("new harness did not use common qualification", err) } diff --git a/services/core/internal/execution/support.go b/services/core/internal/execution/support.go index 4a9192253..139729a19 100644 --- a/services/core/internal/execution/support.go +++ b/services/core/internal/execution/support.go @@ -100,13 +100,13 @@ func (p Policy) engineCapabilities(peer *gateway.Session, engine string, snapsho if !caps.ExecutionControls { return fail("device must advertise execution_controls") } - if profile.WebSearchControl && !caps.WebSearchControl { + if profile.WebSearchControl.IsSupported() && !caps.WebSearchControl { return fail("device must advertise web_search_control") } if snapshot.Agent.Text.Format.Type == "json_schema" && (!caps.StructuredOutput || !caps.MessageItems) { return fail("device must support structured output and message observations") } - if profile.TextVerbosity && !caps.TextVerbosity { + if profile.TextVerbosity.IsSupported() && !caps.TextVerbosity { return fail("device must advertise text_verbosity") } if !caps.ToolObservations { diff --git a/services/core/internal/execution/tool_search_test.go b/services/core/internal/execution/tool_search_test.go index ec4526f44..80a5ea7dc 100644 --- a/services/core/internal/execution/tool_search_test.go +++ b/services/core/internal/execution/tool_search_test.go @@ -7,19 +7,23 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" ) const discoveryConfiguration = `{"agent":{"model":"model","tools":[{"type":"tool_search"},{"type":"function","name":"lookup","description":"Lookup","parameters":{"type":"object"},"defer_loading":true},{"type":"function","name":"clock","description":"Clock","parameters":{"type":"object"}}]},"environment":{"type":"none"}}` func TestDiscoveryUsesSharedOperationQualification(t *testing.T) { for _, qualified := range []bool{false, true} { - policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": {Placements: []string{"none"}, ToolSearch: qualified}})} + policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": enginetest.Profile(func(p *engine.Profile) { p.ToolSearch = proto.CapabilityFromBool(qualified) })})} if err := policy.ValidateSessionConfiguration("new_harness", json.RawMessage(discoveryConfiguration)); (err == nil) != qualified { t.Fatal("common operation qualification was not applied", qualified, err) } } workspace := strings.Replace(discoveryConfiguration, `"type":"none"`, `"type":"openai_hosted"`, 1) - policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": {Placements: []string{"openai_hosted"}, ToolSearch: true}})} + policy := Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"new_harness": enginetest.Profile(func(p *engine.Profile) { + p.Placements = []string{"openai_hosted"} + p.ToolSearch = proto.CapabilitySupported + })})} if err := policy.ValidateSessionConfiguration("new_harness", json.RawMessage(workspace)); err != nil { t.Fatal("Core imposed another adapter's placement restriction", err) } diff --git a/services/core/internal/store/harness_onboarding_test.go b/services/core/internal/store/harness_onboarding_test.go index 34b75b483..b91433481 100644 --- a/services/core/internal/store/harness_onboarding_test.go +++ b/services/core/internal/store/harness_onboarding_test.go @@ -20,6 +20,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -27,17 +28,21 @@ import ( func TestThirdHarnessPublicOnboarding(t *testing.T) { h := newDispatchHarness(t) - profile := engine.Profile{Placements: []string{"none"}, ValidateConfiguration: func(a v1.Agent, _ *v1.Environment, _ bool) error { + profile := enginetest.Profile(nil) + profile.ConfigurationValidation = engine.AdditionalValidation + profile.ToolsValidation = engine.AdditionalValidation + profile.ValidateConfiguration = func(a v1.Agent, _ *v1.Environment, _ bool) error { if a.Text.Verbosity != "medium" || a.Text.Format.Type != "text" || a.MultiAgent.Enabled || a.Reasoning.Effort != nil || a.Reasoning.Summary != nil || a.ServiceTier != "auto" { return engine.ErrInvalidInput } return nil - }, ValidateTools: func(_ *v1.Environment, _ bool, f []proto.FunctionTool, m []proto.MCPHTTPServer) error { + } + profile.ValidateTools = func(_ *v1.Environment, _ bool, f []proto.FunctionTool, m []proto.MCPHTTPServer) error { if len(f)+len(m) > 0 { return engine.ErrInvalidInput } return nil - }} + } policy := execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"fixture_harness": profile})} h.d.Policy = policy // The fixture only supplies an adapter and registration to the real daemon router. diff --git a/services/core/internal/store/message_image_admission_test.go b/services/core/internal/store/message_image_admission_test.go index 275753455..f1202c080 100644 --- a/services/core/internal/store/message_image_admission_test.go +++ b/services/core/internal/store/message_image_admission_test.go @@ -6,6 +6,7 @@ import ( "errors" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -25,7 +26,7 @@ func TestUnqualifiedImageAdmissionIsAtomic(t *testing.T) { // A registered text-only profile must stay closed regardless of the // adapters currently qualified by the built-in catalog. profile, _ := (engine.Catalog{}).Lookup("codex") - profile.MessageImages = false + profile.MessageImages = proto.CapabilityUnsupported h.d.Policy = execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"codex": profile})} worker, err := execution.StartWorker(t.Context(), h.d) if err != nil { diff --git a/services/core/internal/store/structured_output_dispatch_test.go b/services/core/internal/store/structured_output_dispatch_test.go index 733f70546..86a1ee10a 100644 --- a/services/core/internal/store/structured_output_dispatch_test.go +++ b/services/core/internal/store/structured_output_dispatch_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -16,7 +17,7 @@ import ( func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { h := newDispatchHarness(t) configuration := json.RawMessage(`{"agent":{"model":"fixture","text":{"format":{"type":"json_schema","schema":{"type":"object"}}}},"environment":{"type":"none"}}`) - profile := engine.Profile{Placements: []string{"none"}, StructuredOutput: true} + profile := enginetest.Profile(func(p *engine.Profile) { p.StructuredOutput = proto.CapabilitySupported }) policy := execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"fixture_harness": profile})} if err := policy.ValidateSessionConfiguration("fixture_harness", configuration); err != nil { t.Fatal(err) @@ -45,7 +46,7 @@ func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { input := h.message("start", "Run") // A restart can remove qualification while admitted work remains queued. // Runtime advertisements cannot qualify an operation on their own. - profile.StructuredOutput = false + profile.StructuredOutput = proto.CapabilityUnsupported h.d.Policy = execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"fixture_harness": profile})} ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel()