refactor: remove passthrough() from zod schemas and make validation explicit

- Replace .passthrough() with explicit field definitions across all schemas
- Add missing optional fields (homeDir, happyHomeDir, happyLibDir, displayName)
- Refactor RawJSONLinesSchema to use structured base schema for clarity
- Improve schema validation strictness and type safety
- Update Machine interface to reflect explicit fields instead of index signature
This commit is contained in:
weishu
2026-01-23 16:04:37 +08:00
parent 6f8f44a314
commit 6567f22978
6 changed files with 82 additions and 50 deletions
+6 -5
View File
@@ -29,10 +29,11 @@ export const MachineMetadataSchema = z.object({
host: z.string(),
platform: z.string(),
happyCliVersion: z.string(),
displayName: z.string().optional(),
homeDir: z.string(),
happyHomeDir: z.string(),
happyLibDir: z.string()
}).passthrough()
})
export type MachineMetadata = z.infer<typeof MachineMetadataSchema>
@@ -43,7 +44,7 @@ export const RunnerStateSchema = z.object({
startedAt: z.number().optional(),
shutdownRequestedAt: z.number().optional(),
shutdownSource: z.union([z.enum(['mobile-app', 'cli', 'os-signal', 'unknown']), z.string()]).optional()
}).passthrough()
})
export type RunnerState = z.infer<typeof RunnerStateSchema>
@@ -119,7 +120,7 @@ export const MessageMetaSchema = z.object({
appendSystemPrompt: z.string().nullable().optional(),
allowedTools: z.array(z.string()).nullable().optional(),
disallowedTools: z.array(z.string()).nullable().optional()
}).passthrough()
})
export type MessageMeta = z.infer<typeof MessageMetaSchema>
@@ -132,7 +133,7 @@ export const UserMessageSchema = z.object({
}),
localKey: z.string().optional(),
meta: MessageMetaSchema.optional()
}).passthrough()
})
export type UserMessage = z.infer<typeof UserMessageSchema>
@@ -143,7 +144,7 @@ export const AgentMessageSchema = z.object({
data: z.unknown()
}),
meta: MessageMetaSchema.optional()
}).passthrough()
})
export type AgentMessage = z.infer<typeof AgentMessageSchema>
+53 -30
View File
@@ -1,6 +1,6 @@
/**
* Simplified schema that only validates fields actually used in the codebase
* while preserving all other fields through passthrough()
* Schema validates fields used in the codebase and keeps explicit
* log fields required by the CLI and UI.
*/
import { z } from "zod";
@@ -12,46 +12,69 @@ export const UsageSchema = z.object({
cache_read_input_tokens: z.number().int().nonnegative().optional(),
output_tokens: z.number().int().nonnegative(),
service_tier: z.string().optional(),
}).passthrough();
});
// Main schema with minimal validation for only the fields we use
// NOTE: Schema is intentionally lenient to handle various Claude Code message formats
// including synthetic error messages, API errors, and different SDK versions
const RawMessageSchema = z.object({
role: z.string().optional(),
content: z.unknown(),
usage: UsageSchema.optional(),
});
const RawJSONLinesBaseSchema = z.object({
uuid: z.string().optional(),
parentUuid: z.string().nullable().optional(),
isSidechain: z.boolean().optional(),
isMeta: z.boolean().optional(),
isCompactSummary: z.boolean().optional(),
userType: z.string().optional(),
cwd: z.string().optional(),
sessionId: z.string().optional(),
version: z.string().optional(),
gitBranch: z.string().optional(),
timestamp: z.string().optional(),
});
// Main schema with validation for the fields used in the app
// NOTE: Schema remains lenient on message content to handle SDK variations
export const RawJSONLinesSchema = z.discriminatedUnion("type", [
// User message - validates uuid and message.content
z.object({
RawJSONLinesBaseSchema.extend({
type: z.literal("user"),
isSidechain: z.boolean().optional(),
isMeta: z.boolean().optional(),
uuid: z.string(), // Used in getMessageKey()
message: z.object({
content: z.union([z.string(), z.any()]) // Used in sessionScanner.ts
}).passthrough()
}).passthrough(),
uuid: z.string(),
message: RawMessageSchema,
mode: z.string().optional(),
toolUseResult: z.unknown().optional(),
}),
// Assistant message - only validates uuid and type
// message object is optional to handle synthetic error messages (isApiErrorMessage: true)
// which may have different structure than normal assistant messages
z.object({
// message object is optional to handle synthetic error messages
RawJSONLinesBaseSchema.extend({
uuid: z.string(),
type: z.literal("assistant"),
message: z.object({
usage: UsageSchema.optional(), // Used in apiSession.ts
}).passthrough().optional()
}).passthrough(),
message: RawMessageSchema.optional(),
requestId: z.string().optional(),
}),
// Summary message - validates summary and leafUuid
z.object({
RawJSONLinesBaseSchema.extend({
type: z.literal("summary"),
summary: z.string(), // Used in apiSession.ts
leafUuid: z.string() // Used in getMessageKey()
}).passthrough(),
summary: z.string(),
leafUuid: z.string(),
}),
// System message - validates uuid
z.object({
// System message - validates uuid and subtype data used by the UI
RawJSONLinesBaseSchema.extend({
type: z.literal("system"),
uuid: z.string() // Used in getMessageKey()
}).passthrough()
uuid: z.string(),
subtype: z.string().optional(),
model: z.string().optional(),
tools: z.array(z.string()).optional(),
session_id: z.string().optional(),
retryAttempt: z.number().optional(),
maxRetries: z.number().optional(),
error: z.unknown().optional(),
durationMs: z.number().optional(),
}),
]);
export type RawJSONLines = z.infer<typeof RawJSONLinesSchema>
export type RawJSONLines = z.infer<typeof RawJSONLinesSchema>;
+1 -1
View File
@@ -182,7 +182,7 @@ export class CodexMcpClient {
params: z.object({
msg: z.any()
})
}).passthrough();
});
const setNotificationHandler =
this.client.setNotificationHandler.bind(this.client) as (
+1 -1
View File
@@ -6,7 +6,7 @@ const CodexSessionEventSchema = z.object({
timestamp: z.string().optional(),
type: z.string(),
payload: z.unknown().optional()
}).passthrough();
});
export type CodexSessionEvent = z.infer<typeof CodexSessionEventSchema>;