LayerZeroFault
ai agents-api

Fix: ElizaOS plugin-form Control-Type Registry Desynchronization Bug

VV

Written by

Fact-Checked on September 11, 2026

Verified Expert

Fix: ElizaOS plugin-form Control-Type Registry Desynchronization Bug

In autonomous agent architectures powered by ElizaOS (formerly ai16z), the Form Plugin (@elizaos/plugin-form) is tasked with collecting structured user data—such as decentralized identity (DID) credentials, whitelist registration addresses, KYC verification details, and scheduling timestamps.

Developers register custom control types to validate specific Web3 inputs (for example, verifying valid checksummed Ethereum addresses or ENS domains).

However, in production deployments, developers discover that custom validation rules, formatting functions, and extraction prompts are silently ignored:

[ElizaOS:FormService] Registered custom control type: "ethereum_address"
[ElizaOS:FormRunner] Processing field: "recipientAddress" (type: "ethereum_address")
[ElizaOS:Validation] Warning: No typeHandler found for "ethereum_address". Falling back to raw string passthrough!
[ElizaOS:Memory] Stored unvalidated recipientAddress: "not_a_valid_hex_string"

Furthermore, whenever users submit birth dates or scheduling deadlines, the saved date shifts by a full calendar day in agent memory.

Placeholder: ElizaOS Plugin-Form Architecture Dual Registry Desynchronization


1. Architectural Flaw: The Dual Registry Split

The defect lies in a architectural disconnect between packages/plugin-form/src/service.ts and packages/plugin-form/src/validation.ts.

Registry A: The Public API (FormService)

The documented public method in service.ts registers types into the service instance:

// packages/plugin-form/src/service.ts
export class FormService extends Service {
  private controlTypes: Map<string, ControlType> = new Map();

  public registerControlType(type: ControlType): void {
    // Populates service.controlTypes
    this.controlTypes.set(type.name, type);
  }
}

Registry B: The Validation Engine (validation.ts)

However, during runtime execution, the form extraction and parsing pipeline in validation.ts queries an entirely different module-scoped map:

// packages/plugin-form/src/validation.ts
const typeHandlers = new Map<string, TypeHandler>();

export function registerTypeHandler(name: string, handler: TypeHandler) {
  typeHandlers.set(name, handler);
}

export function validateField(field: FormField, value: unknown) {
  // Reads ONLY from typeHandlers!
  const handler = typeHandlers.get(field.controlType);
  if (!handler) {
    // Silently skips validation!
    return { valid: true, value };
  }
  return handler.validate(value);
}

Because registerTypeHandler is never invoked by FormService.registerControlType(), any custom control types registered by developers or built-in plugins exist solely in service.controlTypes and are never executed by validateField().

(For other serialization and type coercion bugs in ElizaOS, see our guide on ElizaOS JS Runtime Bridge False Cycle Serialization Fix).


2. The UTC Midnight Date Shift Defect

When parsing date fields, the built-in date type handler in plugin-form executes:

// VULNERABLE DATE PARSING
const parsedDate = new Date(userInput); // e.g. "2026-09-12"

According to the ECMAScript specification:

  • An ISO date-only string (YYYY-MM-DD) is parsed as UTC midnight: 2026-09-12T00:00:00.000Z.
  • When an agent hosted on a server in Tokyo (UTC+9) or Singapore (UTC+8) formats the date or queries parsedDate.toLocaleDateString(), the timestamp evaluates to 2026-09-12 09:00:00, which is fine.
  • But if the server runs in New York (UTC-4), UTC midnight corresponds to 2026-09-11 20:00:00 (the previous day!).
  • Conversely, when converting back to UTC from local strings, dates in eastern hemispheres shift forward by a day.

Placeholder: Date Timezone Shift Diagram Across UTC Offsets in JavaScript


3. Production Remediation Strategies

Strategy 1: Unify the Registries in FormService

If compiling from source or maintaining an internal fork of @elizaos/plugin-form, bridge registerControlType to invoke registerTypeHandler:

// packages/plugin-form/src/service.ts
import { registerTypeHandler } from './validation';

