diff --git a/src/config/config.secrets-schema.test.ts b/src/config/config.secrets-schema.test.ts index bd5762d8fe0..dd20b23cd9a 100644 --- a/src/config/config.secrets-schema.test.ts +++ b/src/config/config.secrets-schema.test.ts @@ -56,4 +56,54 @@ describe("config secret refs schema", () => { ).toBe(true); } }); + + it("rejects env refs that are not env var names", () => { + const result = validateConfigObjectRaw({ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: { source: "env", id: "/providers/openai/apiKey" }, + models: [{ id: "gpt-5", name: "gpt-5" }], + }, + }, + }, + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect( + result.issues.some( + (issue) => + issue.path.includes("models.providers.openai.apiKey") && + issue.message.includes("Env secret reference id"), + ), + ).toBe(true); + } + }); + + it("rejects file refs that are not absolute JSON pointers", () => { + const result = validateConfigObjectRaw({ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: { source: "file", id: "providers/openai/apiKey" }, + models: [{ id: "gpt-5", name: "gpt-5" }], + }, + }, + }, + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect( + result.issues.some( + (issue) => + issue.path.includes("models.providers.openai.apiKey") && + issue.message.includes("absolute JSON pointer"), + ), + ).toBe(true); + } + }); }); diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 052f3a47a58..ccb8666e6f1 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -3,20 +3,48 @@ import { isSafeExecutableValue } from "../infra/exec-safety.js"; import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js"; import { sensitive } from "./zod-schema.sensitive.js"; -const SECRET_REF_ID_PATTERN = /^[A-Za-z0-9_./:=-](?:[A-Za-z0-9_./:=~-]{0,127})$/; +const ENV_SECRET_REF_ID_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/; +const FILE_SECRET_REF_SEGMENT_PATTERN = /^(?:[^~]|~0|~1)*$/; -export const SecretRefSchema = z +function isValidFileSecretRefId(value: string): boolean { + if (!value.startsWith("/")) { + return false; + } + return value + .slice(1) + .split("/") + .every((segment) => FILE_SECRET_REF_SEGMENT_PATTERN.test(segment)); +} + +const EnvSecretRefSchema = z .object({ - source: z.enum(["env", "file"]), + source: z.literal("env"), id: z .string() .regex( - SECRET_REF_ID_PATTERN, - "Secret reference id must match /^[A-Za-z0-9_./:=-](?:[A-Za-z0-9_./:=~-]{0,127})$/", + ENV_SECRET_REF_ID_PATTERN, + 'Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: "OPENAI_API_KEY").', ), }) .strict(); +const FileSecretRefSchema = z + .object({ + source: z.literal("file"), + id: z + .string() + .refine( + isValidFileSecretRefId, + 'File secret reference id must be an absolute JSON pointer (example: "/providers/openai/apiKey").', + ), + }) + .strict(); + +export const SecretRefSchema = z.discriminatedUnion("source", [ + EnvSecretRefSchema, + FileSecretRefSchema, +]); + export const SecretInputSchema = z.union([z.string(), SecretRefSchema]); const SecretsEnvSourceSchema = z