From bcd758716cfaa8f16ef99bd5ed419c04e1693ea5 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 7 Aug 2026 20:12:03 +0900 Subject: [PATCH] fix(export): block env interpolation in model metadata --- src/clients/config-export.ts | 19 ++++++++++++++----- tests/client-config-new-clients.test.ts | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 0a4a84ed00..1ed91410e3 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -740,8 +740,15 @@ function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): R return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined; } +/** Do not let provider-controlled catalog text become an environment lookup. */ +function containsEnvInterpolation(value: string): boolean { + return value.includes("${"); +} + function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig { - const models = normalizeExportModels(ctx.models).map(model => model.namespaced); + const models = normalizeExportModels(ctx.models) + .filter(model => !containsEnvInterpolation(model.namespaced)) + .map(model => model.namespaced); const headers = proxyAdmissionHeaders(ctx.config, HERMES_API_KEY_ENV_REF); return { providers: { @@ -758,13 +765,15 @@ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig { } function buildOpenclawClientConfig(ctx: ExportContext): OpenclawGeneratedConfig { - const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).map(model => { + const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).flatMap(model => { + const name = exportModelLabel(model); + if (containsEnvInterpolation(model.namespaced) || containsEnvInterpolation(name)) return []; const context = authoritativeContextWindow(model.contextWindow); - return { + return [{ id: model.namespaced, - name: exportModelLabel(model), + name, ...(context !== undefined ? { contextWindow: context } : {}), - }; + }]; }); const headers = proxyAdmissionHeaders(ctx.config, OPENCLAW_API_KEY_ENV_REF); return { diff --git a/tests/client-config-new-clients.test.ts b/tests/client-config-new-clients.test.ts index 51fd6de1f4..01e359f249 100644 --- a/tests/client-config-new-clients.test.ts +++ b/tests/client-config-new-clients.test.ts @@ -103,6 +103,25 @@ describe("openclaw", () => { }); }); +describe("interpolation-capable clients", () => { + test("omit provider-controlled model text that could expand an environment variable", () => { + const models: ExportModel[] = [ + ...MODELS, + { namespaced: "evil/${SENSITIVE_ENV}", provider: "evil", id: "${SENSITIVE_ENV}" }, + { namespaced: "safe/id", provider: "safe", id: "id", displayName: "${SENSITIVE_ENV}" }, + ]; + const maliciousContext = { ...ctx(), models }; + + const hermes = buildClientConfig("hermes", maliciousContext) as HermesGeneratedConfig; + expect(hermes.providers[OPENCODE_PROVIDER_ID]!.models).not.toContain("evil/${SENSITIVE_ENV}"); + + const openclaw = buildClientConfig("openclaw", maliciousContext) as OpenclawGeneratedConfig; + expect(openclaw.models.providers[OPENCODE_PROVIDER_ID]!.models.map(model => model.id)).toEqual( + MODELS.map(model => model.namespaced).sort(), + ); + }); +}); + describe("kimi", () => { test("omits a model with no authoritative context window entirely", () => { const doc = buildClientConfig("kimi", ctx()) as KimiGeneratedConfig;