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 apps/web/e2e/system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test("sets, replaces and clears a harness's default model configuration, and kee
await set.getByLabel("Default model ID").fill("fixture-model");
await expect(set).toContainText("Used for new Sessions when the application does not specify a model.");
// Every rejected URL stays local; Enter cannot bypass validation or write a secret.
for (const url of ["http://model.example/v1", "ftp://model.example", "https://user:password@model.example/v1", "https://@model.example/v1", "https:model.example/v1", "https://model.example/v1?token=x", "https://model.example/v1#fragment", "https://model.example/v1?", "https://model.example/v1#", "not a URL"]) {
for (const url of ["ftp://model.example", "https://user:password@model.example/v1", "https://@model.example/v1", "https:model.example/v1", "https://model.example/v1?token=x", "https://model.example/v1#fragment", "https://model.example/v1?", "https://model.example/v1#", "not a URL"]) {
await set.getByLabel("Base URL").fill(url);
await expect(set.getByLabel("Base URL")).toHaveAttribute("aria-invalid", "true");
await expect(set.getByRole("button", { name: "Save", exact: true })).toBeDisabled();
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/system/ModelProviderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ function tokenLimit(text: string): { value: number | undefined; problem: "whole"
function isProviderUrl(value: string): boolean {
try {
const url = new URL(value);
const authority = /^https:\/\/([^/]+)/iu.exec(value)?.[1];
return authority !== undefined && url.protocol === "https:" && url.hostname !== "" && !authority.includes("@")
const authority = /^https?:\/\/([^/]+)/iu.exec(value)?.[1];
return authority !== undefined && (url.protocol === "https:" || url.protocol === "http:") && url.hostname !== "" && !authority.includes("@")
&& !/[\\\s?#]/u.test(value);
} catch {
return false;
Expand All @@ -38,7 +38,7 @@ function isProviderUrl(value: string): boolean {
/**
* Sets or replaces one harness's model configuration. Non-secret fields start from the
* current provider; the API key never does. The protocol describes the upstream
* model provider. The form checks the HTTPS provider URL and whole-number
* model provider. The form checks the provider URL's shape and whole-number
* limits within Core's range, with max output no larger than the context window.
* Core's typed rejection is shown beside its field, or beside the form
* when no editable field applies. Enter saves; a save in flight blocks another.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/en/core-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const coreErrors = {
"model_configuration_model_invalid": "Enter a model ID of at most 1024 UTF-8 bytes without control characters.",
"harness_config_invalid": "Check the supported native fields and their values. The JSON object must be at most 16 KiB and cannot redefine Core-managed settings.",
"invalid_model_provider": "Enter the complete model provider configuration.",
"model_provider_base_url_invalid": "Use an HTTPS URL without credentials, query parameters or a fragment.",
"model_provider_base_url_invalid": "Use an HTTPS URL, or HTTP to a loopback or private IP address, without credentials, query parameters or a fragment.",
"model_provider_protocol_unsupported": "This protocol is not supported by the Harness.",
"model_provider_api_key_invalid": "Enter a valid API key without control characters.",
"model_provider_token_limits_invalid": "Use valid token limits; output cannot exceed context.",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/en/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const system = {
setTitle: "Set default model configuration for {{harness}}",
replaceTitle: "Replace default model configuration for {{harness}}",
modelName: "Used for new Sessions when the application does not specify a model. Enter an exact model ID accepted by this provider.",
baseUrlInvalid: "Enter an HTTPS URL with a hostname and no credentials, query or fragment, such as https://api.example.com/v1.",
baseUrlInvalid: "Enter an HTTPS URL, or an HTTP URL to a loopback or private IP address, with no credentials, query or fragment, such as https://api.example.com/v1.",
apiKeyHelp: "Required every time. Core encrypts it and never shows it again.",
contextHelp: "The model's context window, in tokens. Optional.",
outputHelp: "The most tokens the model writes in one response. Optional; it needs a context window at least as large.",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/zh-CN/core-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const coreErrors = {
"model_configuration_model_invalid": "请输入模型 ID,最多 1024 个 UTF-8 字节,且不能包含控制字符。",
"harness_config_invalid": "请检查支持的原生字段及其取值。JSON 对象不能超过 16 KiB,也不能重复定义 Core 管理的设置。",
"invalid_model_provider": "请填写完整的模型提供商配置。",
"model_provider_base_url_invalid": "请使用 HTTPS 地址,不要包含凭据、查询参数或片段。",
"model_provider_base_url_invalid": "请使用 HTTPS 地址(回环或私有 IP 地址也可以用 HTTP),不要包含凭据、查询参数或片段。",
"model_provider_protocol_unsupported": "此 Harness 不支持该协议。",
"model_provider_api_key_invalid": "请输入有效的 API 密钥,不要包含控制字符。",
"model_provider_token_limits_invalid": "请填写有效的 token 限制;输出不能超过上下文。",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/zh-CN/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const system: TranslationShape<typeof english> = {
setTitle: "设置 {{harness}} 的默认模型配置",
replaceTitle: "替换 {{harness}} 的默认模型配置",
modelName: "应用未指定模型时,新 Session 使用此模型。请填写该服务接受的准确模型 ID。",
baseUrlInvalid: "请输入包含主机名、不含账号密码、查询参数或片段的 HTTPS URL,例如 https://api.example.com/v1。",
baseUrlInvalid: "请输入 HTTPS URL,或指向回环、私有 IP 地址的 HTTP URL,不含账号密码、查询参数或片段,例如 https://api.example.com/v1。",
apiKeyHelp: "每次都必须填写。Core 会加密保存,之后不再显示。",
contextHelp: "模型的上下文窗口,单位为 token。可选。",
outputHelp: "模型单次回复最多输出的 token 数。可选;填写时,上下文窗口必须不小于它。",
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/core-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf
| `invalid_name` | `name` | `max_length`: 128 for Projects and nodes, 80 for Project keys | The name failed the resource's validator |
| `invalid_node_capacity` | `max_active` or `max_retained` | `min`: 1, `max`: 1000000 | Capacity is invalid; retained capacity must also be at least active capacity |
| `invalid_model_provider` | null | omitted | A complete model-provider bundle is required |
| `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS without credentials, query or fragment |
| `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS, or HTTP to a loopback or private IP address, without credentials, query or fragment |
| `model_provider_protocol_unsupported` | `protocol` | `harness` and `allowed_protocols`, from the build's adapter catalog | The protocol is unknown or unsupported by the selected Harness |
| `model_provider_api_key_invalid` | `api_key` | `max_length`: 16384 | The key is empty, too long or contains a prohibited character |
| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, `max_output_tokens` exceeds a set `context_window`, or the Harness has no native setting for a set limit |
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/model-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A Harness receives one frozen model provider bundle, defined by [`modelprovider.
| Field | Meaning |
| --- | --- |
| `protocol` | The upstream API: `anthropic`, `responses` or `chat_completions`, not an engine. The selected Harness must support it natively |
| `base_url` | An HTTPS URL with a valid host, without credentials, query or fragment |
| `base_url` | An HTTPS URL with a valid host, or an HTTP URL whose host is a loopback or private IP address, without credentials, query or fragment. Loopback is the machine the Harness runs on: in a Core-managed sandbox, the sandbox itself |
| `api_key` | Nonempty, at most 16 KiB, without NUL, CR or LF |
| `context_window`, `max_output_tokens` | Optional nonnegative integers, with output no larger than context. Use the real model's limits |

Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/v1/model_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error {
return &ModelProviderError{Code: "invalid_model_provider", Param: "", message: "model_provider is required"}
}
if !validModelProviderBaseURL(p.BaseURL) {
return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "model provider requires an HTTPS base_url without credentials, query or fragment"}
return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "model provider requires an HTTPS base_url, or HTTP to a loopback or private IP address, without credentials, query or fragment"}
}
if !registry.SupportsProtocol(string(p.Protocol)) {
return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: "unsupported model provider protocol"}
Expand Down
5 changes: 3 additions & 2 deletions contracts/agents-api/v1/model_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ func TestModelExecutionValidation(t *testing.T) {
t.Fatalf("unsupported protocol or harness accepted: %s/%s", tc.protocol, tc.harness)
}
}
for _, url := range []string{"http://example.com", "https://user:pass@example.com", "https://example.com?key=secret", "https://example.com#secret", "https://",
for _, url := range []string{"http://example.com", "http://8.8.8.8/v1", "http://169.254.169.254/v1", "http://localhost:7351/v1", "https://user:pass@example.com", "https://example.com?key=secret", "https://example.com#secret", "https://",
"https://example.com:99999/v1", "https://example.com:0/v1", "https://xn--.test", "https://xn--a.test", "https://a..b", "https://999.1.1.1"} {
if (&ModelProviderInput{Protocol: "responses", BaseURL: url, APIKey: "secret"}).Validate() == nil {
t.Fatal("unsafe or unusable provider URL accepted", url)
}
}
for _, url := range []string{"https://example.com:8443/v1", "https://127.0.0.1/v1", "https://[::1]:8443/v1", "https://model_gateway.internal/v1", "https://bücher.example/v1"} {
for _, url := range []string{"https://example.com:8443/v1", "https://127.0.0.1/v1", "https://[::1]:8443/v1", "https://model_gateway.internal/v1", "https://bücher.example/v1",
"http://127.0.0.1:7351/v1", "http://10.0.0.5:8080/v1", "http://[fd00::1]:8080/v1"} {
if err := (&ModelProviderInput{Protocol: "responses", BaseURL: url, APIKey: "secret"}).Validate(); err != nil {
t.Fatal("valid provider URL rejected", url, err)
}
Expand Down
6 changes: 3 additions & 3 deletions contracts/agents-api/v1/model_provider_admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"strings"

"golang.org/x/net/idna"
)

const providerScheme = "https"
"github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider"
)

// providerHost converts a domain as URL host parsing does (UTS #46 without
// hyphen or STD3 restrictions), rejecting invalid labels such as bad punycode.
Expand Down Expand Up @@ -45,7 +45,7 @@ func ModelProviderRequired(environment string) bool {

func validModelProviderBaseURL(base string) bool {
u, err := url.Parse(base)
return err == nil && u.Scheme == providerScheme && validModelProviderHost(u) && u.User == nil && u.RawQuery == "" && u.Fragment == "" && !strings.ContainsAny(base, "\x00\r\n")
return err == nil && modelprovider.SchemeAllowed(u) && validModelProviderHost(u) && u.User == nil && u.RawQuery == "" && u.Fragment == "" && !strings.ContainsAny(base, "\x00\r\n")
}

// validModelProviderHost requires a usable host: an IP address, or a domain
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/v1/model_provider_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) {
name, harness, code, param, message string
change func(*ModelProviderInput)
}{
{"url first", "codex", "model_provider_base_url_invalid", "base_url", "model provider requires an HTTPS base_url without credentials, query or fragment", func(p *ModelProviderInput) {
{"url first", "codex", "model_provider_base_url_invalid", "base_url", "model provider requires an HTTPS base_url, or HTTP to a loopback or private IP address, without credentials, query or fragment", func(p *ModelProviderInput) {
p.BaseURL = "http://private.example"
p.Protocol = "private"
p.APIKey = ""
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/core-errors.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Core 管理错误"
source: contracts/agents-api/core-errors.md
source_hash: f2c2a927b8faad9c2834ae4d1195940195df5fc617c8395a027f0d21dfb5ea1e
source_hash: 1424dc3288c244cb3be78bb2070530bdc0b5aec8474c1ab943611bee335fdca7
---

`/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。
Expand Down Expand Up @@ -60,7 +60,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封
| `invalid_name` | `name` | `max_length`:项目和节点为 128,项目 API 密钥为 80 | 名称未通过相应资源的验证器 |
| `invalid_node_capacity` | `max_active` 或 `max_retained` | `min`:1,`max`:1000000 | 容量无效;保留容量还必须至少等于活动容量 |
| `invalid_model_provider` | null | 省略 | 必须提供完整的模型提供商配置包 |
| `model_provider_base_url_invalid` | `base_url` | 省略 | 必须使用 HTTPS,且不得包含凭据、查询或片段 |
| `model_provider_base_url_invalid` | `base_url` | 省略 | 必须使用 HTTPS(回环或私有 IP 地址也可以用 HTTP),且不得包含凭据、查询或片段 |
| `model_provider_protocol_unsupported` | `protocol` | `harness` 和 `allowed_protocols`,来自该构建的适配器目录 | 协议未知,或所选 Harness 不支持该协议 |
| `model_provider_api_key_invalid` | `api_key` | `max_length`:16384 | 密钥为空、过长或包含禁止字符 |
| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数、`max_output_tokens` 超过已设置的 `context_window`,或 Harness 没有已设置限制的原生设置 |
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: 2ac821cd3d4635e6780e3cf517298225036a6d24317d1375bfdc44789959f55a
source_hash: d33f9a9919e52136f8ece0a8480c1db059f2f35aa1cefa852ba1d001cc259fa2
---

每个 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) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。
Expand All @@ -13,7 +13,7 @@ Harness 接收一个冻结的模型提供商配置包,其定义见 [`modelprov
| 字段 | 含义 |
| --- | --- |
| `protocol` | 上游 API:`anthropic`、`responses` 或 `chat_completions`,而不是引擎。所选 Harness 必须原生支持它 |
| `base_url` | 带有效主机名的 HTTPS URL,不得包含凭据、查询参数或片段 |
| `base_url` | 带有效主机名的 HTTPS URL,或主机为回环或私有 IP 地址的 HTTP URL,不得包含凭据、查询参数或片段。回环地址指 Harness 所在的机器:在 Core 管理的沙箱中就是沙箱本身 |
| `api_key` | 不得为空,最长 16 KiB,不得包含 NUL、CR 或 LF |
| `context_window`、`max_output_tokens` | 可选的非负整数,输出限制不得大于上下文限制。请使用真实模型的限制 |

Expand Down
15 changes: 12 additions & 3 deletions internal/modelprovider/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ func (p Provider) Validate() error {
if err != nil || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || strings.ContainsAny(p.BaseURL, "\x00\r\n") {
return ErrConfiguration
}
// Remote providers require HTTPS. Runtime-local providers may use loopback
// HTTP; Core retains stricter public provider admission.
if u.Scheme != "https" && !(u.Scheme == "http" && net.ParseIP(u.Hostname()).IsLoopback()) {
if !SchemeAllowed(u) {
return ErrConfiguration
}
if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") {
Expand All @@ -76,6 +74,17 @@ func (p Provider) Validate() error {
return nil
}

// SchemeAllowed is the one base URL scheme rule: HTTPS, or HTTP to a loopback
// or private IP address, so the key never crosses a public network in plain
// text. A host name never qualifies for HTTP, because it can resolve anywhere.
func SchemeAllowed(u *url.URL) bool {
if u.Scheme == "https" {
return true
}
ip := net.ParseIP(u.Hostname())
return u.Scheme == "http" && ip != nil && (ip.IsLoopback() || ip.IsPrivate())
}

// TokenLimitsValid holds the token-limit rule: unset is zero, and an output
// limit never exceeds a set context window.
func TokenLimitsValid(contextWindow, maxOutputTokens int32) bool {
Expand Down
Loading
Loading