← Back to blog

OpenClaw Dual-Schema Analysis

·OpenClaw
openclawdatabasearchitecture

TL;DR: OpenClaw uses two validation systems that never talk to each other—Zod for config files, TypeBox for WebSocket RPCs—and session metadata has no validation at all. This comprehensive breakdown reveals where the gaps are: arbitrary properties can be injected into sessions, health data passes through unchecked, and different LLM providers require incompatible JSON schemas that get normalized at runtime. If you're building on OpenClaw or auditing it, this is your schema reference.

OpenClaw Schema System -- Comprehensive Analysis

Table of Contents

  1. Schema Architecture Overview
  2. Zod Config Schemas
  3. TypeBox Protocol Schemas
  4. Plugin Schema System
  5. SQLite Memory Schema
  6. Session Metadata Types
  7. Validation Infrastructure
  8. Schema Gaps and Security Implications
  9. Provider-Specific Schema Normalization

1. Schema Architecture Overview

OpenClaw uses a dual-schema architecture with two distinct validation libraries serving different layers of the system:

Two Schema Systems

SystemLibraryPurposeValidation Style
Config SchemasZodYAML/JSON5 config file validationParse-time, strict, with transforms and refinements
Protocol SchemasTypeBox (via @sinclair/typebox)WebSocket RPC parameter validationAJV-compiled JSON Schema, runtime type guards

Validation Chain

Config File (YAML/JSON5)
  |
  v
[1] Zod: OpenClawSchema.safeParse()        -- src/config/validation.ts
  |   - Legacy migration check
  |   - Full Zod parse with .strict() enforcement
  |   - Cross-field refinements (superRefine)
  |   - Sensitive field registration
  |
  v
[2] Post-Zod Validation                    -- src/config/validation.ts
  |   - Duplicate agent directory check
  |   - Identity avatar path-traversal check
  |   - Plugin manifest validation (AJV via schema-validator.ts)
  |   - Heartbeat target validation
  |
  v
[3] Defaults Application                   -- src/config/defaults.ts
  |   - applySessionDefaults()
  |   - applyAgentDefaults()
  |   - applyModelDefaults()
  |
  v
Runtime Config Object
  |
  +--> Gateway WebSocket Server
         |
         v
[4] TypeBox/AJV: Per-method param validation  -- src/gateway/protocol/index.ts
         |   - ajv.compile() for each RPC method
         |   - assertValidParams() guard in handlers
         |
         v
       Handler executes

