From 6b1d3aaff646436a9e406f11bd68055eead0a44f Mon Sep 17 00:00:00 2001 From: jariy17 Date: Tue, 22 Sep 2026 17:19:29 +0000 Subject: [PATCH] fix(flags): reject unknown keys in schema-backed JSON flags Zod strips keys a schema does not declare, so a misspelled key in a JSON flag body parses cleanly and then vanishes. On an update path the surviving object replaces the stored configuration, so the typo destroys the value the user was trying to edit rather than reporting an error. The typo cannot be caught later either: the AWS SDK serializes only the members its own schema declares, so the bad key never reaches the service and no server-side validation can see it. Applying strict in the helper rather than at each schema definition means a schema author cannot forget it, and leaves the schemas in projectSchemas/ untouched. Verified against the real CLI. `project add evaluator llm-as-a-judge --rating-scale '{"numerical":[{"value":1,"label":"good","definition":"d"}], "bogus":1}'` previously printed "added evaluator 'e9'" and wrote the evaluator with "bogus" silently dropped; it now fails with `Unrecognized key: "bogus"` and writes nothing. --- src/handlers/utils.test.tsx | 45 +++++++++++++++++++++++++++++++++++++ src/handlers/utils.tsx | 9 ++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/handlers/utils.test.tsx b/src/handlers/utils.test.tsx index 52ba6071b..5d931b479 100644 --- a/src/handlers/utils.test.tsx +++ b/src/handlers/utils.test.tsx @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; +import z from "zod"; import { assertMutuallyExclusiveFlags, parseJsonArrayFlag, + parseJsonFlagWithSchema, parseJsonObjectFlag, parseTags, } from "./utils"; @@ -19,6 +21,49 @@ describe("structured JSON flags", () => { }); }); +describe("parseJsonFlagWithSchema", () => { + const ModelSchema = z + .object({ modelId: z.string(), apiKeyArn: z.string().optional() }) + .refine((m) => m.modelId !== "needs-key" || m.apiKeyArn !== undefined, { + message: "apiKeyArn is required", + }); + + test("returns undefined when the flag is omitted", () => { + expect(parseJsonFlagWithSchema("model", undefined, ModelSchema)).toBeUndefined(); + }); + + test("returns valid input unchanged", () => { + expect(parseJsonFlagWithSchema("model", '{"modelId":"anthropic.x"}', ModelSchema)).toEqual({ + modelId: "anthropic.x", + }); + }); + + // The F17 case: without the strict pass this parses, drops `modlId`, and sends + // an object missing the field the user was trying to set. + test("rejects a key the schema does not declare instead of dropping it", () => { + expect(() => + parseJsonFlagWithSchema("model", '{"modelId":"anthropic.x","modlId":"typo"}', ModelSchema), + ).toThrow("model"); + }); + + test("keeps the schema's own refinements", () => { + expect(() => parseJsonFlagWithSchema("model", '{"modelId":"needs-key"}', ModelSchema)).toThrow( + "apiKeyArn is required", + ); + }); + + // A record accepts any key by definition, so there is nothing to reject and the + // strict pass must leave it alone. + test("leaves a record schema permissive", () => { + const tags = parseJsonFlagWithSchema( + "tags", + '{"env":"prod"}', + z.record(z.string(), z.string()), + ); + expect(tags).toEqual({ env: "prod" }); + }); +}); + describe("parseTags", () => { test("returns undefined for undefined input", () => { expect(parseTags(undefined)).toBeUndefined(); diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index 82d18ae39..fda58f136 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useEffect } from "react"; import type { Context } from "../router"; -import type z from "zod"; +import z from "zod"; import type { CoreOptions } from "../core/types"; import type { AppIO } from "../io"; import { AgentCoreCLIError, InputValidationError, SilentCLIError } from "../errors"; @@ -69,7 +69,7 @@ export function parseJsonFlagWithSchema( const parsed = parseJsonFlag(name, raw); if (parsed === undefined) return undefined; - const result = schema.safeParse(parsed); + const result = strictened(schema).safeParse(parsed); if (!result.success) { throw new InputValidationError( `Invalid value for option '--${name}': ${formatZodError(result.error)}`, @@ -79,6 +79,11 @@ export function parseJsonFlagWithSchema( return result.data; } +// Top level only: a nested typo (model.bedrockModelConfig.modelIdd) still slips through. +function strictened(schema: z.ZodType): z.ZodType { + return schema instanceof z.ZodObject ? (schema.strict() as unknown as z.ZodType) : schema; +} + export function parseJsonObjectFlag(name: string, raw: string): T; export function parseJsonObjectFlag( name: string,