Skip to content

Add Zod runtime validation for agent output and IPC payloads #80

@gregpriday

Description

@gregpriday

Summary

Add runtime payload validation using Zod for agent output parsing and external IPC inputs to prevent malformed data from crashing UI components or causing silent failures.

Problem Statement

Currently, the codebase relies solely on TypeScript compile-time types for data validation. While this is sufficient for internal service communication, it leaves vulnerabilities at system boundaries:

  1. Agent output parsing: Claude/Gemini agents may return malformed JSON or unexpected data structures
  2. IPC boundaries: Renderer could send malformed payloads (though unlikely with TypeScript)
  3. External data sources: File contents, git output, package.json parsing

These can cause runtime errors like "Cannot read property of undefined" or silent failures where components render with incomplete data.

Context

The current type system provides excellent compile-time safety via:

  • CanopyEventMap with 33+ typed events
  • TypeScript interfaces for all data structures
  • Type guards in IPC handlers

However, runtime validation is missing at critical boundaries, especially for:

  • Agent state machine transitions based on terminal output
  • CopyTree progress parsing
  • AI summary generation responses

Deliverables

Code Changes

New Files to Create:

  • electron/schemas/agent.ts - Zod schemas for agent events and state
  • electron/schemas/ipc.ts - Zod schemas for IPC payloads
  • electron/schemas/external.ts - Zod schemas for external data (package.json, git output)

Files to Modify:

Implementation Details

1. Install Zod:

npm install zod

2. Create Agent Event Schemas:

// electron/schemas/agent.ts
import { z } from "zod";

export const AgentSpawnedSchema = z.object({
  agentId: z.string(),
  terminalId: z.string(),
  type: z.enum(["claude", "gemini", "shell", "custom"]),
  worktreeId: z.string().optional(),
  timestamp: z.number(),
});

export const AgentStateChangedSchema = z.object({
  agentId: z.string(),
  state: z.enum(["idle", "working", "waiting", "completed", "failed"]),
  previousState: z.enum(["idle", "working", "waiting", "completed", "failed"]),
  timestamp: z.number(),
});

export const AgentOutputSchema = z.object({
  agentId: z.string(),
  data: z.string(),
  timestamp: z.number(),
});

3. Validate at Boundaries:

// In PtyManager.ts
import { AgentOutputSchema } from "../schemas/agent.js";

ptyProcess.onData((data) => {
  const result = AgentOutputSchema.safeParse({
    agentId: id,
    data,
    timestamp: Date.now(),
  });
  
  if (!result.success) {
    console.error("Invalid agent output", result.error);
    return;
  }
  
  this.emit("data", id, result.data.data);
  // Continue with state machine...
});

4. IPC Payload Validation:

// In ipc/handlers.ts
import { TerminalSpawnOptionsSchema } from "../schemas/ipc.js";

const handleTerminalSpawn = async (event, options) => {
  const result = TerminalSpawnOptionsSchema.safeParse(options);
  
  if (!result.success) {
    throw new Error(`Invalid spawn options: ${result.error.message}`);
  }
  
  // Proceed with validated data
  const { cwd, shell, cols, rows } = result.data;
  // ...
};

Tests

  • Unit tests for schema validation (valid and invalid inputs)
  • Integration tests for IPC handlers with malformed payloads
  • Test agent output parsing with edge cases

Documentation

  • Document validation approach in architecture docs
  • Add JSDoc comments to schemas explaining each field
  • Update error handling documentation

Technical Specifications

Footprint:

  • New: electron/schemas/ directory (~3 files)
  • Modified: 4-5 existing service files
  • Dependencies: zod package (~10KB gzipped)

Performance Considerations:

  • Zod validation is very fast (<1ms for typical payloads)
  • Only validate at system boundaries, not internal events
  • Use safeParse() to avoid throwing exceptions

Validation Strategy:

  • High Priority: Agent outputs, AI responses, external data
  • Medium Priority: IPC payloads from renderer
  • Low Priority: Internal TypedEventBus events (already type-safe)

Dependencies

None - can be implemented independently

Tasks

Acceptance Criteria

  • All agent events are validated with Zod schemas
  • Critical IPC payloads are validated before processing
  • AI responses are validated before state updates
  • Invalid data triggers clear error messages (not generic "undefined" errors)
  • Performance impact is negligible (<5ms added latency)
  • Tests cover both valid and invalid inputs
  • No false positives (valid data rejected)

Edge Cases & Risks

  • Risk: Over-validation can add unnecessary overhead - only validate at boundaries
  • Risk: Schema drift from TypeScript types - consider using zod-to-ts or ts-to-zod for sync
  • Edge case: Unknown fields in payloads - use .passthrough() or .strict() strategically
  • Benefit: Catches bugs before they reach UI components
  • Benefit: Provides actionable error messages for debugging

Alternatives Considered

  1. io-ts: More functional programming style, steeper learning curve
  2. Joi: Larger bundle size, primarily for server-side validation
  3. AJV: JSON Schema-based, less TypeScript-native
  4. Custom validators: More code to maintain, less standardized

Why Zod: TypeScript-first, excellent type inference, small bundle, widely adopted in modern projects.

Additional Context

This is a preventative measure to improve system reliability. The current system hasn't experienced crashes from malformed data, but adding validation at boundaries is a best practice for production systems, especially when integrating with external AI agents.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions