Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/agents-api/model-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- | --- |
Expand Down
10 changes: 9 additions & 1 deletion contracts/agents-api/v1/core_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
}
10 changes: 7 additions & 3 deletions contracts/agents-api/v1/model_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion contracts/agents-api/v1/saved_core_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/model-execution.md
Original file line number Diff line number Diff line change
@@ -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) 声明其协议和原生参数。
Expand Down Expand Up @@ -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 | 接受的字段 | 应用方式 |
| --- | --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/public-agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/api/public-agent-api.md
Original file line number Diff line number Diff line change
@@ -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 存在差异的地方。
Expand Down Expand Up @@ -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 会在创建时冻结其提供商。
Expand Down
9 changes: 4 additions & 5 deletions internal/harnessconfig/builtin/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,23 @@ 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},
} {
err := Registry().ValidateHarnessConfig(tc.kind, json.RawMessage(tc.raw))
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)
Expand Down
33 changes: 16 additions & 17 deletions internal/harnessconfig/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
11 changes: 9 additions & 2 deletions services/core/internal/agents/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand Down
7 changes: 7 additions & 0 deletions services/core/internal/api/errors_agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ 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) {
return
}
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):
Expand Down
26 changes: 20 additions & 6 deletions services/core/internal/api/session_model_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading