mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -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
@@ -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>;
|
||||
|
||||
@@ -182,7 +182,7 @@ export class CodexMcpClient {
|
||||
params: z.object({
|
||||
msg: z.any()
|
||||
})
|
||||
}).passthrough();
|
||||
});
|
||||
|
||||
const setNotificationHandler =
|
||||
this.client.setNotificationHandler.bind(this.client) as (
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -7,8 +7,11 @@ const machineMetadataSchema = z.object({
|
||||
host: z.string().optional(),
|
||||
platform: z.string().optional(),
|
||||
happyCliVersion: z.string().optional(),
|
||||
displayName: z.string().optional()
|
||||
}).passthrough()
|
||||
displayName: z.string().optional(),
|
||||
homeDir: z.string().optional(),
|
||||
happyHomeDir: z.string().optional(),
|
||||
happyLibDir: z.string().optional()
|
||||
})
|
||||
|
||||
export interface Machine {
|
||||
id: string
|
||||
@@ -23,7 +26,9 @@ export interface Machine {
|
||||
platform: string
|
||||
happyCliVersion: string
|
||||
displayName?: string
|
||||
[key: string]: unknown
|
||||
homeDir?: string
|
||||
happyHomeDir?: string
|
||||
happyLibDir?: string
|
||||
} | null
|
||||
metadataVersion: number
|
||||
runnerState: unknown | null
|
||||
@@ -88,12 +93,15 @@ export class MachineCache {
|
||||
const metadata = (() => {
|
||||
const parsed = machineMetadataSchema.safeParse(stored.metadata)
|
||||
if (!parsed.success) return null
|
||||
const data = parsed.data as Record<string, unknown>
|
||||
const data = parsed.data
|
||||
const host = typeof data.host === 'string' ? data.host : 'unknown'
|
||||
const platform = typeof data.platform === 'string' ? data.platform : 'unknown'
|
||||
const happyCliVersion = typeof data.happyCliVersion === 'string' ? data.happyCliVersion : 'unknown'
|
||||
const displayName = typeof data.displayName === 'string' ? data.displayName : undefined
|
||||
return { host, platform, happyCliVersion, displayName, ...data }
|
||||
const homeDir = typeof data.homeDir === 'string' ? data.homeDir : undefined
|
||||
const happyHomeDir = typeof data.happyHomeDir === 'string' ? data.happyHomeDir : undefined
|
||||
const happyLibDir = typeof data.happyLibDir === 'string' ? data.happyLibDir : undefined
|
||||
return { host, platform, happyCliVersion, displayName, homeDir, happyHomeDir, happyLibDir }
|
||||
})()
|
||||
|
||||
const storedActiveAt = stored.activeAt ?? stored.createdAt
|
||||
|
||||
@@ -15,7 +15,7 @@ export const WorktreeMetadataSchema = z.object({
|
||||
name: z.string(),
|
||||
worktreePath: z.string().optional(),
|
||||
createdAt: z.number().optional()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type WorktreeMetadata = z.infer<typeof WorktreeMetadataSchema>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const MetadataSchema = z.object({
|
||||
archiveReason: z.string().optional(),
|
||||
flavor: z.string().nullish(),
|
||||
worktree: WorktreeMetadataSchema.optional()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type Metadata = z.infer<typeof MetadataSchema>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AgentStateRequestSchema = z.object({
|
||||
tool: z.string(),
|
||||
arguments: z.unknown(),
|
||||
createdAt: z.number().nullish()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type AgentStateRequest = z.infer<typeof AgentStateRequestSchema>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const AgentStateCompletedRequestSchema = z.object({
|
||||
decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).optional(),
|
||||
allowTools: z.array(z.string()).optional(),
|
||||
answers: z.record(z.string(), z.array(z.string())).optional()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type AgentStateCompletedRequest = z.infer<typeof AgentStateCompletedRequestSchema>
|
||||
|
||||
@@ -76,7 +76,7 @@ export const AgentStateSchema = z.object({
|
||||
controlledByUser: z.boolean().nullish(),
|
||||
requests: z.record(z.string(), AgentStateRequestSchema).nullish(),
|
||||
completedRequests: z.record(z.string(), AgentStateCompletedRequestSchema).nullish()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type AgentState = z.infer<typeof AgentStateSchema>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const TodoItemSchema = z.object({
|
||||
status: z.enum(['pending', 'in_progress', 'completed']),
|
||||
priority: z.enum(['high', 'medium', 'low']),
|
||||
id: z.string()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type TodoItem = z.infer<typeof TodoItemSchema>
|
||||
|
||||
@@ -108,7 +108,7 @@ export const DecryptedMessageSchema = z.object({
|
||||
localId: z.string().nullable(),
|
||||
content: z.unknown(),
|
||||
createdAt: z.number()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type DecryptedMessage = z.infer<typeof DecryptedMessageSchema>
|
||||
|
||||
@@ -129,7 +129,7 @@ export const SessionSchema = z.object({
|
||||
todos: TodosSchema.optional(),
|
||||
permissionMode: PermissionModeSchema.optional(),
|
||||
modelMode: ModelModeSchema.optional()
|
||||
}).passthrough()
|
||||
})
|
||||
|
||||
export type Session = z.infer<typeof SessionSchema>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user