From 8f5f78bbe89d35c8bbaf9221bda13169ccf3c54c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 3 Apr 2026 23:25:11 +0900 Subject: [PATCH] feat(providers): reopen model request transport config (#60327) * feat(providers): reopen model request transport config * chore(config): refresh request override baselines --- CHANGELOG.md | 2 +- docs/.generated/config-baseline.core.json | 537 +++++++++- docs/.generated/config-baseline.json | 537 +++++++++- .../reference/secretref-credential-surface.md | 8 + ...tref-user-supplied-credentials-matrix.json | 56 + src/config/config.secrets-schema.test.ts | 22 +- src/config/redact-snapshot.test-hints.ts | 1 + src/config/redact-snapshot.test.ts | 50 + src/config/schema.base.generated.ts | 953 +++++++++++++++++- src/config/schema.help.ts | 36 +- src/config/schema.hints.test.ts | 3 +- src/config/schema.labels.ts | 18 + src/config/types.provider-request.ts | 2 +- src/config/zod-schema.core.ts | 8 +- src/secrets/runtime-config-collectors-core.ts | 2 +- src/secrets/runtime.coverage.test.ts | 12 + src/secrets/runtime.test.ts | 27 +- src/secrets/target-registry-data.ts | 104 ++ 18 files changed, 2355 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b59d3c4746..4d4309eac26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Docs: https://docs.openclaw.ai - Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received. - Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras. - Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467. -- Providers/config: add `models.providers.*.request` overrides for headers and auth on model-provider paths, and full request transport overrides for media provider HTTP paths. +- Providers/config: add full `models.providers.*.request` transport overrides for model-provider paths, including headers, auth, proxy, and TLS, and keep media provider HTTP request transport overrides aligned with the same request-policy surface. Thanks @vincentkoc. - Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong. - Outbound/runtime seams: split delivery, target-resolution, and session/transcript helper loading into narrower runtime seams so outbound hot paths and their owner tests avoid broader setup fan-out. (#60311) Thanks @shakkernerd. diff --git a/docs/.generated/config-baseline.core.json b/docs/.generated/config-baseline.core.json index 329682a47df..a1a8ce12a31 100644 --- a/docs/.generated/config-baseline.core.json +++ b/docs/.generated/config-baseline.core.json @@ -4013,6 +4013,26 @@ "tags": [], "hasChildren": true }, + { + "path": "agents.defaults.subagents.allowAgents", + "kind": "core", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "agents.defaults.subagents.allowAgents.*", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "agents.defaults.subagents.announceTimeoutMs", "kind": "core", @@ -15601,7 +15621,7 @@ "models" ], "label": "Model Provider Request Overrides", - "help": "Optional request overrides for model-provider requests. Today this path supports header and auth overrides only; proxy and TLS transport settings are reserved for request paths that can carry them end to end.", + "help": "Optional request overrides for model-provider requests, including extra headers, auth overrides, proxy routing, and TLS client settings. Use these only when your upstream or enterprise network path requires transport customization.", "hasChildren": true }, { @@ -15817,6 +15837,521 @@ "tags": [], "hasChildren": false }, + { + "path": "models.providers.*.request.proxy", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy", + "help": "Optional proxy override for model-provider requests. Use \"env-proxy\" to honor environment proxy settings or \"explicit-proxy\" to route through a specific proxy URL.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.mode", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy Mode", + "help": "Proxy override mode for model-provider requests: \"env-proxy\" or \"explicit-proxy\".", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy TLS", + "help": "Optional TLS settings used when connecting to the configured proxy.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.ca", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS CA", + "help": "Custom CA bundle used to verify the proxy TLS certificate chain.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.ca.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.ca.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.ca.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Cert", + "help": "Client TLS certificate presented to the proxy when mutual TLS is required.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.cert.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.insecureSkipVerify", + "kind": "core", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Skip Verify", + "help": "Skips proxy TLS certificate verification. Use only for controlled development environments.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Key", + "help": "Private key paired with request.proxy.tls.cert for proxy mutual TLS.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.key.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Passphrase", + "help": "Optional passphrase used to decrypt request.proxy.tls.key.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.serverName", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy TLS Server Name", + "help": "Optional SNI/server-name override used when establishing TLS to the proxy.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.url", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [ + "models", + "url-secret" + ], + "label": "Model Provider Request Proxy URL", + "help": "Explicit proxy URL used when request.proxy.mode is explicit-proxy. Credentials embedded in the URL are treated as sensitive and redacted from snapshots.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request TLS", + "help": "Optional TLS settings used when connecting directly to the upstream model endpoint.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.ca", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS CA", + "help": "Custom CA bundle used to verify the upstream TLS certificate chain.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.ca.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.ca.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.ca.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Cert", + "help": "Client TLS certificate presented to the upstream endpoint when mutual TLS is required.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.cert.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.insecureSkipVerify", + "kind": "core", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "models", + "security" + ], + "label": "Model Provider Request TLS Skip Verify", + "help": "Skips upstream TLS certificate verification. Use only for controlled development environments.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Key", + "help": "Private key paired with request.tls.cert for upstream mutual TLS.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.key.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Passphrase", + "help": "Optional passphrase used to decrypt request.tls.key.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.passphrase.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.serverName", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request TLS Server Name", + "help": "Optional SNI/server-name override used when establishing upstream TLS.", + "hasChildren": false + }, { "path": "nodeHost", "kind": "core", diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index d663cbd0ea8..18d8394609c 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -4012,6 +4012,26 @@ "tags": [], "hasChildren": true }, + { + "path": "agents.defaults.subagents.allowAgents", + "kind": "core", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "agents.defaults.subagents.allowAgents.*", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "agents.defaults.subagents.announceTimeoutMs", "kind": "core", @@ -15600,7 +15620,7 @@ "models" ], "label": "Model Provider Request Overrides", - "help": "Optional request overrides for model-provider requests. Today this path supports header and auth overrides only; proxy and TLS transport settings are reserved for request paths that can carry them end to end.", + "help": "Optional request overrides for model-provider requests, including extra headers, auth overrides, proxy routing, and TLS client settings. Use these only when your upstream or enterprise network path requires transport customization.", "hasChildren": true }, { @@ -15816,6 +15836,521 @@ "tags": [], "hasChildren": false }, + { + "path": "models.providers.*.request.proxy", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy", + "help": "Optional proxy override for model-provider requests. Use \"env-proxy\" to honor environment proxy settings or \"explicit-proxy\" to route through a specific proxy URL.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.mode", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy Mode", + "help": "Proxy override mode for model-provider requests: \"env-proxy\" or \"explicit-proxy\".", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy TLS", + "help": "Optional TLS settings used when connecting to the configured proxy.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.ca", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS CA", + "help": "Custom CA bundle used to verify the proxy TLS certificate chain.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.ca.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.ca.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.ca.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Cert", + "help": "Client TLS certificate presented to the proxy when mutual TLS is required.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.cert.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.cert.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.insecureSkipVerify", + "kind": "core", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Skip Verify", + "help": "Skips proxy TLS certificate verification. Use only for controlled development environments.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Key", + "help": "Private key paired with request.proxy.tls.cert for proxy mutual TLS.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.key.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.key.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request Proxy TLS Passphrase", + "help": "Optional passphrase used to decrypt request.proxy.tls.key.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.passphrase.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.tls.serverName", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request Proxy TLS Server Name", + "help": "Optional SNI/server-name override used when establishing TLS to the proxy.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.proxy.url", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [ + "models", + "url-secret" + ], + "label": "Model Provider Request Proxy URL", + "help": "Explicit proxy URL used when request.proxy.mode is explicit-proxy. Credentials embedded in the URL are treated as sensitive and redacted from snapshots.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls", + "kind": "core", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request TLS", + "help": "Optional TLS settings used when connecting directly to the upstream model endpoint.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.ca", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS CA", + "help": "Custom CA bundle used to verify the upstream TLS certificate chain.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.ca.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.ca.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.ca.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Cert", + "help": "Client TLS certificate presented to the upstream endpoint when mutual TLS is required.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.cert.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.cert.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.insecureSkipVerify", + "kind": "core", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced", + "models", + "security" + ], + "label": "Model Provider Request TLS Skip Verify", + "help": "Skips upstream TLS certificate verification. Use only for controlled development environments.", + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Key", + "help": "Private key paired with request.tls.cert for upstream mutual TLS.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.key.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.key.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase", + "kind": "core", + "type": [ + "object", + "string" + ], + "required": false, + "deprecated": false, + "sensitive": true, + "tags": [ + "models", + "security" + ], + "label": "Model Provider Request TLS Passphrase", + "help": "Optional passphrase used to decrypt request.tls.key.", + "hasChildren": true + }, + { + "path": "models.providers.*.request.tls.passphrase.id", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase.provider", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.passphrase.source", + "kind": "core", + "type": "string", + "required": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "models.providers.*.request.tls.serverName", + "kind": "core", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "models" + ], + "label": "Model Provider Request TLS Server Name", + "help": "Optional SNI/server-name override used when establishing upstream TLS.", + "hasChildren": false + }, { "path": "nodeHost", "kind": "core", diff --git a/docs/reference/secretref-credential-surface.md b/docs/reference/secretref-credential-surface.md index a68c424f61c..2ab3abf3203 100644 --- a/docs/reference/secretref-credential-surface.md +++ b/docs/reference/secretref-credential-surface.md @@ -27,6 +27,14 @@ Scope intent: - `models.providers.*.request.auth.token` - `models.providers.*.request.auth.value` - `models.providers.*.request.headers.*` +- `models.providers.*.request.proxy.tls.ca` +- `models.providers.*.request.proxy.tls.cert` +- `models.providers.*.request.proxy.tls.key` +- `models.providers.*.request.proxy.tls.passphrase` +- `models.providers.*.request.tls.ca` +- `models.providers.*.request.tls.cert` +- `models.providers.*.request.tls.key` +- `models.providers.*.request.tls.passphrase` - `skills.entries.*.apiKey` - `agents.defaults.memorySearch.remote.apiKey` - `agents.list[].memorySearch.remote.apiKey` diff --git a/docs/reference/secretref-user-supplied-credentials-matrix.json b/docs/reference/secretref-user-supplied-credentials-matrix.json index 242e71e98c9..b62466e6eb8 100644 --- a/docs/reference/secretref-user-supplied-credentials-matrix.json +++ b/docs/reference/secretref-user-supplied-credentials-matrix.json @@ -461,6 +461,62 @@ "secretShape": "secret_input", "optIn": true }, + { + "id": "models.providers.*.request.proxy.tls.ca", + "configFile": "openclaw.json", + "path": "models.providers.*.request.proxy.tls.ca", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.proxy.tls.cert", + "configFile": "openclaw.json", + "path": "models.providers.*.request.proxy.tls.cert", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.proxy.tls.key", + "configFile": "openclaw.json", + "path": "models.providers.*.request.proxy.tls.key", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.proxy.tls.passphrase", + "configFile": "openclaw.json", + "path": "models.providers.*.request.proxy.tls.passphrase", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.tls.ca", + "configFile": "openclaw.json", + "path": "models.providers.*.request.tls.ca", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.tls.cert", + "configFile": "openclaw.json", + "path": "models.providers.*.request.tls.cert", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.tls.key", + "configFile": "openclaw.json", + "path": "models.providers.*.request.tls.key", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "models.providers.*.request.tls.passphrase", + "configFile": "openclaw.json", + "path": "models.providers.*.request.tls.passphrase", + "secretShape": "secret_input", + "optIn": true + }, { "id": "plugins.entries.brave.config.webSearch.apiKey", "configFile": "openclaw.json", diff --git a/src/config/config.secrets-schema.test.ts b/src/config/config.secrets-schema.test.ts index f2bef4a5b26..322aa794c2d 100644 --- a/src/config/config.secrets-schema.test.ts +++ b/src/config/config.secrets-schema.test.ts @@ -136,7 +136,7 @@ describe("config secret refs schema", () => { expect(result.ok).toBe(true); }); - it("accepts model provider request secret refs for auth and headers", () => { + it("accepts model provider request secret refs for auth, headers, and tls material", () => { const result = validateConfigObjectRaw({ models: { providers: { @@ -150,6 +150,17 @@ describe("config secret refs schema", () => { mode: "authorization-bearer", token: { source: "env", provider: "default", id: "OPENAI_PROVIDER_TOKEN" }, }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: { source: "file", provider: "filemain", id: "/tls/provider-proxy-ca" }, + }, + }, + tls: { + cert: { source: "file", provider: "filemain", id: "/tls/provider-cert" }, + key: { source: "file", provider: "filemain", id: "/tls/provider-key" }, + }, }, models: [{ id: "gpt-5", name: "gpt-5" }], }, @@ -160,7 +171,7 @@ describe("config secret refs schema", () => { expect(result.ok).toBe(true); }); - it("rejects model provider request proxy and tls overrides", () => { + it("rejects model provider request proxy url secret refs", () => { const result = validateConfigObjectRaw({ models: { providers: { @@ -169,10 +180,7 @@ describe("config secret refs schema", () => { request: { proxy: { mode: "explicit-proxy", - url: "http://proxy.example:8080", - }, - tls: { - cert: { source: "file", provider: "filemain", id: "/tls/provider-cert" }, + url: { source: "env", provider: "default", id: "PROVIDER_PROXY_URL" }, }, }, models: [{ id: "gpt-5", name: "gpt-5" }], @@ -184,7 +192,7 @@ describe("config secret refs schema", () => { expect(result.ok).toBe(false); if (!result.ok) { expect( - result.issues.some((issue) => issue.path.includes("models.providers.openai.request")), + result.issues.some((issue) => issue.path.includes("models.providers.openai.request.proxy")), ).toBe(true); } }); diff --git a/src/config/redact-snapshot.test-hints.ts b/src/config/redact-snapshot.test-hints.ts index 8e601aa3b77..354f89f9026 100644 --- a/src/config/redact-snapshot.test-hints.ts +++ b/src/config/redact-snapshot.test-hints.ts @@ -12,5 +12,6 @@ export const redactSnapshotTestHints: ConfigUiHints = { "models.providers.*.baseUrl": { sensitive: true }, "models.providers.*.request.headers.*": { sensitive: true }, "models.providers.*.request.auth.token": { sensitive: true }, + "models.providers.*.request.proxy.url": { sensitive: true }, "skills.entries.*.env.GEMINI_API_KEY": { sensitive: true }, }; diff --git a/src/config/redact-snapshot.test.ts b/src/config/redact-snapshot.test.ts index 2de53487944..92733040950 100644 --- a/src/config/redact-snapshot.test.ts +++ b/src/config/redact-snapshot.test.ts @@ -330,6 +330,56 @@ describe("redactConfigSnapshot", () => { expect(restored.models.providers.openai.request.auth.token).toBe("provider-secret-token"); }); + it("redacts model provider request proxy URLs from config snapshots", () => { + const hints = buildConfigSchema().uiHints; + const raw = `{ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + models: [], + request: { + proxy: { + mode: "explicit-proxy", + url: "http://alice:secret@proxy.example.internal:8080", + }, + }, + }, + }, + }, +}`; + const snapshot = makeSnapshot( + { + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + models: [], + request: { + proxy: { + mode: "explicit-proxy", + url: "http://alice:secret@proxy.example.internal:8080", + }, + }, + }, + }, + }, + }, + raw, + ); + + const result = redactConfigSnapshot(snapshot, hints); + const cfg = result.config as typeof snapshot.config; + expect(cfg.models.providers.openai.request.proxy.url).toBe(REDACTED_SENTINEL); + expect(result.raw).toContain(REDACTED_SENTINEL); + expect(result.raw).not.toContain("alice:secret@"); + + const restored = restoreRedactedValues(result.config, snapshot.config, hints); + expect(restored.models.providers.openai.request.proxy.url).toBe( + "http://alice:secret@proxy.example.internal:8080", + ); + }); + it("does not redact maxTokens-style fields", () => { const snapshot = makeSnapshot({ maxTokens: 16384, diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index fd05dd80054..87ea89deabe 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -1385,6 +1385,864 @@ export const GENERATED_BASE_CONFIG_SCHEMA = { }, ], }, + proxy: { + anyOf: [ + { + type: "object", + properties: { + mode: { + type: "string", + const: "env-proxy", + }, + tls: { + type: "object", + properties: { + ca: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + cert: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + key: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + passphrase: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + serverName: { + type: "string", + }, + insecureSkipVerify: { + type: "boolean", + }, + }, + additionalProperties: false, + }, + }, + required: ["mode"], + additionalProperties: false, + }, + { + type: "object", + properties: { + mode: { + type: "string", + const: "explicit-proxy", + }, + url: { + type: "string", + minLength: 1, + }, + tls: { + type: "object", + properties: { + ca: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + cert: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + key: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + passphrase: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + serverName: { + type: "string", + }, + insecureSkipVerify: { + type: "boolean", + }, + }, + additionalProperties: false, + }, + }, + required: ["mode", "url"], + additionalProperties: false, + }, + ], + }, + tls: { + type: "object", + properties: { + ca: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + cert: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + key: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + passphrase: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + serverName: { + type: "string", + }, + insecureSkipVerify: { + type: "boolean", + }, + }, + additionalProperties: false, + }, }, additionalProperties: false, }, @@ -21614,7 +22472,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA = { }, "models.providers.*.request": { label: "Model Provider Request Overrides", - help: "Optional request overrides for model-provider requests. Use this only for header and auth overrides today; proxy and TLS transport settings are reserved for request paths that can carry them end to end.", + help: "Optional request overrides for model-provider requests, including extra headers, auth overrides, proxy routing, and TLS client settings. Use these only when your upstream or enterprise network path requires transport customization.", tags: ["models"], }, "models.providers.*.request.headers": { @@ -21654,6 +22512,99 @@ export const GENERATED_BASE_CONFIG_SCHEMA = { help: "Optional prefix prepended to request.auth.value when auth mode is header.", tags: ["models"], }, + "models.providers.*.request.proxy": { + label: "Model Provider Request Proxy", + help: 'Optional proxy override for model-provider requests. Use "env-proxy" to honor environment proxy settings or "explicit-proxy" to route through a specific proxy URL.', + tags: ["models"], + }, + "models.providers.*.request.proxy.mode": { + label: "Model Provider Request Proxy Mode", + help: 'Proxy override mode for model-provider requests: "env-proxy" or "explicit-proxy".', + tags: ["models"], + }, + "models.providers.*.request.proxy.url": { + label: "Model Provider Request Proxy URL", + help: "Explicit proxy URL used when request.proxy.mode is explicit-proxy. Credentials embedded in the URL are treated as sensitive and redacted from snapshots.", + tags: ["models", "url-secret"], + }, + "models.providers.*.request.proxy.tls": { + label: "Model Provider Request Proxy TLS", + help: "Optional TLS settings used when connecting to the configured proxy.", + tags: ["models"], + }, + "models.providers.*.request.proxy.tls.ca": { + label: "Model Provider Request Proxy TLS CA", + help: "Custom CA bundle used to verify the proxy TLS certificate chain.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.proxy.tls.cert": { + label: "Model Provider Request Proxy TLS Cert", + help: "Client TLS certificate presented to the proxy when mutual TLS is required.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.proxy.tls.key": { + label: "Model Provider Request Proxy TLS Key", + help: "Private key paired with request.proxy.tls.cert for proxy mutual TLS.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.proxy.tls.passphrase": { + label: "Model Provider Request Proxy TLS Passphrase", + help: "Optional passphrase used to decrypt request.proxy.tls.key.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.proxy.tls.serverName": { + label: "Model Provider Request Proxy TLS Server Name", + help: "Optional SNI/server-name override used when establishing TLS to the proxy.", + tags: ["models"], + }, + "models.providers.*.request.proxy.tls.insecureSkipVerify": { + label: "Model Provider Request Proxy TLS Skip Verify", + help: "Skips proxy TLS certificate verification. Use only for controlled development environments.", + tags: ["security", "models", "advanced"], + }, + "models.providers.*.request.tls": { + label: "Model Provider Request TLS", + help: "Optional TLS settings used when connecting directly to the upstream model endpoint.", + tags: ["models"], + }, + "models.providers.*.request.tls.ca": { + label: "Model Provider Request TLS CA", + help: "Custom CA bundle used to verify the upstream TLS certificate chain.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.tls.cert": { + label: "Model Provider Request TLS Cert", + help: "Client TLS certificate presented to the upstream endpoint when mutual TLS is required.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.tls.key": { + label: "Model Provider Request TLS Key", + help: "Private key paired with request.tls.cert for upstream mutual TLS.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.tls.passphrase": { + label: "Model Provider Request TLS Passphrase", + help: "Optional passphrase used to decrypt request.tls.key.", + tags: ["security", "models"], + sensitive: true, + }, + "models.providers.*.request.tls.serverName": { + label: "Model Provider Request TLS Server Name", + help: "Optional SNI/server-name override used when establishing upstream TLS.", + tags: ["models"], + }, + "models.providers.*.request.tls.insecureSkipVerify": { + label: "Model Provider Request TLS Skip Verify", + help: "Skips upstream TLS certificate verification. Use only for controlled development environments.", + tags: ["security", "models", "advanced"], + }, "models.providers.*.models": { label: "Model Provider Model List", help: "Declared model list for a provider including identifiers, metadata, and optional compatibility/cost hints. Keep IDs exact to provider catalog values so selection and fallback resolve correctly.", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 597dc7b3370..e7d21efce7e 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -748,7 +748,7 @@ export const FIELD_HELP: Record = { "models.providers.*.authHeader": "When true, credentials are sent via the HTTP Authorization header even if alternate auth is possible. Use this only when your provider or proxy explicitly requires Authorization forwarding.", "models.providers.*.request": - "Optional request overrides for model-provider requests. Use this only for header and auth overrides today; proxy and TLS transport settings are reserved for request paths that can carry them end to end.", + "Optional request overrides for model-provider requests, including extra headers, auth overrides, proxy routing, and TLS client settings. Use these only when your upstream or enterprise network path requires transport customization.", "models.providers.*.request.headers": "Extra headers merged into provider requests after default attribution and auth resolution.", "models.providers.*.request.auth": @@ -763,6 +763,40 @@ export const FIELD_HELP: Record = { "Custom auth header value used when auth mode is header.", "models.providers.*.request.auth.prefix": "Optional prefix prepended to request.auth.value when auth mode is header.", + "models.providers.*.request.proxy": + 'Optional proxy override for model-provider requests. Use "env-proxy" to honor environment proxy settings or "explicit-proxy" to route through a specific proxy URL.', + "models.providers.*.request.proxy.mode": + 'Proxy override mode for model-provider requests: "env-proxy" or "explicit-proxy".', + "models.providers.*.request.proxy.url": + "Explicit proxy URL used when request.proxy.mode is explicit-proxy. Credentials embedded in the URL are treated as sensitive and redacted from snapshots.", + "models.providers.*.request.proxy.tls": + "Optional TLS settings used when connecting to the configured proxy.", + "models.providers.*.request.proxy.tls.ca": + "Custom CA bundle used to verify the proxy TLS certificate chain.", + "models.providers.*.request.proxy.tls.cert": + "Client TLS certificate presented to the proxy when mutual TLS is required.", + "models.providers.*.request.proxy.tls.key": + "Private key paired with request.proxy.tls.cert for proxy mutual TLS.", + "models.providers.*.request.proxy.tls.passphrase": + "Optional passphrase used to decrypt request.proxy.tls.key.", + "models.providers.*.request.proxy.tls.serverName": + "Optional SNI/server-name override used when establishing TLS to the proxy.", + "models.providers.*.request.proxy.tls.insecureSkipVerify": + "Skips proxy TLS certificate verification. Use only for controlled development environments.", + "models.providers.*.request.tls": + "Optional TLS settings used when connecting directly to the upstream model endpoint.", + "models.providers.*.request.tls.ca": + "Custom CA bundle used to verify the upstream TLS certificate chain.", + "models.providers.*.request.tls.cert": + "Client TLS certificate presented to the upstream endpoint when mutual TLS is required.", + "models.providers.*.request.tls.key": + "Private key paired with request.tls.cert for upstream mutual TLS.", + "models.providers.*.request.tls.passphrase": + "Optional passphrase used to decrypt request.tls.key.", + "models.providers.*.request.tls.serverName": + "Optional SNI/server-name override used when establishing upstream TLS.", + "models.providers.*.request.tls.insecureSkipVerify": + "Skips upstream TLS certificate verification. Use only for controlled development environments.", "models.providers.*.models": "Declared model list for a provider including identifiers, metadata, and optional compatibility/cost hints. Keep IDs exact to provider catalog values so selection and fallback resolve correctly.", "models.bedrockDiscovery": diff --git a/src/config/schema.hints.test.ts b/src/config/schema.hints.test.ts index 550e8aa4186..2401bc6e34e 100644 --- a/src/config/schema.hints.test.ts +++ b/src/config/schema.hints.test.ts @@ -169,6 +169,7 @@ describe("mapSensitivePaths", () => { expect(hints["gateway.auth.token"]?.sensitive).toBe(true); expect(hints["models.providers.*.headers.*"]?.sensitive).toBe(true); expect(hints["models.providers.*.request.headers.*"]?.sensitive).toBe(true); + expect(hints["models.providers.*.request.proxy.tls.cert"]?.sensitive).toBe(true); expect(hints["skills.entries.*.apiKey"]?.sensitive).toBe(true); }); @@ -194,7 +195,7 @@ describe("collectMatchingSchemaPaths", () => { expect(paths.has("mcp.servers.*.url")).toBe(true); expect(paths.has("models.providers.*.baseUrl")).toBe(true); - expect(paths.has("models.providers.*.request.proxy.url")).toBe(false); + expect(paths.has("models.providers.*.request.proxy.url")).toBe(true); expect(paths.has("tools.media.audio.request.proxy.url")).toBe(true); }); }); diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 10123a037a7..5a663379c2f 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -456,6 +456,24 @@ export const FIELD_LABELS: Record = { "models.providers.*.request.auth.headerName": "Model Provider Request Auth Header Name", "models.providers.*.request.auth.value": "Model Provider Request Auth Header Value", "models.providers.*.request.auth.prefix": "Model Provider Request Auth Header Prefix", + "models.providers.*.request.proxy": "Model Provider Request Proxy", + "models.providers.*.request.proxy.mode": "Model Provider Request Proxy Mode", + "models.providers.*.request.proxy.url": "Model Provider Request Proxy URL", + "models.providers.*.request.proxy.tls": "Model Provider Request Proxy TLS", + "models.providers.*.request.proxy.tls.ca": "Model Provider Request Proxy TLS CA", + "models.providers.*.request.proxy.tls.cert": "Model Provider Request Proxy TLS Cert", + "models.providers.*.request.proxy.tls.key": "Model Provider Request Proxy TLS Key", + "models.providers.*.request.proxy.tls.passphrase": "Model Provider Request Proxy TLS Passphrase", + "models.providers.*.request.proxy.tls.serverName": "Model Provider Request Proxy TLS Server Name", + "models.providers.*.request.proxy.tls.insecureSkipVerify": + "Model Provider Request Proxy TLS Skip Verify", + "models.providers.*.request.tls": "Model Provider Request TLS", + "models.providers.*.request.tls.ca": "Model Provider Request TLS CA", + "models.providers.*.request.tls.cert": "Model Provider Request TLS Cert", + "models.providers.*.request.tls.key": "Model Provider Request TLS Key", + "models.providers.*.request.tls.passphrase": "Model Provider Request TLS Passphrase", + "models.providers.*.request.tls.serverName": "Model Provider Request TLS Server Name", + "models.providers.*.request.tls.insecureSkipVerify": "Model Provider Request TLS Skip Verify", "models.providers.*.models": "Model Provider Model List", "models.bedrockDiscovery": "Bedrock Model Discovery", "models.bedrockDiscovery.enabled": "Bedrock Discovery Enabled", diff --git a/src/config/types.provider-request.ts b/src/config/types.provider-request.ts index 53933f1443a..d054227eb5a 100644 --- a/src/config/types.provider-request.ts +++ b/src/config/types.provider-request.ts @@ -42,4 +42,4 @@ export type ConfiguredProviderRequest = { tls?: ConfiguredProviderRequestTls; }; -export type ConfiguredModelProviderRequest = Pick; +export type ConfiguredModelProviderRequest = ConfiguredProviderRequest; diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index f2027787929..f849ff35ca2 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -288,13 +288,7 @@ const ConfiguredProviderRequestSchema = z .strict() .optional(); -const ConfiguredModelProviderRequestSchema = z - .object({ - headers: z.record(z.string(), SecretInputSchema.register(sensitive)).optional(), - auth: ConfiguredProviderRequestAuthSchema, - }) - .strict() - .optional(); +const ConfiguredModelProviderRequestSchema = ConfiguredProviderRequestSchema; export const ModelDefinitionSchema = z .object({ diff --git a/src/secrets/runtime-config-collectors-core.ts b/src/secrets/runtime-config-collectors-core.ts index 2aabb59e596..d3f221890e7 100644 --- a/src/secrets/runtime-config-collectors-core.ts +++ b/src/secrets/runtime-config-collectors-core.ts @@ -79,7 +79,7 @@ function collectModelProviderAssignments(params: { context: params.context, active: providerIsActive, inactiveReason: "provider is disabled.", - collectTransportSecrets: false, + collectTransportSecrets: true, }); } } diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 08bd2e6e9ea..0ec523b4ac0 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -282,6 +282,18 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string) "x-api-key", ); } + if (entry.id.startsWith("models.providers.*.request.proxy.tls.")) { + setPathCreateStrict( + config, + ["models", "providers", "sample", "request", "proxy", "mode"], + "explicit-proxy", + ); + setPathCreateStrict( + config, + ["models", "providers", "sample", "request", "proxy", "url"], + "http://proxy.example:8080", + ); + } return config; } diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 2c0d279b137..db0c0145dee 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -982,7 +982,7 @@ describe("secrets runtime snapshot", () => { expect(second?.search.selectedProvider).toBe("gemini"); }); - it("resolves model provider request secret refs for headers and auth", async () => { + it("resolves model provider request secret refs for headers, auth, and tls material", async () => { const config = asConfig({ models: { providers: { @@ -996,6 +996,17 @@ describe("secrets runtime snapshot", () => { mode: "authorization-bearer", token: { source: "env", provider: "default", id: "OPENAI_PROVIDER_TOKEN" }, }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: { source: "env", provider: "default", id: "OPENAI_PROVIDER_PROXY_CA" }, + }, + }, + tls: { + cert: { source: "env", provider: "default", id: "OPENAI_PROVIDER_CERT" }, + key: { source: "env", provider: "default", id: "OPENAI_PROVIDER_KEY" }, + }, }, models: [], }, @@ -1008,6 +1019,9 @@ describe("secrets runtime snapshot", () => { env: { OPENAI_PROVIDER_TENANT: "tenant-acme", OPENAI_PROVIDER_TOKEN: "sk-provider-runtime", // pragma: allowlist secret + OPENAI_PROVIDER_PROXY_CA: "proxy-ca", + OPENAI_PROVIDER_CERT: "client-cert", + OPENAI_PROVIDER_KEY: "client-key", }, agentDirs: ["/tmp/openclaw-agent-main"], loadAuthStore: () => ({ version: 1, profiles: {} }), @@ -1021,6 +1035,17 @@ describe("secrets runtime snapshot", () => { mode: "authorization-bearer", token: "sk-provider-runtime", }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: "proxy-ca", + }, + }, + tls: { + cert: "client-cert", + key: "client-key", + }, }); }); diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 84ed455d7f6..2f7aa33648b 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -708,6 +708,110 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInAudit: true, providerIdPathSegmentIndex: 2, }, + { + id: "models.providers.*.request.proxy.tls.ca", + targetType: "models.providers.request.proxy.tls.ca", + targetTypeAliases: ["models.providers.*.request.proxy.tls.ca"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.proxy.tls.ca", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.proxy.tls.cert", + targetType: "models.providers.request.proxy.tls.cert", + targetTypeAliases: ["models.providers.*.request.proxy.tls.cert"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.proxy.tls.cert", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.proxy.tls.key", + targetType: "models.providers.request.proxy.tls.key", + targetTypeAliases: ["models.providers.*.request.proxy.tls.key"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.proxy.tls.key", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.proxy.tls.passphrase", + targetType: "models.providers.request.proxy.tls.passphrase", + targetTypeAliases: ["models.providers.*.request.proxy.tls.passphrase"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.proxy.tls.passphrase", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.tls.ca", + targetType: "models.providers.request.tls.ca", + targetTypeAliases: ["models.providers.*.request.tls.ca"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.tls.ca", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.tls.cert", + targetType: "models.providers.request.tls.cert", + targetTypeAliases: ["models.providers.*.request.tls.cert"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.tls.cert", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.tls.key", + targetType: "models.providers.request.tls.key", + targetTypeAliases: ["models.providers.*.request.tls.key"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.tls.key", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, + { + id: "models.providers.*.request.tls.passphrase", + targetType: "models.providers.request.tls.passphrase", + targetTypeAliases: ["models.providers.*.request.tls.passphrase"], + configFile: "openclaw.json", + pathPattern: "models.providers.*.request.tls.passphrase", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + providerIdPathSegmentIndex: 2, + }, { id: "skills.entries.*.apiKey", targetType: "skills.entries.apiKey",