export class FormService extends Service {
  public registerControlType(type: ControlType): void {
    this.controlTypes.set(type.name, type);

    // CRITICAL FIX: Synchronize with the validation registry
    registerTypeHandler(type.name, {
      validate: type.validate,
      parse: type.parse,
      format: type.format,
      extractionPrompt: type.extractionPrompt,
    });
  }
}

Strategy 2: Timezone-Agnostic UTC Date Normalization

Replace the naive new Date() constructor with a deterministic date parser that preserves calendar dates regardless of server timezone:

// utils/safeDateParser.ts
export function parseCalendarDate(input: string): string {
  // Normalize string to match YYYY-MM-DD
  const match = input.trim().match(/^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/);
  if (!match) {
    throw new Error(`Invalid date format: ${input}. Expected YYYY-MM-DD.`);
  }

  const year = parseInt(match[1], 10);
  const month = parseInt(match[2], 10) - 1; // 0-indexed
  const day = parseInt(match[3], 10);

  // Construct date using explicit UTC constructor
  const utcDate = new Date(Date.UTC(year, month, day, 12, 0, 0)); // Noon UTC avoids midnight roll-over

  return utcDate.toISOString().split('T')[0]; // Guarantees YYYY-MM-DD
}

Strategy 3: Direct Registration Workaround (Without Forking)

If your application consumes @elizaos/plugin-form via npm and cannot modify internal files directly, import registerTypeHandler directly from the package’s subpath:

// bootstrap.ts
import { FormService } from '@elizaos/plugin-form';
import { registerTypeHandler } from '@elizaos/plugin-form/dist/validation';
import { isAddress } from 'viem';

// Register to both endpoints manually
const ethAddressControlType = {
  name: 'ethereum_address',
  validate: (val: string) => isAddress(val),
  parse: (val: string) => val.toLowerCase(),
  format: (val: string) => val,
};

formService.registerControlType(ethAddressControlType);
registerTypeHandler('ethereum_address', ethAddressControlType);

Placeholder: Architecture Diagram of Unified FormService and TypeHandlers Engine


4. Verification Unit Test

Validate that custom validators and date normalization execute correctly:

import { describe, it, expect } from 'vitest';
import { validateField } from './validation';
import { parseCalendarDate } from './safeDateParser';

describe('Form Validation & Date Shifting Fix', () => {
  it('enforces custom ethereum address validation', () => {
    const field = { name: 'wallet', controlType: 'ethereum_address' };

    const invalidResult = validateField(field, 'invalid_address');
    expect(invalidResult.valid).toBe(false);

    const validResult = validateField(field, '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
    expect(validResult.valid).toBe(true);
  });

  it('prevents calendar day roll-over across all timezones', () => {
    const rawDate = '2026-09-12';
    const normalized = parseCalendarDate(rawDate);
    expect(normalized).toBe('2026-09-12');
  });
});
Partner Spotlight: Gate.io

Trade Securely on Gate.io

Don't risk your assets on centralized silos or unverified endpoints. Trade securely on Gate.io with deep liquidity and institutional-grade security protocols.

Claim $100 Sign-up Bonus

Official Partner Referral Link

Related Inquiries

Why do custom control types registered via FormService.registerControlType() fail to validate in ElizaOS?

In @elizaos/plugin-form, the codebase maintains two isolated registries that never synchronize. The public API FormService.registerControlType() writes to service.controlTypes. However, the runtime extraction and validation engine in src/validation.ts reads exclusively from a separate, unexported typeHandlers map. Because nothing in the plugin bridges the two registries, registered validation routines, parsers, and custom extraction prompts are completely bypassed.

Why do date fields collected by ElizaOS agents shift by a day east or west?

When agents parse user responses like '1995-04-12' using native JavaScript new Date('1995-04-12'), the runtime evaluates the string in UTC midnight. When formatted back to the user or stored in the database in local server time, timezones east of UTC (e.g. UTC+3 to UTC+12) or west of UTC roll over to the previous or next calendar day.

How can developers bridge FormService and typeHandlers?

Developers must either export registerTypeHandler() from validation.ts and call it inside FormService.registerControlType(), or refactor validation.ts to accept the FormService instance and read directly from service.controlTypes.