Skip to content

fix(core): guard non-Zod memory schema instead of silently casting it#279

Merged
blove merged 2 commits into
mainfrom
blove/memory-schema-guard
Jun 27, 2026
Merged

fix(core): guard non-Zod memory schema instead of silently casting it#279
blove merged 2 commits into
mainfrom
blove/memory-schema-guard

Conversation

@blove

@blove blove commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up addressing the advisory review feedback carried over from #257 (it merged before these were applied).

  1. memory.ts — runtime guard instead of a silent cast. The remember tool's data shape comes from context.memory.schema, which arrives as unknown (loaded via dynamic import() in loadRouteMemory, validated structurally). The old (mem.schema ?? …) as z.ZodTypeAny cast was silent — a non-Zod value (a plain JSON-Schema object, etc.) was handed straight to z.object() and blew up with an opaque "expected a Zod schema" error at tool-schema use time. Now an isZodSchema check guards it and falls back to the permissive record when the value isn't a real Zod type.

  2. runtime-exports.ts — documents runMemoryCommand as a test-only surface (supports @dawn-ai/testing's live-smoke), not a stable public API.

Why a runtime guard, not the literal type-narrow the reviewer suggested

The review asked to narrow MemoryContext.schema?: unknownZodTypeAny. That's cosmetic here: the value genuinely originates as unknown from a dynamic import, so narrowing the field just relocates the as ZodTypeAny cast to the setter (buildMemoryContext, where LoadedRouteMemory.schema is unknown) without adding real safety. The actual concern — an opaque runtime blowup — is a runtime problem, so the fix is a runtime guard at the use site.

Tests

packages/core/test/capabilities/memory.test.ts:

  • falls back to a permissive data schema when context.memory.schema is not a Zod type — fails before the guard (the non-Zod value reaches z.object() and safeParse throws "expected a Zod schema"), passes after.
  • uses the route's Zod schema as the remember data shape when provided — confirms the happy path still wires the real schema through (a wrong-typed field is rejected).

Validation

  • @dawn-ai/core 190 ✓, @dawn-ai/cli 278 ✓; build/typecheck green; biome clean (exit 0)
  • patch changeset (fixed group, stays pre-1.0)

🤖 Generated with Claude Code

The remember tool's `data` shape comes from `context.memory.schema`, which
arrives as `unknown` (loaded via dynamic import, validated structurally). The
previous `as z.ZodTypeAny` cast was silent: a non-Zod value (absent, or a
plain JSON-Schema object) was handed to `z.object()` and blew up with an
opaque "expected a Zod schema" error at tool-schema use time. Guard it with an
`isZodSchema` check and fall back to a permissive record instead.

Also documents `runMemoryCommand`'s `@dawn-ai/cli/runtime` export as a
test-only surface, not a stable public API.

Addresses the advisory review feedback carried over from #257.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@blove blove enabled auto-merge (squash) June 27, 2026 22:18
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dawnai Ready Ready Preview, Comment Jun 27, 2026 10:24pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 1m 11s —— View job


Code Review

  • Gather context
  • Read changed files
  • Analyze diff
  • Post inline comments
  • Summarize findings

Overall: looks good. The runtime guard is the right fix — as the PR description explains, narrowing MemoryContext.schema to ZodTypeAny at the type level would just relocate the as cast to the setter without eliminating the runtime risk. The duck-type check on safeParse is the conventional Zod guard and is correct here. Tests are targeted and cover both the fallback and the happy path.

Two minor findings (both inline):

  1. isZodSchema defined inside load() — it has no closure dependencies and gets re-declared on every call. Hoisting it to module scope is a one-line improvement. Not a bug. (line 35)

  2. MemoryContext type doesn't model schema — the test has to cast ctx.memory as { schema?: unknown } to inject the field, which confirms the TypeScript type doesn't reflect this runtime-populated field. The PR correctly treats it as unknown at the use site, but adding schema?: unknown to MemoryContext would close the loop. Worth a follow-up. (line 107)

Nothing blocking. The changeset classification (patch) and the runtime-exports.ts doc comment are both appropriate.

Comment thread packages/core/src/capabilities/built-in/memory.ts Outdated
Comment thread packages/core/test/capabilities/memory.test.ts
The guard helper has no closure dependencies and load() runs per-request, so
keep it module-scoped rather than recreating it each call. Addresses review
feedback on #279.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 1m 51s —— View job


Code Review

  • Gather context
  • Read CLAUDE.md for repo guidelines
  • Read changed files and diff
  • Analyze for correctness, security, type-safety, test coverage
  • Post inline comments
  • Summarize findings

Both findings from the prior review are now resolved. Nothing blocking here.

Previous finding #1isZodSchema scope: Addressed in commit e4282d4. The function is now at module scope (memory.ts:11–14) and is not recreated on each load() call. ✓

Previous finding #2MemoryContext type: The prior review stated the type "doesn't currently model the schema field." That observation was inaccurate — MemoryContext.schema?: unknown was already present on main before this PR (packages/core/src/capabilities/types.ts:46). The as { schema?: unknown } cast in the test is needed to bypass the readonly modifier for injection in tests, not because the field is absent from the type.

Guard correctness: The safeParse-presence duck-type check (memory.ts:11–14) is the standard approach for cross-package Zod detection (avoids instanceof brittleness across module boundaries). The fallback to z.record(z.string(), z.unknown()) handles both the undefined case (mem.schema is optional) and non-Zod values correctly.

Test coverage: Both paths are covered — fallback for a plain JSON-Schema object, and the happy path confirming the real schema rejects wrong-typed fields. Targeted and sufficient for the change.

No issues found — no correctness bugs, security problems, or type-safety gaps introduced by this PR.

@blove blove merged commit f195096 into main Jun 27, 2026
7 checks passed
@blove blove deleted the blove/memory-schema-guard branch June 27, 2026 22:34
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