Skip to content

fix(validation): reject unknown keys in --global-arg and override --input flags#1312

Merged
stack72 merged 2 commits intomainfrom
worktree-244
May 5, 2026
Merged

fix(validation): reject unknown keys in --global-arg and override --input flags#1312
stack72 merged 2 commits intomainfrom
worktree-244

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented May 5, 2026

Summary

  • Unknown keys passed via --global-arg on model create (and in YAML definitions) are now rejected with a clear error instead of being silently stripped by Zod's default mode
  • Unknown override inputs passed via --input on model method run (keys not in the model's inputs.properties) are validated against the method schema and rejected with a clear error
  • All three global-argument validation sites (create, validateModel, executeWorkflow) use .strict() via a duck-typed cast for Zod v4 compatibility where .strict() only exists on ZodObject

Test plan

  • Unit tests in create_test.ts: unknown global arg key yields validation_failed error
  • Unit tests in validation_service_test.ts: validateModel rejects unknown global arg and method arg keys
  • Unit tests in run_test.ts: unknown override input key yields validation_failed error; known key succeeds
  • Unit tests in method_execution_service_test.ts: executeWorkflow rejects unknown global arg key
  • Integration tests in inputs_test.ts, vary_test.ts, workflow_partial_inputs_test.ts: workflow step inputs (CEL expression variables) forwarded as method arguments no longer trigger false positives
  • Repro: swamp model create test-strict bad-name --global-arg name=hello --global-arg typoKey=oops now errors; before this fix it silently succeeded

Fixes swamp-club#244

🤖 Generated with Claude Code

stack72 and others added 2 commits May 5, 2026 19:07
…nput flags

Zod's default strip mode was silently discarding unknown keys passed via
`--global-arg` on `model create` and via `--input` on `model method run`
(for inputs not matched by the model's inputs schema). This meant typos
in flag names gave no feedback and the incorrect key was quietly ignored.

Fix: use `.strict()` (via duck-typed cast for Zod v4 compatibility) at
all three global-argument validation sites (create, validateModel,
executeWorkflow) so unrecognised keys yield a clear error. Unknown
override-input keys are now detected in the libswamp run generator
before they reach the domain execution layer, which keeps the check
out of the workflow execution path where step inputs forwarded as
method arguments would cause false positives.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The core validation change is solid — applying .strict() to reject unknown keys in --global-arg and --input flags is the right fix. The duck-typed cast for Zod v4 compatibility is pragmatic, test coverage is thorough (both rejection and acceptance paths), and the domain layering is correct: schema validation stays in the domain services while the override-input key check lives in the libswamp orchestration layer where it belongs.

No blocking issues.

Suggestions

  1. Unrelated gitignore change bundled in (repo_service.ts): The .claude/.claude/worktrees/ + specific file paths change is a good improvement (allows committing .claude/skills/), but it's unrelated to the validation fix. Ideally it would be a separate commit/PR for cleaner history and easier bisection. Not a blocker since it's well-tested.

  2. Multi-line comment in run.ts:353-357: The 5-line block explaining override input semantics carries important context but could be condensed to 1-2 lines per CLAUDE.md conventions (e.g., // Reject unknown override inputs before Zod's strip mode silently discards them). The existing phrasing documents the "what" alongside the "why."

  3. Comment in method_execution_service_test.ts:352-353: The 2-line comment explaining why the test fixture was changed is useful context, but could be a single line: // Empty args: exercises Zod error path without triggering unknown-key rejection.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI UX Review

Blocking

None.

Suggestions

  1. model create --global-arg error has a confusing : prefix (src/libswamp/models/create.ts:172). The issue formatter uses ${i.path.join(".")}: ${i.message}. For regular Zod errors (e.g. missing required field), path is ["name"] name: Required. But Zod's unrecognized_keys issue has an empty path [], producing : Unrecognized key(s) in object: 'typoKey'. The leading : looks like broken output. The formatter pre-dates this PR but this is the first time an empty-path issue is produced here. Could guard with i.path.length > 0 ? ${i.path.join(".")}: : "" before the message.

  2. model create --global-arg error doesn't tell users what valid keys are. The model method run --input error helpfully adds Valid inputs are: key1, key2, but the --global-arg error only says what was wrong, not what's accepted. After seeing Unrecognized key(s) in object: 'typoKey', a user's next question is "ok, what keys are valid?" — they'd have to run swamp model type get to find out. Not a blocker (the fix itself is valuable), but the --input error's pattern is worth following.

Verdict

PASS — the validation tightening improves UX by surfacing previously silent failures with clear, actionable errors. Both new error paths (model create and model method run) tell users what went wrong. The .gitignore change is transparent to users.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None found.

Medium

  1. src/libswamp/models/run.ts:374-376in operator checks prototype chain, allowing prototype property names to bypass the unknown-key check.
    The override input validation uses !(k in methodSchema.shape!) to detect unknown keys. The in operator walks the prototype chain, so input keys that happen to match Object.prototype properties (constructor, toString, valueOf, hasOwnProperty, __proto__, etc.) would pass this check even though they aren't valid method arguments. They'd then be set as method arguments and silently stripped by Zod's non-strict safeParse in the execute path — the user gets no error for the typo.

    Breaking example: swamp model method run my-model run --input constructor=oops — no error is raised; the key is silently discarded.

    Suggested fix: Replace !(k in methodSchema.shape!) with !Object.hasOwn(methodSchema.shape!, k) (or !Object.prototype.hasOwnProperty.call(methodSchema.shape!, k)).

  2. src/libswamp/models/run.ts:370-372 and all 4 .strict() sites — silent fallback when schema is not a plain ZodObject.
    If a user extension defines globalArguments or method arguments via z.object({...}).refine(...) (or .transform(), .pipe()), the wrapping ZodEffects doesn't have .strict() or .shape. Both the .strict()?.() ?? fallback pattern and the .shape !== undefined guard silently degrade to the previous strip behavior. This means extensions using refinements don't benefit from the fix.

    This isn't a regression (they get the same behavior as before the PR), but it is a gap in coverage that may be worth documenting or addressing. The coerceMethodArgs utility already has an unwrapSchema function that unwraps effects/optionals to find the inner ZodObject — a similar approach could be used here to reach the inner object's .strict() or .shape.

Low

  1. src/domain/models/method_execution_service.ts:432-442hasUnresolved branch of executeWorkflow doesn't apply strict validation.
    When global args contain expression fields (${{ ... }}), the code takes the hasUnresolved branch which only coerces resolved fields and sets them directly — no safeParse() at all. Unknown keys among the resolved subset pass through unchecked. This is pre-existing behavior and not a regression, but it means an unknown key like --global-arg typoKey=oops alongside an expression-containing field would be silently accepted.

  2. Unrelated .gitignore change bundled with validation fix. The repo_service.ts change from .claude/ to specific file exclusions is a separate concern. It's correctly tested and the specific files chosen (worktrees, settings.local.json, scheduled_tasks.*) make sense for the stated goal of letting extension authors commit .claude/skills/. Just noting it's orthogonal to the validation fix.

Verdict

PASS — The core fix is sound: unknown keys in --global-arg and override --input flags are now rejected at all three validation sites (create, validateModel, executeWorkflow) and in the libswamp run generator. Tests cover the happy path, rejection path, and the workflow-step-inputs non-regression. The .strict() duck-typing pattern is a reasonable Zod v4 compatibility approach. The medium findings are real gaps but not regressions — they represent cases where the previous silent-strip behavior is preserved rather than improved.

@stack72 stack72 merged commit 7e3fc45 into main May 5, 2026
11 checks passed
@stack72 stack72 deleted the worktree-244 branch May 5, 2026 18:25
stack72 added a commit that referenced this pull request May 5, 2026
Three follow-ups from the #1312 adversarial review:

1. Prototype-chain bypass: the unknown-key check used `k in shape` which
   walks Object.prototype, so flag keys like `--input constructor=oops`
   bypassed the check. Switch to `Object.hasOwn(shape, k)`.

2. ZodEffects not unwrapped: schemas defined with `.refine()`,
   `.transform()`, or `.pipe()` lack `.strict()` and `.shape` on the
   outer wrapper, so the previous duck-typed cast silently fell back to
   strip mode. Introduce `getObjectShape()` that uses the existing
   `unwrapSchema` helper to find the inner ZodObject's shape, then do an
   explicit unknown-key check against it. The original schema then
   handles normal validation, preserving refinements/transforms.

3. Zod v3 vs v4 internals: extensions may import `npm:zod@3` (where
   `_def.typeName` is e.g. `"ZodObject"` and `_def.shape` is a function)
   or `npm:zod@4` (where `_def.type` is `"object"` and `_def.shape` is
   a value). Normalize both in `getSchemaType()` and `readShape()`.

Also fixes the CLI UX issue from the #1312 review: the `model create`
error no longer shows a leading `: ` prefix because the unknown-key
error is now produced explicitly with a clear message naming valid
arguments, instead of via Zod's empty-path `unrecognized_keys` issue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant