LayerZeroFault
ai agents-api

ElizaOS plugin-form Control-Type Registry Desynchronization Bug

VV

Written by

Published:

Technical Guide

To resolve ElizaOS plugin-form Control-Type Registry Desynchronization Bug, resolve ElizaOS @elizaos/plugin-form validation bypass and UTC date shifting defects. Fix dual registry architecture between FormService and typeHandlers. Ensure all derivation paths, environment parameters, and cryptographic signatures are verified client-side using deterministic tools before broadcasting live transactions.

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.

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.

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);

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');
  });
});

[!TIP] Diagnostic Verification & Air-Gapped Testing: Before executing state changes or broad recovery steps, cross-reference failure parameters with our companion guide on Configure ElizaOS Plugin-Prflght for DeFi Slippage & Spending Limits and verify transaction calldata or cryptographic envelopes using the Master Web3 & RPC Error Code Directory.

Interactive Diagnostic Sandbox

Local Agent API & Calldata Sandboxes

Inspect autonomous agent UserOperations, decode packed execution calldata, and test programmatic authentication payloads locally.

Companion Diagnostic Guide ElizaOS JS Runtime Bridge False '[cycle]' Serialization Bug →

Resolve ElizaOS JS runtime bridge bug where shared DAG object references are serialized as '[cycle]', corrupting agent memory, token balances, and action parameters.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

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.