From 31e64822076038047f23241982079348d94b0f48 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 17:20:03 +0000 Subject: [PATCH] Require the harness for a non-empty harness_config Native parameters belong to one adapter, so validating them against whichever registered adapter accepts them was an implicit fallback. Delete the any-adapter loop: an empty kind with parameters now returns harnessconfig.ErrHarnessRequired. The resolved Agent decides: a saved Agent after the write (an update keeps the saved harness) and, for a Session, the inline agent's harness or the saved Agent's. Both reject with 400 invalid_request_error and a param that names the missing field. --- contracts/agents-api/model-execution.md | 2 +- contracts/agents-api/v1/core_extension.go | 10 +++++- .../agents-api/v1/model_configuration.go | 10 ++++-- .../agents-api/v1/saved_core_extension.go | 4 ++- contracts/agents-api/zh/model-execution.md | 4 +-- docs/api/public-agent-api.md | 2 +- docs/zh/api/public-agent-api.md | 4 +-- internal/harnessconfig/builtin/native_test.go | 9 +++-- internal/harnessconfig/native.go | 33 +++++++++---------- .../core/internal/agents/configuration.go | 11 +++++-- services/core/internal/api/errors_agents.go | 7 ++++ .../api/session_model_configuration.go | 26 +++++++++++---- .../api/session_model_configuration_test.go | 6 ++-- .../unified_model_configuration_http_test.go | 21 ++++++++++++ 14 files changed, 105 insertions(+), 44 deletions(-) diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index ca27f0193..056bc9394 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -100,7 +100,7 @@ The Harness reaches its frozen upstream through a Session-local credential gatew ## Native model parameters -`harness_config` holds the selected Harness's native model parameters. Saved Agents accept it in `x_agents_core`, Sessions in the inline `agent.x_agents_core` and in the top-level `x_agents_core`; the top-level value wins. +`harness_config` holds the selected Harness's native model parameters. Saved Agents accept it in `x_agents_core`, Sessions in the inline `agent.x_agents_core` and in the top-level `x_agents_core`; the top-level value wins. A non-empty object needs an explicitly selected Harness: the Agent's `x_agents_core.harness`, which an update keeps, or for a Session the inline `agent.x_agents_core.harness` or the saved Agent's. The deployment default Harness does not count. Without one, the request fails with 400 `invalid_request_error` and `param` `x_agents_core.harness`, or `agent.x_agents_core.harness` on Session creation. | Harness | Accepted fields | Applied as | | --- | --- | --- | diff --git a/contracts/agents-api/v1/core_extension.go b/contracts/agents-api/v1/core_extension.go index 156761009..fe84766c4 100644 --- a/contracts/agents-api/v1/core_extension.go +++ b/contracts/agents-api/v1/core_extension.go @@ -2,7 +2,10 @@ package v1 import ( "encoding/json" + "errors" "fmt" + + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" ) @@ -22,5 +25,10 @@ func (x *AgentsCore) Validate() error { if x.Harness != "" && !builtin.Contains(x.Harness) { return fmt.Errorf("x_agents_core.harness must select a registered harness") } - return ValidateHarnessConfig(x.Harness, x.HarnessConfig) + // Session admission requires the harness on the resolved Agent: an inline + // extension without one keeps the saved Agent's. + if err := ValidateHarnessConfig(x.Harness, x.HarnessConfig); err != nil && !errors.Is(err, harnessconfig.ErrHarnessRequired) { + return err + } + return nil } diff --git a/contracts/agents-api/v1/model_configuration.go b/contracts/agents-api/v1/model_configuration.go index 001285756..729667f5f 100644 --- a/contracts/agents-api/v1/model_configuration.go +++ b/contracts/agents-api/v1/model_configuration.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" @@ -28,11 +29,14 @@ func (c ModelConfigurationInput) ValidateHarness(harness string) error { return ValidateNativeModelConfiguration(harness, c.Model, c.HarnessConfig) } +// ValidateHarnessConfig returns harnessconfig.ErrHarnessRequired unchanged: +// the caller names the missing harness field of its own resource. func ValidateHarnessConfig(harness string, raw json.RawMessage) error { - if err := builtin.Registry().ValidateHarnessConfig(harness, raw); err != nil { - return &ModelProviderError{Code: "harness_config_invalid", Param: "harness_config", message: "harness_config contains unsupported or invalid native model parameters"} + err := builtin.Registry().ValidateHarnessConfig(harness, raw) + if err == nil || errors.Is(err, harnessconfig.ErrHarnessRequired) { + return err } - return nil + return &ModelProviderError{Code: "harness_config_invalid", Param: "harness_config", message: "harness_config contains unsupported or invalid native model parameters"} } func (c ModelConfigurationInput) SafeView() ModelConfigurationView { diff --git a/contracts/agents-api/v1/saved_core_extension.go b/contracts/agents-api/v1/saved_core_extension.go index 1b1353a3d..735295d6d 100644 --- a/contracts/agents-api/v1/saved_core_extension.go +++ b/contracts/agents-api/v1/saved_core_extension.go @@ -40,7 +40,9 @@ func (x *SavedAgentCoreInput) Validate() error { return err } } - if err := ValidateHarnessConfig(x.Harness, x.HarnessConfig); err != nil { + // The saved Agent after the write must have the harness: an update keeps + // the saved one. + if err := ValidateHarnessConfig(x.Harness, x.HarnessConfig); err != nil && !errors.Is(err, harnessconfig.ErrHarnessRequired) { return err } if x.ModelProvider == nil { diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 3961e53e4..bfde567c6 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: 36c013fb36432a792ee693a7cee46d739710d6e3faba7f8448d418226dae38b4 +source_hash: 2a80d9958e34d744793307b46354dfc887becc632f70d1e5193ee4b7c27d2172 --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接并声明[凭据网关](#credential-gateway)转发的内容,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -102,7 +102,7 @@ Harness 通过 agent host 上 Session 本地的凭据网关访问其冻结的上 ## 原生模型参数 {#native-model-parameters} -`harness_config` 保存所选 Harness 的原生模型参数。已保存 Agent 在 `x_agents_core` 中接受它,Session 则在内联的 `agent.x_agents_core` 和顶层 `x_agents_core` 中接受它;顶层值优先。 +`harness_config` 保存所选 Harness 的原生模型参数。已保存 Agent 在 `x_agents_core` 中接受它,Session 则在内联的 `agent.x_agents_core` 和顶层 `x_agents_core` 中接受它;顶层值优先。非空对象需要显式选择的 Harness:Agent 的 `x_agents_core.harness`(更新时保留已保存的值),或者对 Session 而言,内联的 `agent.x_agents_core.harness` 或已保存 Agent 的 Harness。部署默认 Harness 不算在内。缺少 Harness 时,请求以 400 `invalid_request_error` 失败,`param` 为 `x_agents_core.harness`;创建 Session 时为 `agent.x_agents_core.harness`。 | Harness | 接受的字段 | 应用方式 | | --- | --- | --- | diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index 27200285e..869b0a4c4 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -152,7 +152,7 @@ Any other member is rejected with 400. `api_key` is write-only: reads return `ap ## Choose a harness and a model -The harness is the agent program that runs a Session: Codex (`codex`), Claude Code (`claude_sdk`) or MiniMax Code (`mcode`). Set `x_agents_core.harness` on the Agent or the inline `agent`; without it, the installation's default harness applies ([`core.default_harness`](../configuration.md#settings), Codex unless the operator changed it). +The harness is the agent program that runs a Session: Codex (`codex`), Claude Code (`claude_sdk`) or MiniMax Code (`mcode`). Set `x_agents_core.harness` on the Agent or the inline `agent`; without it, the installation's default harness applies ([`core.default_harness`](../configuration.md#settings), Codex unless the operator changed it) and `harness_config` must be empty. - **Model.** `model` is the provider's exact model ID. An inline Agent may omit it to use the default model configuration of its harness. A saved Agent always needs one. - **Provider.** The harness calls your provider with one of the harness's native protocols, through a [credential gateway](../../contracts/agents-api/model-execution.md#credential-gateway) that keeps your key out of the harness; there is no conversion, and a mismatch is rejected when the Session is created. [Model execution](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) lists each harness's protocols and which provider a Session uses on each Environment type. A Session freezes its provider at creation. diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 354d7ce40..121358a74 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: f7999939977b8cf4a516c2bea09cd50c8078d6fb05dffeb137dfba45c47839d1 +source_hash: 07bcf81c1f4d131d55e38bf1158805a24e53bae13c79205857dc8fbe25001123 --- Core 在 `/v1` 提供 [OpenAI Agents API](https://platform.openai.com/docs/api-reference)。可以使用官方 OpenAI SDK 或普通 HTTP。本指南针对每项常见操作同时展示这两种方式,并说明 Core 与 OpenAI 存在差异的地方。 @@ -154,7 +154,7 @@ Core 可以运行多种 harness,并允许接入你自己的模型访问方式 ## 选择 harness 和模型 {#choose-a-harness-and-a-model} -harness 是运行 Session 的 Agent 程序:Codex(`codex`)、Claude Code(`claude_sdk`)或 MiniMax Code(`mcode`)。请在 Agent 或内联 `agent` 上设置 `x_agents_core.harness`;如果未设置,则使用安装的默认 harness([`core.default_harness`](../configuration.md#settings),除非操作员另行更改,否则为 Codex)。 +harness 是运行 Session 的 Agent 程序:Codex(`codex`)、Claude Code(`claude_sdk`)或 MiniMax Code(`mcode`)。请在 Agent 或内联 `agent` 上设置 `x_agents_core.harness`;如果未设置,则使用安装的默认 harness([`core.default_harness`](../configuration.md#settings),除非操作员另行更改,否则为 Codex),且 `harness_config` 必须为空。 - **模型。** `model` 是提供商给出的准确模型 ID。内联 Agent 可以省略此字段,以使用其 harness 的默认模型配置。保存的 Agent 始终必须指定模型。 - **提供商。** harness 会使用其原生协议之一,经由一个让你的密钥不进入 harness 的[凭据网关](../../../contracts/agents-api/zh/model-execution.md#credential-gateway)调用你的提供商;系统不会进行转换,不匹配时会在创建 Session 阶段拒绝请求。[Model execution](../../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence) 列出了每个 harness 的协议,以及各类 Environment 上 Session 使用的提供商。Session 会在创建时冻结其提供商。 diff --git a/internal/harnessconfig/builtin/native_test.go b/internal/harnessconfig/builtin/native_test.go index e449d1d4f..5560d74b5 100644 --- a/internal/harnessconfig/builtin/native_test.go +++ b/internal/harnessconfig/builtin/native_test.go @@ -32,9 +32,8 @@ func TestNativeModelParameters(t *testing.T) { {"claude_sdk", `{"maxThinkingTokens":1024}`, false}, {"mcode", `{}`, true}, {"mcode", `{"effort":"high"}`, false}, - {"", `{"effort":"high"}`, true}, - {"", `{"model_reasoning_effort":"high"}`, true}, - {"", `{"secret":"private-value"}`, false}, + {"", `{}`, true}, + {"", `{"effort":"high"}`, false}, {"unknown", `{}`, true}, {"unknown", `{"effort":"high"}`, false}, } { @@ -42,14 +41,14 @@ func TestNativeModelParameters(t *testing.T) { if (err == nil) != tc.valid { t.Fatalf("%s %s: %v", tc.kind, tc.raw, err) } - if err != nil && err != harnessconfig.ErrHarnessConfig { + if err != nil && err != harnessconfig.ErrHarnessConfig && err != harnessconfig.ErrHarnessRequired { t.Fatalf("unsafe error: %v", err) } } } func TestNativeConfigurationBoundary(t *testing.T) { - for _, kind := range []string{"codex", "claude_sdk", "mcode", ""} { + for _, kind := range []string{"codex", "claude_sdk", "mcode"} { for _, raw := range []string{"null", "[]", "1", `"value"`, `{"unknown":"` + strings.Repeat("x", harnessconfig.MaxHarnessConfigBytes) + `"}`} { if err := Registry().ValidateHarnessConfig(kind, json.RawMessage(raw)); err != harnessconfig.ErrHarnessConfig { t.Fatalf("%s accepted invalid shape: %v", kind, err) diff --git a/internal/harnessconfig/native.go b/internal/harnessconfig/native.go index 89cc9a69a..c98038d10 100644 --- a/internal/harnessconfig/native.go +++ b/internal/harnessconfig/native.go @@ -9,8 +9,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -// ErrHarnessConfig deliberately excludes caller-controlled keys and values. -var ErrHarnessConfig = errors.New("invalid or unsupported harness_config") +var ( + // ErrHarnessConfig deliberately excludes caller-controlled keys and values. + ErrHarnessConfig = errors.New("invalid or unsupported harness_config") + // ErrHarnessRequired rejects native parameters without the Harness whose + // adapter defines them. + ErrHarnessRequired = errors.New("harness_config parameters require a selected harness") +) const MaxHarnessConfigBytes = 16 * 1024 @@ -44,9 +49,8 @@ func (c Configuration) ParseHarnessConfig(raw proto.HarnessConfig) (map[string]a return result, nil } -// ValidateHarnessConfig validates a selected adapter. An empty kind is for saved -// Agents without a selected Harness: at least one registered adapter must accept -// the object. Session admission always validates its resolved kind again. +// ValidateHarnessConfig validates native parameters against the selected +// adapter. Only an empty object is valid without one. func (r Registry) ValidateHarnessConfig(kind string, raw json.RawMessage) error { config, err := decodeHarnessConfig(raw) if err != nil { @@ -57,20 +61,15 @@ func (r Registry) ValidateHarnessConfig(kind string, raw json.RawMessage) error if len(config) == 0 { return nil } - if kind != "" { - c, ok := r.Lookup(kind) - if !ok { - return ErrHarnessConfig - } - _, err := c.ParseHarnessConfig(raw) - return err + if kind == "" { + return ErrHarnessRequired } - for _, c := range r.configurations { - if _, err := c.ParseHarnessConfig(raw); err == nil { - return nil - } + c, ok := r.Lookup(kind) + if !ok { + return ErrHarnessConfig } - return ErrHarnessConfig + _, err = c.ParseHarnessConfig(raw) + return err } // Reject repeated members before storage so values discarded by encoding/json diff --git a/services/core/internal/agents/configuration.go b/services/core/internal/agents/configuration.go index 209e98998..d5479a86e 100644 --- a/services/core/internal/agents/configuration.go +++ b/services/core/internal/agents/configuration.go @@ -2,9 +2,11 @@ package agents import ( "encoding/json" + "errors" "fmt" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata" ) @@ -55,7 +57,8 @@ func normalizeConfiguration(raw json.RawMessage) (json.RawMessage, error) { // validateModelExecution checks the saved Harness configuration and that the // model provider bundle suits the saved Harness. provider is the bundle being -// saved, if any. +// saved, if any. Native parameters without a saved Harness return +// harnessconfig.ErrHarnessRequired. func validateModelExecution(configuration json.RawMessage, provider *v1.ModelProviderInput) error { if provider != nil { if err := provider.Validate(); err != nil { @@ -69,7 +72,11 @@ func validateModelExecution(configuration json.RawMessage, provider *v1.ModelPro return fmt.Errorf("%w: x_agents_core: %v", ErrInvalidInput, err) } if config.Core != nil { - if err := v1.ValidateHarnessConfig(config.Core.Harness, config.Core.HarnessConfig); err != nil { + err := v1.ValidateHarnessConfig(config.Core.Harness, config.Core.HarnessConfig) + if errors.Is(err, harnessconfig.ErrHarnessRequired) { + return err + } + if err != nil { return fmt.Errorf("%w: %s", ErrInvalidInput, err) } } diff --git a/services/core/internal/api/errors_agents.go b/services/core/internal/api/errors_agents.go index 211e6c3fe..6c4607784 100644 --- a/services/core/internal/api/errors_agents.go +++ b/services/core/internal/api/errors_agents.go @@ -5,10 +5,15 @@ import ( "net/http" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents" ) +// harnessRequiredMessage reports native parameters on an Agent or Session +// without a selected Harness. +const harnessRequiredMessage = "harness_config parameters require an explicit x_agents_core.harness." + // writeAgentsError reports an error of the Agent operations. func writeAgentsError(w http.ResponseWriter, r *http.Request, err error) { if writeTextValueError(w, r, err) || writeAuditSourceError(w, r, err) || writeCredentialUnavailableError(w, r, err) { @@ -16,6 +21,8 @@ func writeAgentsError(w http.ResponseWriter, r *http.Request, err error) { } var provider *v1.ModelProviderError switch { + case errors.Is(err, harnessconfig.ErrHarnessRequired): + writeError(w, http.StatusBadRequest, "invalid_request_error", harnessRequiredMessage, "x_agents_core.harness") case errors.As(err, &provider): writeError(w, http.StatusBadRequest, "unsupported_or_invalid_configuration", provider.Error()) case errors.Is(err, agents.ErrNotFound): diff --git a/services/core/internal/api/session_model_configuration.go b/services/core/internal/api/session_model_configuration.go index 86304ea40..29006f915 100644 --- a/services/core/internal/api/session_model_configuration.go +++ b/services/core/internal/api/session_model_configuration.go @@ -6,6 +6,7 @@ import ( "errors" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) // Resolve mutable defaults once, before constructing the immutable Agent. Model @@ -26,6 +27,22 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s } } } + var inline, session json.RawMessage + if input.Agent != nil && input.Agent.XAgentsCore != nil { + inline = input.Agent.XAgentsCore.HarnessConfig + } + if input.XAgentsCore != nil { + session = input.XAgentsCore.HarnessConfig + } + harness := "" + if extension != nil { + harness = extension.Harness + } + for _, native := range []json.RawMessage{inline, session} { + if errors.Is(v1.ValidateHarnessConfig(harness, native), harnessconfig.ErrHarnessRequired) { + return &fieldError{param: "agent.x_agents_core.harness", message: harnessRequiredMessage} + } + } selected, _ := json.Marshal(configuration{Agent: v1.Agent{XAgentsCore: extension}}) engine, err := h.sessionHarness(selected) if err != nil { @@ -61,12 +78,9 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s } raw := json.RawMessage(`{}`) source := "unknown" - var supplied json.RawMessage - if input.Agent != nil && input.Agent.XAgentsCore != nil { - supplied = input.Agent.XAgentsCore.HarnessConfig - } - if input.XAgentsCore != nil && len(input.XAgentsCore.HarnessConfig) > 0 { - supplied = input.XAgentsCore.HarnessConfig + supplied := inline + if len(session) > 0 { + supplied = session } if len(supplied) > 0 { raw, source = supplied, "session" diff --git a/services/core/internal/api/session_model_configuration_test.go b/services/core/internal/api/session_model_configuration_test.go index 7a7fbc5d7..7d7558886 100644 --- a/services/core/internal/api/session_model_configuration_test.go +++ b/services/core/internal/api/session_model_configuration_test.go @@ -24,10 +24,10 @@ func TestSessionNativeConfigurationSources(t *testing.T) { {"model override discards", `{"agent_id":"a","agent":{"model":"other"},"environment":{"type":"openai_hosted"}}`, saved, "other", `{}`, "session", false}, {"explicit model discards deployment", `{"agent":{"model":"other"},"environment":{"type":"openai_hosted"}}`, nil, "other", `{}`, "session", false}, {"explicit clear", `{"agent_id":"a","environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{}}}`, saved, "saved-model", `{}`, "session", false}, - {"inline native", `{"agent":{"model":"other","x_agents_core":{"harness_config":{"model_reasoning_effort":"medium"}}},"environment":{"type":"openai_hosted"}}`, nil, "other", `{"model_reasoning_effort":"medium"}`, "session", false}, - {"session beats inline", `{"agent":{"model":"other","x_agents_core":{"harness_config":{"model_reasoning_effort":"medium"}}},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{}}}`, nil, "other", `{}`, "session", false}, + {"inline native", `{"agent":{"model":"other","x_agents_core":{"harness":"codex","harness_config":{"model_reasoning_effort":"medium"}}},"environment":{"type":"openai_hosted"}}`, nil, "other", `{"model_reasoning_effort":"medium"}`, "session", false}, + {"session beats inline", `{"agent":{"model":"other","x_agents_core":{"harness":"codex","harness_config":{"model_reasoning_effort":"medium"}}},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{}}}`, nil, "other", `{}`, "session", false}, {"null rejects", `{"agent":{"model":"other"},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":null}}`, nil, "", "", "", true}, - {"reserved rejects", `{"agent":{"model":"other"},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{"api_key":"secret"}}}`, nil, "", "", "", true}, + {"reserved rejects", `{"agent":{"model":"other","x_agents_core":{"harness":"codex"}},"environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":{"api_key":"secret"}}}`, nil, "", "", "", true}, {"self_hosted deployment", `{"agent":{},"environment":{"type":"self_hosted","workspace_directory":"/tmp/work"}}`, nil, "deployment-model", `{"model_reasoning_effort":"high"}`, "deployment", false}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/services/core/tests/integration/unified_model_configuration_http_test.go b/services/core/tests/integration/unified_model_configuration_http_test.go index 413b71806..69237eaed 100644 --- a/services/core/tests/integration/unified_model_configuration_http_test.go +++ b/services/core/tests/integration/unified_model_configuration_http_test.go @@ -179,6 +179,27 @@ func TestUnifiedModelConfigurationHTTP(t *testing.T) { } }) } + // Native parameters need the explicit Harness that defines them. + t.Run("harness_config needs its harness", func(t *testing.T) { + agentError := `{"error":{"message":"harness_config parameters require an explicit x_agents_core.harness.","type":"invalid_request_error","code":"invalid_request_error","param":"x_agents_core.harness"}}` + sessionError := strings.Replace(agentError, `"param":"x_agents_core.harness"`, `"param":"agent.x_agents_core.harness"`, 1) + harnessless := text(object(call("POST", "/v1/agents", token, `{"model":"saved-model"}`, "", 201))["id"]) + codex := text(object(call("POST", "/v1/agents", token, `{"model":"saved-model","x_agents_core":{"harness":"codex"}}`, "", 201))["id"]) + for _, tc := range []struct{ path, body, want string }{ + {"/v1/agents", `{"model":"saved-model","x_agents_core":{"harness_config":` + high + `}}`, agentError}, + {"/v1/agents/" + harnessless, `{"x_agents_core":{"harness_config":` + high + `}}`, agentError}, + {"/v1/agents/sessions", `{"agent":{"x_agents_core":{"harness_config":` + high + `}},"environment":{"type":"openai_hosted"}}`, sessionError}, + {"/v1/agents/sessions", `{"agent_id":"` + harnessless + `","environment":{"type":"openai_hosted"},"x_agents_core":{"harness_config":` + high + `}}`, sessionError}, + } { + if got := strings.TrimSpace(string(call("POST", tc.path, token, tc.body, uuid.NewString(), 400))); got != tc.want { + t.Fatalf("%s: %s", tc.path, got) + } + } + // Native parameters set alone keep the saved Agent's Harness. + call("POST", "/v1/agents/"+codex, token, `{"x_agents_core":{"harness_config":`+high+`}}`, "", 200) + session := create(`{"agent_id":"`+codex+`","agent":{"x_agents_core":{"harness_config":`+low+`}},"environment":{"type":"openai_hosted"}}`, uuid.NewString()) + assertSession(session, "saved-model", low, "agent", "session", "deployment", "deployment-canary") + }) // Disabling tools cannot enable a non-native model protocol. for _, protocol := range []string{"anthropic", "chat_completions"} { t.Run("non-native protocol "+protocol, func(t *testing.T) {