mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 23:14:31 +00:00
Secrets: harden SecretRef-safe models.json persistence (#38955)
This commit is contained in:
@@ -179,6 +179,28 @@ describe("buildInlineProviderModels", () => {
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves literal marker-shaped headers in inline provider models", () => {
|
||||
const providers: Parameters<typeof buildInlineProviderModels>[0] = {
|
||||
custom: {
|
||||
headers: {
|
||||
Authorization: "secretref-env:OPENAI_HEADER_TOKEN",
|
||||
"X-Managed": "secretref-managed",
|
||||
"X-Static": "tenant-a",
|
||||
},
|
||||
models: [makeModel("custom-model")],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildInlineProviderModels(providers);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].headers).toEqual({
|
||||
Authorization: "secretref-env:OPENAI_HEADER_TOKEN",
|
||||
"X-Managed": "secretref-managed",
|
||||
"X-Static": "tenant-a",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveModel", () => {
|
||||
@@ -223,6 +245,56 @@ describe("resolveModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves literal marker-shaped provider headers in fallback models", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "http://localhost:9000",
|
||||
headers: {
|
||||
Authorization: "secretref-env:OPENAI_HEADER_TOKEN",
|
||||
"X-Managed": "secretref-managed",
|
||||
"X-Custom-Auth": "token-123",
|
||||
},
|
||||
models: [makeModel("listed-model")],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveModel("custom", "missing-model", "/tmp/agent", cfg);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect((result.model as unknown as { headers?: Record<string, string> }).headers).toEqual({
|
||||
Authorization: "secretref-env:OPENAI_HEADER_TOKEN",
|
||||
"X-Managed": "secretref-managed",
|
||||
"X-Custom-Auth": "token-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("drops marker headers from discovered models.json entries", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "custom",
|
||||
modelId: "listed-model",
|
||||
templateModel: {
|
||||
...makeModel("listed-model"),
|
||||
provider: "custom",
|
||||
headers: {
|
||||
Authorization: "secretref-env:OPENAI_HEADER_TOKEN",
|
||||
"X-Managed": "secretref-managed",
|
||||
"X-Static": "tenant-a",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = resolveModel("custom", "listed-model", "/tmp/agent");
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect((result.model as unknown as { headers?: Record<string, string> }).headers).toEqual({
|
||||
"X-Static": "tenant-a",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers matching configured model metadata for fallback token limits", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ModelDefinitionConfig } from "../../config/types.js";
|
||||
import { resolveOpenClawAgentDir } from "../agent-paths.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
|
||||
import { buildModelAliasLines } from "../model-alias-lines.js";
|
||||
import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js";
|
||||
import { normalizeModelCompat } from "../model-compat.js";
|
||||
import { resolveForwardCompatModel } from "../model-forward-compat.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
|
||||
@@ -19,9 +20,29 @@ type InlineProviderConfig = {
|
||||
baseUrl?: string;
|
||||
api?: ModelDefinitionConfig["api"];
|
||||
models?: ModelDefinitionConfig[];
|
||||
headers?: Record<string, string>;
|
||||
headers?: unknown;
|
||||
};
|
||||
|
||||
function sanitizeModelHeaders(
|
||||
headers: unknown,
|
||||
opts?: { stripSecretRefMarkers?: boolean },
|
||||
): Record<string, string> | undefined {
|
||||
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
||||
return undefined;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const [headerName, headerValue] of Object.entries(headers)) {
|
||||
if (typeof headerValue !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (opts?.stripSecretRefMarkers && isSecretRefHeaderValueMarker(headerValue)) {
|
||||
continue;
|
||||
}
|
||||
next[headerName] = headerValue;
|
||||
}
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
export { buildModelAliasLines };
|
||||
|
||||
function resolveConfiguredProviderConfig(
|
||||
@@ -46,16 +67,23 @@ function applyConfiguredProviderOverrides(params: {
|
||||
}): Model<Api> {
|
||||
const { discoveredModel, providerConfig, modelId } = params;
|
||||
if (!providerConfig) {
|
||||
return discoveredModel;
|
||||
return {
|
||||
...discoveredModel,
|
||||
// Discovered models originate from models.json and may contain persistence markers.
|
||||
headers: sanitizeModelHeaders(discoveredModel.headers, { stripSecretRefMarkers: true }),
|
||||
};
|
||||
}
|
||||
const configuredModel = providerConfig.models?.find((candidate) => candidate.id === modelId);
|
||||
if (
|
||||
!configuredModel &&
|
||||
!providerConfig.baseUrl &&
|
||||
!providerConfig.api &&
|
||||
!providerConfig.headers
|
||||
) {
|
||||
return discoveredModel;
|
||||
const discoveredHeaders = sanitizeModelHeaders(discoveredModel.headers, {
|
||||
stripSecretRefMarkers: true,
|
||||
});
|
||||
const providerHeaders = sanitizeModelHeaders(providerConfig.headers);
|
||||
const configuredHeaders = sanitizeModelHeaders(configuredModel?.headers);
|
||||
if (!configuredModel && !providerConfig.baseUrl && !providerConfig.api && !providerHeaders) {
|
||||
return {
|
||||
...discoveredModel,
|
||||
headers: discoveredHeaders,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...discoveredModel,
|
||||
@@ -67,13 +95,13 @@ function applyConfiguredProviderOverrides(params: {
|
||||
contextWindow: configuredModel?.contextWindow ?? discoveredModel.contextWindow,
|
||||
maxTokens: configuredModel?.maxTokens ?? discoveredModel.maxTokens,
|
||||
headers:
|
||||
providerConfig.headers || configuredModel?.headers
|
||||
discoveredHeaders || providerHeaders || configuredHeaders
|
||||
? {
|
||||
...discoveredModel.headers,
|
||||
...providerConfig.headers,
|
||||
...configuredModel?.headers,
|
||||
...discoveredHeaders,
|
||||
...providerHeaders,
|
||||
...configuredHeaders,
|
||||
}
|
||||
: discoveredModel.headers,
|
||||
: undefined,
|
||||
compat: configuredModel?.compat ?? discoveredModel.compat,
|
||||
};
|
||||
}
|
||||
@@ -86,15 +114,22 @@ export function buildInlineProviderModels(
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
const providerHeaders = sanitizeModelHeaders(entry?.headers);
|
||||
return (entry?.models ?? []).map((model) => ({
|
||||
...model,
|
||||
provider: trimmed,
|
||||
baseUrl: entry?.baseUrl,
|
||||
api: model.api ?? entry?.api,
|
||||
headers:
|
||||
entry?.headers || (model as InlineModelEntry).headers
|
||||
? { ...entry?.headers, ...(model as InlineModelEntry).headers }
|
||||
: undefined,
|
||||
headers: (() => {
|
||||
const modelHeaders = sanitizeModelHeaders((model as InlineModelEntry).headers);
|
||||
if (!providerHeaders && !modelHeaders) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...providerHeaders,
|
||||
...modelHeaders,
|
||||
};
|
||||
})(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -161,6 +196,8 @@ export function resolveModelWithRegistry(params: {
|
||||
}
|
||||
|
||||
const configuredModel = providerConfig?.models?.find((candidate) => candidate.id === modelId);
|
||||
const providerHeaders = sanitizeModelHeaders(providerConfig?.headers);
|
||||
const modelHeaders = sanitizeModelHeaders(configuredModel?.headers);
|
||||
if (providerConfig || modelId.startsWith("mock-")) {
|
||||
return normalizeModelCompat({
|
||||
id: modelId,
|
||||
@@ -180,9 +217,7 @@ export function resolveModelWithRegistry(params: {
|
||||
providerConfig?.models?.[0]?.maxTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
headers:
|
||||
providerConfig?.headers || configuredModel?.headers
|
||||
? { ...providerConfig?.headers, ...configuredModel?.headers }
|
||||
: undefined,
|
||||
providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined,
|
||||
} as Model<Api>);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user