From 6c2b4c1126a18294d6d661d3669efd4ceafd82ee Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sun, 25 Oct 2026 05:54:43 +0000 Subject: [PATCH 1/4] Accept HTTP model providers on loopback and private addresses A model provider base_url may now use HTTP when its host is a loopback or private IP address, so a local or LAN gateway works without a TLS terminator. A host name still needs HTTPS, because it can resolve to any address. Core admission and the Runtime's frozen-bundle check share the one rule in internal/modelprovider. Closes #416. --- .../src/features/system/ModelProviderDialog.tsx | 6 +++--- apps/web/src/i18n/locales/en/core-errors.ts | 2 +- apps/web/src/i18n/locales/en/system.ts | 2 +- apps/web/src/i18n/locales/zh-CN/core-errors.ts | 2 +- apps/web/src/i18n/locales/zh-CN/system.ts | 2 +- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/model-execution.md | 2 +- contracts/agents-api/v1/model_execution.go | 2 +- contracts/agents-api/v1/model_execution_test.go | 5 +++-- .../agents-api/v1/model_provider_admission.go | 6 +++--- .../agents-api/v1/model_provider_error_test.go | 2 +- contracts/agents-api/zh/core-errors.md | 4 ++-- contracts/agents-api/zh/model-execution.md | 4 ++-- internal/modelprovider/config.go | 15 ++++++++++++--- .../src/execution-configuration-projection.ts | 7 ++++--- .../api/model_provider_public_validation_test.go | 2 +- 16 files changed, 38 insertions(+), 27 deletions(-) diff --git a/apps/web/src/features/system/ModelProviderDialog.tsx b/apps/web/src/features/system/ModelProviderDialog.tsx index b62a876ff..ca4ac082c 100644 --- a/apps/web/src/features/system/ModelProviderDialog.tsx +++ b/apps/web/src/features/system/ModelProviderDialog.tsx @@ -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; @@ -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. diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts index 2dd7c8fe1..48d6b906a 100644 --- a/apps/web/src/i18n/locales/en/core-errors.ts +++ b/apps/web/src/i18n/locales/en/core-errors.ts @@ -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.", diff --git a/apps/web/src/i18n/locales/en/system.ts b/apps/web/src/i18n/locales/en/system.ts index c5230056a..f63e5bf48 100644 --- a/apps/web/src/i18n/locales/en/system.ts +++ b/apps/web/src/i18n/locales/en/system.ts @@ -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 HTTP or HTTPS URL with a hostname and 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.", diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts index 957ee9144..cd4120883 100644 --- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts +++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts @@ -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 限制;输出不能超过上下文。", diff --git a/apps/web/src/i18n/locales/zh-CN/system.ts b/apps/web/src/i18n/locales/zh-CN/system.ts index 7e63fcb06..4bd11c79a 100644 --- a/apps/web/src/i18n/locales/zh-CN/system.ts +++ b/apps/web/src/i18n/locales/zh-CN/system.ts @@ -80,7 +80,7 @@ export const system: TranslationShape = { setTitle: "设置 {{harness}} 的默认模型配置", replaceTitle: "替换 {{harness}} 的默认模型配置", modelName: "应用未指定模型时,新 Session 使用此模型。请填写该服务接受的准确模型 ID。", - baseUrlInvalid: "请输入包含主机名、不含账号密码、查询参数或片段的 HTTPS URL,例如 https://api.example.com/v1。", + baseUrlInvalid: "请输入包含主机名、不含账号密码、查询参数或片段的 HTTP 或 HTTPS URL,例如 https://api.example.com/v1。", apiKeyHelp: "每次都必须填写。Core 会加密保存,之后不再显示。", contextHelp: "模型的上下文窗口,单位为 token。可选。", outputHelp: "模型单次回复最多输出的 token 数。可选;填写时,上下文窗口必须不小于它。", diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index 297a2a711..624b6d3fd 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -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 | diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 4b90470d7..2d8200b37 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -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 | diff --git a/contracts/agents-api/v1/model_execution.go b/contracts/agents-api/v1/model_execution.go index e193af040..a94782269 100644 --- a/contracts/agents-api/v1/model_execution.go +++ b/contracts/agents-api/v1/model_execution.go @@ -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"} diff --git a/contracts/agents-api/v1/model_execution_test.go b/contracts/agents-api/v1/model_execution_test.go index b6ddb0526..b801c99c0 100644 --- a/contracts/agents-api/v1/model_execution_test.go +++ b/contracts/agents-api/v1/model_execution_test.go @@ -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) } diff --git a/contracts/agents-api/v1/model_provider_admission.go b/contracts/agents-api/v1/model_provider_admission.go index d1b0d3f1e..190551d18 100644 --- a/contracts/agents-api/v1/model_provider_admission.go +++ b/contracts/agents-api/v1/model_provider_admission.go @@ -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. @@ -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 diff --git a/contracts/agents-api/v1/model_provider_error_test.go b/contracts/agents-api/v1/model_provider_error_test.go index 3952c6f30..1b00b3d5c 100644 --- a/contracts/agents-api/v1/model_provider_error_test.go +++ b/contracts/agents-api/v1/model_provider_error_test.go @@ -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 = "" diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index 63260327f..56cedb10e 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -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`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -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 没有已设置限制的原生设置 | diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 3725b5aba..2643dd36c 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: 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) 声明其协议和原生参数。 @@ -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` | 可选的非负整数,输出限制不得大于上下文限制。请使用真实模型的限制 | diff --git a/internal/modelprovider/config.go b/internal/modelprovider/config.go index 95d10fcfd..0e695e373 100644 --- a/internal/modelprovider/config.go +++ b/internal/modelprovider/config.go @@ -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") { @@ -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 { diff --git a/packages/agents-client/src/execution-configuration-projection.ts b/packages/agents-client/src/execution-configuration-projection.ts index b78e69a3b..dbd1ce5f3 100644 --- a/packages/agents-client/src/execution-configuration-projection.ts +++ b/packages/agents-client/src/execution-configuration-projection.ts @@ -18,12 +18,13 @@ function selection(value: unknown, invalid: Invalid): SessionExecutionConfigurat } // Splits an absolute URL as RFC 3986 appendix B does, without parsing its host. -const baseURLPattern = /^https:\/\/([^/?#]*)[^?#]*(?:\?([^#]*))?(?:#([\s\S]*))?$/iu; +const baseURLPattern = /^https?:\/\/([^/?#]*)[^?#]*(?:\?([^#]*))?(?:#([\s\S]*))?$/iu; /** * Checks a stored base URL as Core's write rule does, never more strictly: - * HTTPS with a host and no credentials, query or fragment. Host syntax is - * Core's to enforce, so one stored value never fails a whole list. + * HTTP or HTTPS with a host and no credentials, query or fragment. Host syntax, + * including which hosts HTTP may use, is Core's to enforce, so one stored value + * never fails a whole list. */ function safeBaseURL(value: string): boolean { const match = baseURLPattern.exec(value); diff --git a/services/core/internal/api/model_provider_public_validation_test.go b/services/core/internal/api/model_provider_public_validation_test.go index e334d368b..c6e1a6665 100644 --- a/services/core/internal/api/model_provider_public_validation_test.go +++ b/services/core/internal/api/model_provider_public_validation_test.go @@ -17,7 +17,7 @@ func TestModelProviderPublicValidation(t *testing.T) { body := `{"agent":{"model":"fixture"},"environment":{"type":"openai_hosted"},"input":"hello","x_agents_core":{"model_provider":` + tc.provider + `}}` out := credentialRequest(h, http.MethodPost, "/v1/agents/sessions", body) messages := map[string]string{ - "url": "model provider requires an HTTPS base_url without credentials, query or fragment", + "url": "model provider requires an HTTPS base_url, or HTTP to a loopback or private IP address, without credentials, query or fragment", "protocol": "unsupported model provider protocol", "key": "invalid model provider API key", "limits": "invalid model token limits", } From 01fad0379ea25502f3a2726193b73e313ed7561a Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sun, 25 Oct 2026 05:57:55 +0000 Subject: [PATCH 2/4] Keep the client's base_url check no stricter than Core --- packages/agents-client/src/admin-client.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index 541650322..e40ad125a 100644 --- a/packages/agents-client/src/admin-client.test.ts +++ b/packages/agents-client/src/admin-client.test.ts @@ -165,7 +165,7 @@ describe("AdminClient transport boundary", () => { const input = { model: "test-model", harness_config: {}, model_provider: { protocol: "responses", base_url: "https://model.example/v1", api_key: "write-only", context_window: 200000, max_output_tokens: 8000 } } as const; expect(await client.setHarnessModelConfiguration("codex", input)).toEqual(provider); expect(JSON.parse(fetch.mock.calls[0]![1]!.body as string)).toEqual(input); - for (const unsafe of [{ ...provider, api_key: "leak" }, { ...provider, harness: "mcode" }, { ...provider, model_provider: { ...provider.model_provider, api_key_configured: false } }, { ...provider, model_provider: { ...provider.model_provider, base_url: "http://model.example/v1" } }, { ...provider, model_provider: { ...provider.model_provider, api_key: "leak" } }, { ...provider, model: "" }, { ...provider, harness_config: [] }, { ...provider, extra: 1 }]) { + for (const unsafe of [{ ...provider, api_key: "leak" }, { ...provider, harness: "mcode" }, { ...provider, model_provider: { ...provider.model_provider, api_key_configured: false } }, { ...provider, model_provider: { ...provider.model_provider, base_url: "ftp://model.example/v1" } }, { ...provider, model_provider: { ...provider.model_provider, api_key: "leak" } }, { ...provider, model: "" }, { ...provider, harness_config: [] }, { ...provider, extra: 1 }]) { await expect(clientWith(unsafe).client.retrieveHarnessModelConfiguration("codex")).rejects.toMatchObject({ code: "invalid_admin_response" }); } for (const unsafe of [{ ...harnesses, data: [{ ...harnesses.data[1], model_configuration: { ...provider, api_key: "leak" } }] }, { ...harnesses, data: [harnesses.data[1], harnesses.data[1]] }, { data: harnesses.data }]) { @@ -240,9 +240,9 @@ describe("AdminClient response contracts", () => { const saved = { id: resourceId, object: "agent", model: "model", name: null, instructions: null, metadata: {}, multi_agent: { enabled: false, max_concurrent_subagents: null }, reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], created_at: 1, updated_at: 1 }; const withURL = (base_url: string, id = resourceId) => ({ ...saved, id, x_agents_core: { model_provider: { protocol: "responses", base_url, api_key_configured: true } } }); // The client is never stricter than Core's write rule: a host or port that URL parsing rejects must not fail the list. - const stored = ["https://p.test:99999/v1", "https://xn--.test", "https://[::1]:8443/v1", "HTTPS://p.test/v1?"].map((url, index) => withURL(url, `agent-${index}`)); + const stored = ["https://p.test:99999/v1", "https://xn--.test", "https://[::1]:8443/v1", "HTTPS://p.test/v1?", "http://10.0.0.5:8080/v1"].map((url, index) => withURL(url, `agent-${index}`)); expect((await clientWith(page(stored)).client.listAgents(projectId)).data).toEqual(stored); - for (const url of ["http://p.test", "https://user:pw@p.test", "https://p.test/?key=secret", "https://p.test/#secret", "https://:443/v1"]) { + for (const url of ["ftp://p.test", "https://user:pw@p.test", "https://p.test/?key=secret", "https://p.test/#secret", "https://:443/v1"]) { await expect(clientWith(page([withURL(url)])).client.listAgents(projectId)).rejects.toBeInstanceOf(AgentCoreError); } }); From a6bee9b6821fc6809580000a6cede29ba98ee044 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sun, 25 Oct 2026 06:03:48 +0000 Subject: [PATCH 3/4] Leave the HTTP host rule to Core in the provider dialog test --- apps/web/e2e/system.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/e2e/system.spec.ts b/apps/web/e2e/system.spec.ts index cf4277fac..bf8b30f15 100644 --- a/apps/web/e2e/system.spec.ts +++ b/apps/web/e2e/system.spec.ts @@ -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(); From 74d8c812a7685c2e69c06172ec7607defc27a332 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sun, 25 Oct 2026 06:24:48 +0000 Subject: [PATCH 4/4] Describe the HTTP host rule in the provider hint --- apps/web/src/i18n/locales/en/system.ts | 2 +- apps/web/src/i18n/locales/zh-CN/system.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/i18n/locales/en/system.ts b/apps/web/src/i18n/locales/en/system.ts index f63e5bf48..48845e78d 100644 --- a/apps/web/src/i18n/locales/en/system.ts +++ b/apps/web/src/i18n/locales/en/system.ts @@ -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 HTTP or 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.", diff --git a/apps/web/src/i18n/locales/zh-CN/system.ts b/apps/web/src/i18n/locales/zh-CN/system.ts index 4bd11c79a..b2ee52295 100644 --- a/apps/web/src/i18n/locales/zh-CN/system.ts +++ b/apps/web/src/i18n/locales/zh-CN/system.ts @@ -80,7 +80,7 @@ export const system: TranslationShape = { setTitle: "设置 {{harness}} 的默认模型配置", replaceTitle: "替换 {{harness}} 的默认模型配置", modelName: "应用未指定模型时,新 Session 使用此模型。请填写该服务接受的准确模型 ID。", - baseUrlInvalid: "请输入包含主机名、不含账号密码、查询参数或片段的 HTTP 或 HTTPS URL,例如 https://api.example.com/v1。", + baseUrlInvalid: "请输入 HTTPS URL,或指向回环、私有 IP 地址的 HTTP URL,不含账号密码、查询参数或片段,例如 https://api.example.com/v1。", apiKeyHelp: "每次都必须填写。Core 会加密保存,之后不再显示。", contextHelp: "模型的上下文窗口,单位为 token。可选。", outputHelp: "模型单次回复最多输出的 token 数。可选;填写时,上下文窗口必须不小于它。",