Key Architectural Observations

  • No shared schema definition: Zod and TypeBox schemas are defined independently. There is no mechanism to generate one from the other or ensure they stay in sync.
  • Strict mode everywhere in Zod: Nearly every object uses .strict(), which rejects unknown properties. Exception: ChannelsSchema uses .passthrough() for extension channels, and HookConfigSchema uses .passthrough() for per-hook custom keys.
  • Sensitive field registry: Zod schemas use .register(sensitive) to mark fields for automatic redaction when config is exposed to dashboards.
  • Session metadata has NO runtime validation: SessionEntry is a plain TypeScript interface with no Zod or TypeBox schema (security gap #18).

2. Zod Config Schemas

2.1 Root Schema -- zod-schema.ts

File: src/config/zod-schema.ts Export: OpenClawSchema

The root schema assembles all sub-schemas into the top-level OpenClaw configuration object. Uses .strict() at every level and .superRefine() for cross-field validation of broadcast agent IDs.

Top-Level FieldSchema SourceRequired
$schemaz.string()optional
metainline (lastTouchedVersion, lastTouchedAt)optional
envinline (shellEnv, vars) -- uses .catchall(z.string())optional
wizardinline (lastRunAt, lastRunVersion, etc.)optional
diagnosticsinline (enabled, flags, otel, cacheTrace)optional
logginginline (level, file, consoleLevel, consoleStyle, redact)optional
updateinline (channel: stable/beta/dev, checkOnStart)optional
browserinline (38+ fields across profiles, ssrfPolicy, etc.)optional
uiinline (seamColor, assistant name/avatar)optional
authinline (profiles, order, cooldowns)optional
modelsModelsConfigSchema from coreoptional
nodeHostinline (browserProxy)optional
agentsAgentsSchema from agentsoptional
toolsToolsSchema from agent-runtimeoptional
bindingsBindingsSchema from agentsoptional
broadcastBroadcastSchema from agentsoptional
audioAudioSchema from agentsoptional
mediainline (preserveFilenames)optional
messagesMessagesSchema from sessionoptional
commandsCommandsSchema from sessionoptional
approvalsApprovalsSchema from approvalsoptional
sessionSessionSchema from sessionoptional
croninline (enabled, store, maxConcurrentRuns, webhook)optional
hooksinline + HookMappingSchema, HooksGmailSchema, InternalHooksSchemaoptional
webinline (enabled, heartbeat, reconnect)optional
channelsChannelsSchema from providersoptional
discoveryinline (wideArea, mdns)optional
canvasHostinline (enabled, root, port, liveReload)optional
talkinline (voiceId, modelId, apiKey sensitive)optional
gatewayinline (60+ fields across auth, tls, remote, reload, http)optional
memoryinline (backend: builtin/qmd, citations, qmd config)optional
skillsinline (allowBundled, load, install, limits, entries)optional
pluginsinline (enabled, allow, deny, load, slots, entries, installs)optional

Cross-field refinement (superRefine):

  • Validates that every agent ID referenced in broadcast mappings exists in agents.list

Notable validation patterns:

  • HttpUrlSchema: z.string().url().refine() -- ensures http:// or https:// protocol
  • Memory searchMode: union of "query", "search", "vsearch"
  • MemoryQmdLimitsSchema: positive integers for maxResults, maxSnippetChars, etc.
  • OTEL sampleRate: z.number().min(0).max(1)

2.2 Core Schema -- zod-schema.core.ts

File: src/config/zod-schema.core.ts

Contains ~50+ exported schemas used as building blocks across the configuration:

SchemaKey FieldsConstraints
ModelApiSchemaunion literal7 API types: openai-completions, anthropic-messages, google-generative-ai, etc.
ModelCompatSchemasupportsStore, maxTokensField, thinkingFormat, etc..strict().optional()
ModelDefinitionSchemaid, name, api, reasoning, input, cost, contextWindow, maxTokens, compatid/name: .min(1), contextWindow/maxTokens: .positive()
ModelProviderSchemabaseUrl, apiKey(sensitive), auth, api, modelsbaseUrl: .min(1), models: array of ModelDefinition
ModelsConfigSchemamode (merge/replace), providers, bedrockDiscoveryoptional top-level
GroupPolicySchemaenum"open", "disabled", "allowlist"
DmPolicySchemaenum"pairing", "allowlist", "open", "disabled"
QueueModeSchemaunion7 modes: steer, followup, collect, steer-backlog, queue, interrupt, etc.
HexColorSchemastringregex /^#?[0-9a-fA-F]{6}$/
ExecutableTokenSchemastring.refine(isSafeExecutableValue) -- security check
TranscribeAudioSchemacommand, timeoutSecondscommand[0] validated via isSafeExecutableValue
TtsConfigSchemaauto, provider, elevenlabs, openai, edgeelevenlabs seed: .min(0).max(4294967295)
CliBackendSchemacommand, args, output format, reliability/watchdog25+ fields for CLI agent backends
BlockStreamingCoalesceSchemaminChars, maxChars, idleMspositive integers
RetryConfigSchemaattempts, minDelayMs, maxDelayMs, jitterjitter: .min(0).max(1)
ToolsMediaUnderstandingSchemaenabled, scope, maxBytes, modelsNested media understanding config
LinkModelSchematype: "cli", command, args, timeoutSecondscommand: .min(1)
ProviderCommandsSchemanative, nativeSkillsz.union([z.boolean(), z.literal("auto")])

Security-relevant schemas:

  • requireOpenAllowFrom(): Enforces that dmPolicy="open" requires allowFrom to include "*" -- prevents accidentally exposing DMs to everyone
  • isSafeExecutableValue: Validates executable paths to prevent command injection

2.3 Agent Runtime Schema -- zod-schema.agent-runtime.ts

File: src/config/zod-schema.agent-runtime.ts

SchemaKey FieldsConstraints
HeartbeatSchemaevery (duration), activeHours, model, session, promptCustom superRefine validates duration and HH:MM time format
SandboxDockerSchemaimage, network, capDrop, pidsLimit, memory, bindsSecurity refinements: blocks network: "host", seccompProfile: "unconfined", apparmorProfile: "unconfined", validates bind mount paths are absolute
SandboxBrowserSchemaenabled, image, cdpPort, headless, bindsBlocks network: "host"
ToolPolicySchemaallow, alsoAllow, denyRefinement: cannot set both allow AND alsoAllow
AgentToolsSchemaprofile, allow, deny, byProvider, elevated, exec, fs, loopDetectionProfile: minimal/coding/messaging/full
ToolExecSchemahost (sandbox/gateway/node), security (deny/allowlist/full), ask, timeoutSecapprovalRunningNoticeMs only on agent variant
ToolLoopDetectionSchemaenabled, historySize, warningThreshold, criticalThresholdRefinement: warning < critical < globalCircuitBreaker
AgentSandboxSchemamode (off/non-main/all), workspaceAccess (none/ro/rw), scope (session/agent/shared)
ToolsWebSearchSchemaprovider (brave/perplexity/grok), apiKey(sensitive), maxResults
MemorySearchSchemaenabled, sources, provider (openai/local/gemini/voyage), store, chunking, query, hybrid, mmr, temporalDecayComplex nested search config
AgentEntrySchemaid, default, name, workspace, model, skills, tools, sandbox, heartbeat, identityid: required string
ToolsSchemaprofile, allow, deny, web, media, links, sessions, loopDetection, message, exec, fsTop-level tools config

Security-relevant fields:

  • ToolExecSchema.host: Controls where commands execute (sandbox/gateway/node)
  • ToolExecSchema.security: Controls command execution policy (deny/allowlist/full)
  • AgentSandboxSchema.workspaceAccess: Controls filesystem access level
  • ElevatedAllowFromSchema: z.record(z.string(), z.array(z.union([z.string(), z.number()]))) -- provider-keyed allowlists

2.4 Agents Schema -- zod-schema.agents.ts

File: src/config/zod-schema.agents.ts

SchemaKey FieldsConstraints
AgentsSchemadefaults (lazy -> AgentDefaultsSchema), list (array of AgentEntrySchema).strict().optional()
BindingsSchemaarray of {agentId, match: {channel, accountId, peer}}peer.kind: direct/group/channel/dm(deprecated)
BroadcastSchemastrategy (parallel/sequential) + catchall z.array(z.string())Uses .catchall() for dynamic peer IDs
AudioSchematranscription: TranscribeAudioSchema

2.5 Agent Model Schema -- zod-schema.agent-model.ts

File: src/config/zod-schema.agent-model.ts

AgentModelSchema = z.union([
  z.string(),                              // Simple model name
  z.object({
    primary: z.string().optional(),
    fallbacks: z.array(z.string()).optional(),
  }).strict(),
]);

Supports both a simple string model identifier and an object with primary + fallback models.

2.6 Session Schema -- zod-schema.session.ts

File: src/config/zod-schema.session.ts

SchemaKey FieldsConstraints
SessionSchemascope (per-sender/global), dmScope, resetTriggers, idleMinutes, reset, maintenanceidleMinutes: positive int
SessionSendPolicySchemaCreated via createAllowDenyChannelRulesSchema()Allow/deny rules with channel/chatType matching
SessionResetConfigSchemamode (daily/idle), atHour, idleMinutesatHour: .min(0).max(23)
MessagesSchemamessagePrefix, groupChat, queue, ackReaction, statusReactions, ttsstatusReactions has emoji and timing config
CommandsSchemanative, nativeSkills, bash, config, debug, restart, ownerAllowFromDefault factory: {native: "auto", nativeSkills: "auto", restart: true, ownerDisplay: "raw"}

Session maintenance validation (superRefine):

  • pruneAfter: Validated via parseDurationMs() with "d" default unit
  • rotateBytes: Validated via parseByteSize() with "b" default unit

Notable: CommandsSchema uses .optional().default(() => ...) which provides a runtime-safe default even when the entire section is omitted.

2.7 Approvals Schema -- zod-schema.approvals.ts

File: src/config/zod-schema.approvals.ts

SchemaKey FieldsConstraints
ApprovalsSchemaexec: ExecApprovalForwardingSchema.strict().optional()
ExecApprovalForwardingSchemaenabled, mode (session/targets/both), agentFilter, targets
ExecApprovalForwardTargetSchemachannel (min 1), to (min 1), accountId, threadIdchannel and to are required non-empty

2.8 Providers Schema -- zod-schema.providers.ts

File: src/config/zod-schema.providers.ts

The aggregator schema that assembles all channel provider configs:

FieldSchema Source
defaultsinline (groupPolicy, heartbeat)
modelByChannelz.record(z.string(), z.record(z.string(), z.string()))
whatsappWhatsAppConfigSchema (separate file)
telegramTelegramConfigSchema
discordDiscordConfigSchema
ircIrcConfigSchema
googlechatGoogleChatConfigSchema
slackSlackConfigSchema
signalSignalConfigSchema
imessageIMessageConfigSchema
bluebubblesBlueBubblesConfigSchema
msteamsMSTeamsConfigSchema

Critical: Uses .passthrough() instead of .strict() to allow extension channel configs from plugins. This is intentional for extensibility but bypasses Zod's unknown-property rejection for the entire channels object.

2.9 Providers Core Schema -- zod-schema.providers-core.ts

File: src/config/zod-schema.providers-core.ts

Defines the schema for each messaging channel provider. Very large file (~1100 lines).

ProviderKey Security FieldsNotable Patterns
TelegrambotToken(sensitive), webhookSecret(sensitive), dmPolicy, allowFromMulti-account support, custom commands with superRefine validation, streaming mode normalization
Discordtoken(sensitive), dmPolicy, allowFrom, guilds, execApprovalsDiscord ID validation (must be strings, not numbers), multi-account, voice config
SlackbotToken(sensitive), appToken(sensitive), userToken(sensitive), signingSecret(sensitive)HTTP/socket mode, per-channel tool policies, safeExtend() usage
Signalaccount, cliPath(ExecutableTokenSchema), dmPolicy, allowFromSCP remote host validation via isSafeScpRemoteHost
iMessagecliPath(ExecutableTokenSchema), dmPolicy, allowFrom, attachmentRootsPath validation via isValidInboundPathRootPattern
IRCpassword(sensitive), nickserv with register validationNickServ registration requires email
MS TeamsappPassword(sensitive), tenantId, dmPolicy, groupPolicySharePoint site ID for file uploads
Google ChatserviceAccount, audience, webhookUrlService account can be string or object
BlueBubblespassword(sensitive), serverUrlActions schema (reactions, edit, unsend, etc.)

Common security patterns across all providers:

  • requireOpenAllowFrom(): Called in every provider's superRefine to enforce that dmPolicy="open" requires explicit allowFrom wildcard
  • dmPolicy defaults to "pairing" (most restrictive default)
  • groupPolicy defaults to "allowlist"
  • Multi-account pattern: base config + accounts: z.record(z.string(), AccountSchema)

2.10 Hooks Schema -- zod-schema.hooks.ts

File: src/config/zod-schema.hooks.ts

SchemaKey FieldsConstraints
HookMappingSchemaid, match (path, source), action (wake/agent), channel, transformtransform.module validated via SafeRelativeModulePathSchema
InternalHookHandlerSchemaevent, module, exportmodule: safe relative path
InternalHooksSchemaenabled, handlers, entries, load, installsentries use .passthrough() for per-hook custom keys
HooksGmailSchemaaccount, label, topic, pushToken(sensitive), serve, tailscale, model

Security: SafeRelativeModulePathSchema prevents:

  • Absolute paths
  • Tilde-prefixed paths (~)
  • URL-ish paths (containing :)
  • Parent directory traversal (.. segments)

2.11 Allow/Deny Schema -- zod-schema.allowdeny.ts

File: src/config/zod-schema.allowdeny.ts

Factory function createAllowDenyChannelRulesSchema() used for session send policies and media understanding scopes.

Structure:
{
  default?: "allow" | "deny",
  rules?: Array<{
    action: "allow" | "deny",
    match?: {
      channel?: string,
      chatType?: "direct" | "group" | "channel" | "dm"(deprecated),
      keyPrefix?: string,
      rawKeyPrefix?: string,
    }
  }>
}

2.12 Installs Schema -- zod-schema.installs.ts

File: src/config/zod-schema.installs.ts

FieldTypeNotes
source`"npm""archive"
specstringoptional
sourcePathstringoptional
installPathstringoptional
versionstringoptional
resolvedNamestringoptional
resolvedVersionstringoptional
resolvedSpecstringoptional
integritystringoptional, SRI hash
shasumstringoptional
resolvedAtstringoptional, ISO timestamp
installedAtstringoptional, ISO timestamp

Exported as InstallRecordShape (not a full schema -- used with spread in parent objects).

2.13 Sensitive Field Registry -- zod-schema.sensitive.ts

File: src/config/zod-schema.sensitive.ts

export const sensitive = z.registry<undefined, z.ZodType>();

A Zod registry used to mark fields as sensitive. Fields registered with .register(sensitive) are automatically redacted when config is serialized for the dashboard/control UI. Sensitive fields found across all schemas:

  • Auth tokens: botToken, appToken, userToken, token, webhookSecret, signingSecret
  • API keys: apiKey, apiKey (elevenlabs, openai, perplexity, grok, skills)
  • Passwords: password, appPassword
  • Gateway: gateway.auth.token, gateway.auth.password, gateway.remote.token, gateway.remote.password
  • Cron: webhookToken
  • Hooks: token, pushToken, sessionKey
  • Commands: ownerDisplaySecret

3. TypeBox Protocol Schemas

TypeBox schemas define the WebSocket RPC protocol for client-gateway communication. They compile to JSON Schema and are validated at runtime via AJV.

3.1 Frames -- schema/frames.ts

Core WebSocket frame types:

SchemaFieldsPurpose
TickEventSchemats: Integer(min:0)Keepalive heartbeat
ShutdownEventSchemareason: NonEmptyString, restartExpectedMs?Graceful shutdown notification
ConnectParamsSchemaminProtocol, maxProtocol, client, caps, commands, permissions, device, auth, localeWebSocket handshake params. Client includes id, displayName, version, platform, mode
HelloOkSchematype:"hello-ok", protocol, server, features, snapshot, policyServer handshake response. Features lists available methods and events
ErrorShapeSchemacode, message, details?, retryable?, retryAfterMs?Standard error envelope
RequestFrameSchematype:"req", id, method, params?Client-to-server RPC request
ResponseFrameSchematype:"res", id, ok, payload?, error?Server-to-client RPC response
EventFrameSchematype:"event", event, payload?, seq?, stateVersion?Server-to-client push event
GatewayFrameSchemaUnion of Request/Response/EventDiscriminated on type field

Device pairing in ConnectParams:

device?: {
  id: NonEmptyString,
  publicKey: NonEmptyString,
  signature: NonEmptyString,
  signedAt: Integer(min:0),
  nonce?: NonEmptyString,
}

3.2 Primitives -- schema/primitives.ts

SchemaDefinition
NonEmptyStringType.String({ minLength: 1 })
SessionLabelStringType.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH })
GatewayClientIdSchemaUnion of literals from GATEWAY_CLIENT_IDS
GatewayClientModeSchemaUnion of literals from GATEWAY_CLIENT_MODES

3.3 Agent -- schema/agent.ts

SchemaFieldsMethods Validated
AgentEventSchemarunId, seq, stream, ts, data (Record)agent.event event
SendParamsSchemato, message?, mediaUrl?, mediaUrls?, gifPlayback?, channel?, idempotencyKeyagent.send
PollParamsSchemato, question, options(2-12 items), maxSelections(1-12), durationSeconds(1-604800)agent.poll
AgentParamsSchemamessage, agentId?, to?, sessionKey?, thinking?, deliver?, attachments?, timeout?, inputProvenance?, idempotencyKey, label?, spawnedBy?agent.run
AgentIdentityParamsSchemaagentId?, sessionKey?agent.identity
AgentIdentityResultSchemaagentId, name?, avatar?, emoji?agent.identity response
AgentWaitParamsSchemarunId, timeoutMs?agent.wait
WakeParamsSchemamode: "now""next-heartbeat", text

3.4 Agents/Models/Skills -- schema/agents-models-skills.ts

SchemaFieldsMethods Validated
ModelChoiceSchemaid, name, provider, contextWindow?, reasoning?models.list response items
AgentSummarySchemaid, name?, identity? (name, theme, emoji, avatar, avatarUrl)agents.list response items
AgentsListResultSchemadefaultId, mainKey, scope, agents[]agents.list response
AgentsCreateParamsSchemaname, workspace, emoji?, avatar?agents.create
AgentsUpdateParamsSchemaagentId, name?, workspace?, model?, avatar?agents.update
AgentsDeleteParamsSchemaagentId, deleteFiles?agents.delete
AgentsFilesListParamsSchemaagentIdagents.files.list
AgentsFilesGetParamsSchemaagentId, nameagents.files.get
AgentsFilesSetParamsSchemaagentId, name, contentagents.files.set
ModelsListParamsSchema(empty object)models.list
SkillsStatusParamsSchemaagentId?skills.status
SkillsBinsParamsSchema(empty object)skills.bins
SkillsInstallParamsSchemaname, installId, timeoutMs?(min:1000)skills.install
SkillsUpdateParamsSchemaskillKey, enabled?, apiKey?, env?skills.update

3.5 Config -- schema/config.ts

SchemaFieldsMethods Validated
ConfigGetParamsSchema(empty object)config.get
ConfigSetParamsSchemaraw (NonEmptyString), baseHash?config.set
ConfigApplyParamsSchemaraw, baseHash?, sessionKey?, note?, restartDelayMs?config.apply
ConfigPatchParamsSchemasame as Applyconfig.patch
ConfigSchemaParamsSchema(empty object)config.schema
UpdateRunParamsSchemasessionKey?, note?, restartDelayMs?, timeoutMs?update.run
ConfigUiHintSchemalabel?, help?, group?, order?, advanced?, sensitive?, placeholder?, itemTemplate?Part of schema response
ConfigSchemaResponseSchemaschema (Unknown), uiHints (Record), version, generatedAtconfig.schema response

Note: config.set and config.apply accept raw as a string (the full config YAML/JSON5), not a pre-parsed object. The server parses and validates it internally.

3.6 Sessions -- schema/sessions.ts

SchemaFieldsMethods Validated
SessionsListParamsSchemalimit?, activeMinutes?, includeGlobal?, includeDerivedTitles?, includeLastMessage?, label?, spawnedBy?, agentId?, search?sessions.list
SessionsPreviewParamsSchemakeys (array, minItems:1), limit?, maxChars?(min:20)sessions.preview
SessionsResolveParamsSchemakey?, sessionId?, label?, agentId?, spawnedBy?, includeGlobal?sessions.resolve
SessionsPatchParamsSchemakey, label?, thinkingLevel?, verboseLevel?, reasoningLevel?, responseUsage?, model?, execHost?, execSecurity?, sendPolicy?, groupActivation?, spawnedBy?, spawnDepth?sessions.patch
SessionsResetParamsSchemakey, reason? ("new""reset")
SessionsDeleteParamsSchemakey, deleteTranscript?, emitLifecycleHooks?sessions.delete
SessionsCompactParamsSchemakey, maxLines?(min:1)sessions.compact
SessionsUsageParamsSchemakey?, startDate? (YYYY-MM-DD pattern), endDate?, mode?, utcOffset? (UTC+/-N pattern), limit?, includeContextWeight?sessions.usage

Notable: Date patterns use regex validation: ^\\d{4}-\\d{2}-\\d{2}$ for dates, ^UTC[+-]\\d{1,2}(?::[0-5]\\d)?$ for UTC offsets.

3.7 Cron -- schema/cron.ts

SchemaFields
CronScheduleSchemaUnion: {kind:"at", at}, {kind:"every", everyMs, anchorMs?}, {kind:"cron", expr, tz?, staggerMs?}
CronPayloadSchemaUnion: {kind:"systemEvent", text}, {kind:"agentTurn", message, model?, thinking?, timeoutSeconds?, deliver?, channel?, to?}
CronDeliverySchemaUnion: {mode:"none"}, {mode:"announce"}, {mode:"webhook", to}
CronJobSchemaid, agentId?, name, enabled, schedule, sessionTarget (main/isolated), wakeMode, payload, delivery?, state
CronJobStateSchemanextRunAtMs?, runningAtMs?, lastRunAtMs?, lastStatus?, lastError?, consecutiveErrors?
CronAddParamsSchemaname, schedule, sessionTarget, wakeMode, payload, delivery?, agentId?, description?
CronUpdateParamsSchemaUnion of {id, patch} or {jobId, patch}
CronRemoveParamsSchemaUnion of {id} or {jobId}
CronRunParamsSchemaid/jobId + mode? (due/force)
CronRunsParamsSchemaid/jobId + limit?(1-5000)
CronRunLogEntrySchemats, jobId, action:"finished", status?, error?, sessionKey?, durationMs?

3.8 Exec Approvals -- schema/exec-approvals.ts

SchemaFields
ExecApprovalsAllowlistEntrySchemaid?, pattern, lastUsedAt?, lastUsedCommand?, lastResolvedPath?
ExecApprovalsDefaultsSchemasecurity?, ask?, askFallback?, autoAllowSkills?
ExecApprovalsAgentSchemasecurity?, ask?, askFallback?, autoAllowSkills?, allowlist?
ExecApprovalsFileSchemaversion:1, socket?(path, token), defaults?, agents?(Record)
ExecApprovalsSnapshotSchemapath, exists, hash, file
ExecApprovalRequestParamsSchemaid?, command, cwd?, host?, security?, agentId?, resolvedPath?, sessionKey?, timeoutMs?, twoPhase?
ExecApprovalResolveParamsSchemaid, decision

Note: security, ask, and decision fields use Type.String() without enum constraints. This means any string value passes TypeBox validation -- the actual enforcement happens in application logic.

3.9 Devices -- schema/devices.ts

SchemaMethods
DevicePairListParamsSchemadevice.pair.list
DevicePairApproveParamsSchemadevice.pair.approve (requestId)
DevicePairRejectParamsSchemadevice.pair.reject (requestId)
DevicePairRemoveParamsSchemadevice.pair.remove (deviceId)
DeviceTokenRotateParamsSchemadevice.token.rotate (deviceId, role, scopes?)
DeviceTokenRevokeParamsSchemadevice.token.revoke (deviceId, role)
DevicePairRequestedEventSchemaEvent with requestId, deviceId, publicKey, displayName?, platform?, roles?, scopes?, remoteIp?
DevicePairResolvedEventSchemaEvent with requestId, deviceId, decision, ts

3.10 Logs/Chat -- schema/logs-chat.ts

SchemaFields
LogsTailParamsSchemacursor?(min:0), limit?(1-5000), maxBytes?(1-1000000)
LogsTailResultSchemafile, cursor, size, lines[], truncated?, reset?
ChatHistoryParamsSchemasessionKey, limit?(1-1000)
ChatSendParamsSchemasessionKey, message, thinking?, deliver?, attachments?, timeoutMs?, idempotencyKey
ChatAbortParamsSchemasessionKey, runId?
ChatInjectParamsSchemasessionKey, message, label?(maxLength:100)
ChatEventSchemarunId, sessionKey, seq, state (delta/final/aborted/error), message?, usage?, stopReason?

3.11 Nodes -- schema/nodes.ts

SchemaMethods
NodePairRequestParamsSchemanode.pair.request (nodeId, displayName?, platform?, caps?, commands?)
NodePairListParamsSchemanode.pair.list
NodePairApproveParamsSchemanode.pair.approve (requestId)
NodePairRejectParamsSchemanode.pair.reject (requestId)
NodePairVerifyParamsSchemanode.pair.verify (nodeId, token)
NodeRenameParamsSchemanode.rename (nodeId, displayName)
NodeListParamsSchemanode.list
NodeDescribeParamsSchemanode.describe (nodeId)
NodeInvokeParamsSchemanode.invoke (nodeId, command, params?, timeoutMs?, idempotencyKey)
NodeInvokeResultParamsSchemanode.invoke.result (id, nodeId, ok, payload?, error?)
NodeEventParamsSchemanode.event (event, payload?)
NodeInvokeRequestEventSchemaEvent pushed to nodes (id, nodeId, command, paramsJSON?)

3.12 Wizard -- schema/wizard.ts

SchemaFields
WizardStartParamsSchemamode? (local/remote), workspace?
WizardAnswerSchemastepId, value?
WizardNextParamsSchemasessionId, answer?
WizardStepSchemaid, type (note/select/text/confirm/multiselect/progress/action), title?, message?, options?, initialValue?, placeholder?, sensitive?, executor?
WizardStartResultSchemasessionId, done, step?, status?, error?
WizardNextResultSchemadone, step?, status?, error?
WizardStatusResultSchemastatus (running/done/cancelled/error), error?

3.13 Snapshot -- schema/snapshot.ts

SchemaFields
PresenceEntrySchemahost?, ip?, version?, platform?, deviceFamily?, mode?, lastInputSeconds?, tags?, deviceId?, roles?, scopes?
HealthSnapshotSchemaType.Any() -- no validation
SessionDefaultsSchemadefaultAgentId, mainKey, mainSessionKey, scope?
StateVersionSchemapresence: Integer(min:0), health: Integer(min:0)
SnapshotSchemapresence[], health, stateVersion, uptimeMs, configPath?, sessionDefaults?, authMode?, updateAvailable?

Gap: HealthSnapshotSchema = Type.Any() -- health data passes through with zero validation.

3.14 Push -- schema/push.ts

SchemaFields
PushTestParamsSchemanodeId, title?, body?, environment? ("sandbox"/"production")
PushTestResultSchemaok, status (Integer), apnsId?, reason?, tokenSuffix, topic, environment

3.15 Channels -- schema/channels.ts

SchemaFields
TalkModeParamsSchemaenabled, phase?
TalkConfigParamsSchemaincludeSecrets?
TalkConfigResultSchemaconfig: {talk?, session?, ui?}
ChannelsStatusParamsSchemaprobe?, timeoutMs?
ChannelAccountSnapshotSchemaaccountId, name?, enabled?, configured?, linked?, running?, connected?, lastError?, mode?, dmPolicy?, allowFrom?, etc. -- uses additionalProperties: true
ChannelUiMetaSchemaid, label, detailLabel, systemImage?
ChannelsStatusResultSchemats, channelOrder[], channelLabels, channels (Record Unknown), channelAccounts, channelDefaultAccountId
ChannelsLogoutParamsSchemachannel, accountId?
WebLoginStartParamsSchemaforce?, timeoutMs?, verbose?, accountId?
WebLoginWaitParamsSchematimeoutMs?, accountId?

Note: ChannelAccountSnapshotSchema uses additionalProperties: true -- intentionally schema-light so new channels can ship without protocol updates.


4. Plugin Schema System

Manifest Structure

File: src/plugins/manifest.ts

Plugin manifests are declared in openclaw.plugin.json files:

type PluginManifest = {
  id: string;                              // Required, non-empty
  configSchema: Record<string, unknown>;    // Required, JSON Schema object
  kind?: "memory";                         // Optional plugin kind
  channels?: string[];                     // Channel IDs this plugin provides
  providers?: string[];                    // Provider IDs this plugin provides
  skills?: string[];                       // Skill IDs this plugin provides
  name?: string;
  description?: string;
  version?: string;
  uiHints?: Record<string, PluginConfigUiHint>;
};

Config Validation Flow

  1. Manifest loading (loadPluginManifest()): Reads JSON, validates required id and configSchema fields
  2. Registry assembly (loadPluginManifestRegistry()): Scans bundled, global, workspace, and config-specified plugin paths
  3. Enable resolution (resolveEnableState()): Checks allow/deny lists, memory slot decisions
  4. Schema validation (validateJsonSchemaValue()): Uses AJV to validate plugin config values against the manifest's configSchema

Plugin Config Schema Interface

File: src/plugins/types.ts

type OpenClawPluginConfigSchema = {
  safeParse?: (value: unknown) => { success: boolean; data?: unknown; error?: ... };
  parse?: (value: unknown) => unknown;
  validate?: (value: unknown) => PluginConfigValidation;
  uiHints?: Record<string, PluginConfigUiHint>;
  jsonSchema?: Record<string, unknown>;
};

Plugins can provide validation via:

  • safeParse (Zod-compatible)
  • parse (throwing validation)
  • validate (custom validation returning {ok, errors})
  • jsonSchema (raw JSON Schema for AJV)

UI Hints System

File: src/plugins/types.ts and src/gateway/protocol/schema/config.ts

UI hints are metadata attached to config fields to drive control UI rendering:

type PluginConfigUiHint = {
  label?: string;        // Human-readable field label
  help?: string;         // Help text / tooltip
  advanced?: boolean;    // Hide in "basic" view
  sensitive?: boolean;   // Mask input value
  placeholder?: string;  // Input placeholder text
};

The protocol schema extends this with additional fields:

ConfigUiHintSchema = {
  label?, help?, group?,     // Grouping for UI layout
  order?: Integer,           // Sort order within group
  advanced?, sensitive?,
  placeholder?,
  itemTemplate?: Unknown     // Template for array items
};

UI hints flow through:

  1. Plugin manifest declares uiHints keyed by config path
  2. config.schema RPC method returns schema + uiHints to the control UI
  3. Control UI renders form fields based on JSON Schema type + uiHints metadata

5. SQLite Memory Schema

File: src/memory/memory-schema.ts

Tables

TableColumnsKeyPurpose
metakey TEXT, value TEXTkey (PK)Key-value metadata store
filespath TEXT, source TEXT DEFAULT 'memory', hash TEXT, mtime INTEGER, size INTEGERpath (PK)Tracks indexed files and their hashes
chunksid TEXT, path TEXT, source TEXT DEFAULT 'memory', start_line INTEGER, end_line INTEGER, hash TEXT, model TEXT, text TEXT, embedding TEXT, updated_at INTEGERid (PK)Text chunks with embeddings
{embeddingCacheTable}provider TEXT, model TEXT, provider_key TEXT, hash TEXT, embedding TEXT, dims INTEGER, updated_at INTEGER(provider, model, provider_key, hash) composite PKCaches computed embeddings
{ftsTable}text, id UNINDEXED, path UNINDEXED, source UNINDEXED, model UNINDEXED, start_line UNINDEXED, end_line UNINDEXEDFTS5 virtual tableFull-text search over chunk text

Indices

IndexTableColumn(s)
idx_embedding_cache_updated_atembeddingCacheTableupdated_at
idx_chunks_pathchunkspath
idx_chunks_sourcechunkssource

FTS5 Setup

The FTS5 virtual table is created conditionally (ftsEnabled parameter):

  • Only the text column is indexed for search
  • All other columns (id, path, source, model, start_line, end_line) are UNINDEXED -- stored in the FTS table but not searchable, available for result retrieval
  • FTS creation is wrapped in try/catch -- gracefully degrades if FTS5 extension is not available
  • Returns {ftsAvailable: boolean, ftsError?: string} to callers

Schema Migration

The ensureColumn() helper function handles additive migrations:

  • Uses PRAGMA table_info() to check column existence
  • Adds missing columns via ALTER TABLE ADD COLUMN
  • Currently migrates: files.source, chunks.source (added after initial schema)

6. Session Metadata Types

File: src/config/sessions/types.ts

SessionEntry Interface

The SessionEntry interface is a plain TypeScript type with NO runtime validation schema. It is used directly with Partial<SessionEntry> patches and object spread merging.

FieldTypePurpose
sessionIdstringUUID session identifier
updatedAtnumberLast update timestamp (ms)
sessionFilestring?Path to JSONL transcript file
spawnedBystring?Parent session key (sandbox scoping)
spawnDepthnumber?Subagent depth (0=main, 1=sub, 2=sub-sub)
systemSentboolean?Whether system prompt has been sent
abortedLastRunboolean?Whether last run was aborted
chatTypeSessionChatType?direct/group/channel
thinkingLevelstring?Per-session thinking override
verboseLevelstring?Per-session verbosity
reasoningLevelstring?Per-session reasoning effort
elevatedLevelstring?Elevated permissions level
ttsAutoTtsAutoMode?Text-to-speech mode
execHoststring?Execution host override
execSecuritystring?Execution security policy override
execAskstring?Execution approval mode override
execNodestring?Node override for execution
responseUsagestring?Token usage display mode
providerOverridestring?Model provider override
modelOverridestring?Model override
authProfileOverridestring?Auth profile override
groupActivationstring?"mention" or "always"
sendPolicystring?"allow" or "deny"
queueModestring?Message queue mode
inputTokens / outputTokens / totalTokensnumber?Token usage tracking
totalTokensFreshboolean?Whether token count is from latest run
cacheRead / cacheWritenumber?Cache usage
model / modelProviderstring?Active model info
contextTokens / compactionCountnumber?Context window usage
labelstring?User-assigned session label
channel / groupId / groupChannel / spacestring?Messaging context
originSessionOrigin?Where the session originated
lastChannel / lastTo / lastAccountId / lastThreadIdvariousLast delivery target
skillsSnapshotSessionSkillSnapshot?Cached skills state
systemPromptReportSessionSystemPromptReport?Context weight analysis

Security Gap #18

The SessionEntry interface has no Zod or TypeBox schema for runtime validation. The mergeSessionEntry() function performs a simple object spread:

function mergeSessionEntry(existing, patch) {
  return { ...existing, ...patch, sessionId, updatedAt };
}

This means:

  • Arbitrary properties can be injected into session metadata
  • String fields like execSecurity, execHost, sendPolicy accept any string -- no enum validation
  • spawnDepth is typed as number? but could receive any value at runtime
  • queueMode lists valid values in the TypeScript type but has no runtime check
  • Session entries are persisted to disk and read back without validation

7. Validation Infrastructure

7.1 Plugin Schema Validator -- src/plugins/schema-validator.ts

Uses AJV (Another JSON Schema Validator) to validate plugin config values:

const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });

function validateJsonSchemaValue(params: {
  schema: Record<string, unknown>;    // JSON Schema from plugin manifest
  cacheKey: string;                   // Cache key for compiled validator
  value: unknown;                     // Config value to validate
}): { ok: true } | { ok: false; errors: string[] }

Key behaviors:

  • Compiled validator caching: Validators are cached by cacheKey and recompiled only if the schema object reference changes
  • strict: false: Does not enforce strict JSON Schema validation (allows unknown keywords)
  • removeAdditional: false: Does not strip unknown properties
  • Error formatting: Converts AJV error paths from /foo/bar to foo.bar dot notation

7.2 Config Validation -- src/config/validation.ts

Three tiers of config validation functions:

FunctionWhat It Does
validateConfigObjectRaw()Legacy check + Zod parse + duplicate agent dirs + avatar paths. Returns raw validated config (no defaults).
validateConfigObject()Calls Raw, then applies applySessionDefaults, applyAgentDefaults, applyModelDefaults
validateConfigObjectWithPlugins()Calls validateConfigObject + validates plugin manifests, channel IDs, heartbeat targets, plugin config schemas

Plugin validation within validateConfigObjectWithPluginsBase():

  1. Validates channel keys against known channel IDs (built-in + plugin-registered)
  2. Validates heartbeat targets against known channels
  3. For each plugin with an explicit config entry:
    • Loads plugin manifest registry
    • Resolves enable/disable state
    • Validates config value against manifest's configSchema via AJV
    • Warns if plugin is disabled but has config present

7.3 RPC Parameter Validation -- src/gateway/protocol/index.ts

The protocol index file creates AJV validators for every RPC method by compiling TypeBox schemas:

const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });

export const validateConnectParams = ajv.compile<ConnectParams>(ConnectParamsSchema);
export const validateAgentParams = ajv.compile(AgentParamsSchema);
// ... 70+ more validators

7.4 Handler-Level Validation -- src/gateway/server-methods/validation.ts

The assertValidParams() function is called at the top of each RPC handler:

function assertValidParams<T>(
  params: unknown,
  validate: Validator<T>,     // AJV compiled validator
  method: string,             // Method name for error messages
  respond: RespondFn,         // Response callback
): params is T

On validation failure, responds with ErrorCodes.INVALID_REQUEST and formatted validation error details. Returns false to allow early handler exit.

Validation Chain Summary

Layer 1: Config File Load
  Zod (strict parse + refinements + transforms)
    |
    v
Layer 2: Post-Parse Config Validation
  Custom checks (duplicates, avatar paths, plugin schemas via AJV)
    |
    v
Layer 3: Runtime Defaults
  applySessionDefaults -> applyAgentDefaults -> applyModelDefaults
    |
    v
Layer 4: RPC Ingress (per-request)
  TypeBox schemas compiled to JSON Schema -> AJV validators
    |
    v
Layer 5: Application Logic
  Type assertions, manual checks (no schema)

8. Schema Gaps and Security Implications

Gap 1: SessionEntry Has No Runtime Validation

Files: src/config/sessions/types.ts Severity: High

SessionEntry is a TypeScript interface only. Fields like execSecurity, execHost, sendPolicy, queueMode, and elevatedLevel accept arbitrary strings at runtime despite having defined enum values in the type. mergeSessionEntry() performs unguarded object spread, allowing property injection.

Impact: A crafted session patch (e.g., via sessions.patch RPC) could inject arbitrary metadata fields or set security-sensitive fields to unexpected values. While SessionsPatchParamsSchema validates the RPC parameters, the actual session store merge has no schema enforcement.

Gap 2: HealthSnapshotSchema = Type.Any()

File: src/gateway/protocol/schema/snapshot.ts Severity: Medium

HealthSnapshotSchema = Type.Any() means health data passes through the protocol with zero validation. Any structure or value is accepted and forwarded to clients.

Gap 3: ChannelAccountSnapshotSchema additionalProperties: true

File: src/gateway/protocol/schema/channels.ts Severity: Low-Medium

ChannelAccountSnapshotSchema uses additionalProperties: true, intentionally allowing any extra fields. While this supports extensibility, it means channel status responses can contain arbitrary data.

Gap 4: Exec Approvals Use Unvalidated Strings

File: src/gateway/protocol/schema/exec-approvals.ts Severity: Medium

ExecApprovalsDefaultsSchema and ExecApprovalsAgentSchema define security, ask, and askFallback as Type.Optional(Type.String()) without enum constraints. The ExecApprovalResolveParamsSchema.decision is also a bare NonEmptyString. Validation of valid values happens only in application logic.

Gap 5: No Response Schema Validation

Severity: Medium

While every RPC method has a params validator (ajv.compile(ParamsSchema)), there is no equivalent validation of outbound response payloads in most cases. Response schemas exist (e.g., AgentsListResultSchema, LogsTailResultSchema) but are not systematically compiled and checked before sending responses. This means server bugs could send malformed responses to clients.

Gap 6: channels.passthrough() Bypasses Strict Validation

File: src/config/zod-schema.providers.ts Severity: Low

ChannelsSchema uses .passthrough() instead of .strict(). While this is intentional for plugin extensibility, it means typos in channel config keys are silently accepted. The post-parse validation in validation.ts does check channel keys against known IDs, partially mitigating this gap.

Gap 7: Plugin Config Uses AJV strict:false

File: src/plugins/schema-validator.ts Severity: Low

Plugin config validation uses strict: false in AJV, which does not reject unknown JSON Schema keywords. A malformed plugin configSchema could behave unexpectedly.

Gap 8: ConnectParams auth.token and auth.password Are Type.String()

File: src/gateway/protocol/schema/frames.ts Severity: Low

Auth credentials in WebSocket connection params accept any string (including empty strings) because they use Type.Optional(Type.String()) rather than NonEmptyString. The actual auth check happens in application logic, but malformed auth params pass schema validation.

Gap 9: HookConfigSchema Uses .passthrough()

File: src/config/zod-schema.hooks.ts Severity: Low

HookConfigSchema uses .passthrough() to allow per-hook custom keys. This means hook configs are only partially validated (enabled/env are typed, everything else is accepted).

Gap 10: No Schema for Session File (JSONL) Content

Severity: Medium

Session transcript files are JSONL format with no schema validation on read. Corrupted or crafted session files are processed without validation, relying on application-level error handling.


9. Provider-Specific Schema Normalization

File: src/agents/pi-tools.schema.ts

The normalizeToolParameters() function adapts tool schemas to work across different LLM providers, each of which has different JSON Schema requirements:

Provider Compatibility Matrix

ProviderIssueNormalization Applied
Gemini (google-generative-ai)Rejects several JSON Schema keywordscleanSchemaForGemini() strips unsupported keywords
OpenAIRejects function tool schemas unless top-level is type: "object"Forces type: "object" on schemas with properties but no explicit type
AnthropicExpects full JSON Schema draft 2020-12 compliancePasses schema through without modification

Schema Normalization Logic

  1. Simple object schemas (has type + properties, no anyOf):

    • Gemini: Apply cleanSchemaForGemini()
    • Others: Pass through unchanged
  2. Missing type with object-ish fields (has properties/required, no type, no anyOf/oneOf):

    • Add type: "object" to satisfy OpenAI
    • Gemini: Also clean for Gemini compatibility
  3. Union schemas (top-level anyOf or oneOf):

    • Flatten union variants: Merge all variant properties into a single type: "object" schema
    • Enum merging: mergePropertySchemas() combines enum values from different variants (e.g., an action field with different allowed values in each variant)
    • Required field computation: Only fields required in ALL variants become required in the merged schema
    • Gemini: Also clean the flattened schema
    • This is necessary because:
      • Gemini does not allow top-level type together with anyOf
      • OpenAI rejects schemas without top-level type: "object"
      • The merged flat schema works across all providers

Provider Detection

const isGeminiProvider = provider.includes("google") || provider.includes("gemini");
const isAnthropicProvider = provider.includes("anthropic") || provider.includes("google-antigravity");

Note: google-antigravity is treated as Anthropic, suggesting this is a provider alias or internal codename.

Enum Extraction Helper

extractEnumValues() handles multiple JSON Schema enum representations:

  • Direct enum arrays
  • const values (wrapped in array)
  • anyOf/oneOf arrays (recursively extracted)

This ensures that when flattening a union type, property-level enums from different variants are merged correctly.


Appendix: Schema File Index

FileSchema SystemLayer
src/config/zod-schema.tsZodConfig root
src/config/zod-schema.core.tsZodConfig building blocks
src/config/zod-schema.agent-runtime.tsZodAgent + tools config
src/config/zod-schema.agents.tsZodAgent list + bindings
src/config/zod-schema.agent-model.tsZodModel selection
src/config/zod-schema.agent-defaults.tsZodAgent defaults
src/config/zod-schema.session.tsZodSession + messages + commands
src/config/zod-schema.approvals.tsZodExec approval forwarding
src/config/zod-schema.providers.tsZodChannel aggregator
src/config/zod-schema.providers-core.tsZodPer-provider schemas
src/config/zod-schema.hooks.tsZodWebhook + internal hooks
src/config/zod-schema.allowdeny.tsZodAllow/deny rule factory
src/config/zod-schema.installs.tsZodPlugin/skill install records
src/config/zod-schema.channels.tsZodChannel heartbeat visibility
src/config/zod-schema.sensitive.tsZodSensitive field registry
src/gateway/protocol/schema/frames.tsTypeBoxWebSocket frames
src/gateway/protocol/schema/primitives.tsTypeBoxPrimitive types
src/gateway/protocol/schema/agent.tsTypeBoxAgent RPC methods
src/gateway/protocol/schema/agents-models-skills.tsTypeBoxCRUD + catalog methods
src/gateway/protocol/schema/config.tsTypeBoxConfig RPC methods
src/gateway/protocol/schema/sessions.tsTypeBoxSession management methods
src/gateway/protocol/schema/cron.tsTypeBoxCron job methods
src/gateway/protocol/schema/exec-approvals.tsTypeBoxExec approval methods
src/gateway/protocol/schema/devices.tsTypeBoxDevice pairing methods
src/gateway/protocol/schema/logs-chat.tsTypeBoxLog tailing + chat methods
src/gateway/protocol/schema/nodes.tsTypeBoxNode management methods
src/gateway/protocol/schema/wizard.tsTypeBoxSetup wizard methods
src/gateway/protocol/schema/snapshot.tsTypeBoxState snapshots
src/gateway/protocol/schema/push.tsTypeBoxPush notification methods
src/gateway/protocol/schema/channels.tsTypeBoxChannel status + talk methods
src/memory/memory-schema.tsRaw SQLSQLite memory index
src/config/sessions/types.tsTypeScript onlySession metadata (NO runtime validation)
src/plugins/schema-validator.tsAJVPlugin config validation
src/config/validation.tsZod + customConfig validation pipeline
src/gateway/protocol/index.tsAJV (from TypeBox)RPC param validators
src/gateway/server-methods/validation.tsAJVHandler-level validation guard
src/agents/pi-tools.schema.tsCustomProvider schema normalization
src/plugins/manifest.tsManualPlugin manifest loading
src/plugins/types.tsTypeScriptPlugin type definitions