Verb schema format (ZON) and CLI/MCP mapping
| Field | Value |
|---|---|
| Status | Accepted |
| Authors | commixon (commixon) |
| Created | 2026-11-06 |
| Accepted | 2026-11-06, approved by the maintainer |
| Linear | COM-203 |
| Supersedes | None |
| Superseded by | None |
History
- 2026-11-07 (COM-398; small compatible normative addition, accepted through
the COM-398 PR review): §3 adds the optional
nullablemember to every type descriptor kind, ratifying venom-std COM-390. It is legal inresult,itemand errordatadescriptors at any depth, but not underparams. §7 says the digest includes it only when true. This is a compatible addition: descriptors withoutnullablekeep their meaning and their digest.
Summary
A tool declares its verbs in a data-only ZON manifest. venom-std validates
that manifest and generates the CLI, MCP tool catalog, Markdown/man pages,
zsh/bash/fish completions and skill output from one normalized model.
Handlers remain compiled Zig functions. This RFC defines schema format 1,
its type system and mappings, with a worked venom-echo example.
Motivation
COM-213 (CLI generation) and the P0 venom-echo conformance tool (COM-216)
need a shared declaration format. Separately maintained parsers, MCP schemas
and documentation drift on defaults, errors and permissions. The
ecosystem contract
requires one schema and forbids dynamic plugins or embedded scripting.
The wire protocol already defines dotted verb
names, request envelopes, streams and stable exit codes; this RFC supplies
the metadata those transports consume.
Design
1. Worked example: a toy echo tool
Save this complete manifest as verbs.zon:
.{
.format = 1,
.tool = "venom-echo",
.summary = "A toy daemon for protocol conformance",
.verbs = .{
.{
.name = "echo",
.summary = "Return the supplied text",
.params = .{
.{
.name = "text",
.summary = "Text to return",
.type = .{ .kind = .string },
.required = true,
},
.{
.name = "style",
.summary = "Output casing",
.type = .{ .kind = .string, .choices = .{ "plain", "upper" } },
.default = "plain",
},
},
.result = .{
.kind = .object,
.fields = .{
.{
.name = "text",
.summary = "Returned text",
.type = .{ .kind = .string },
.required = true,
},
},
},
.errors = .{},
.grant = .read,
.streams = false,
.examples = .{
.{ .params = .{ .text = "hi" }, .result = .{ .text = "hi" } },
.{ .params = .{ .text = "hi", .style = "upper" }, .result = .{ .text = "HI" } },
},
},
},
}
This is the tool-specific fragment. The build merges standard verb bundles
(section 7); this fragment alone is not a complete agent-contract binary.
echo transforms text in memory and needs only read, never write.
$ venom-echo echo --text hi
{"text":"hi"}
$ venom-echo echo --text hi --style upper --json
{"text":"HI"}
$ venom-echo echo --json
{"error":{"code":"invalid_params.missing","message":"missing required param 'text'","data":{"param":"text"}}}
$ echo $?
2
Human rendering defaults to compact JSON for results, one value per line; errors use the message on stderr. A tool MAY supply a compiled human renderer, which MUST NOT affect validation, structured output or exit codes.
After the client’s hello, the second invocation sends and receives:
{"id":2,"verb":"echo","params":{"text":"hi","style":"upper"}}
{"id":2,"ok":{"text":"HI"}}
2. Manifest and normalized model
The manifest MUST be static ZON data, loaded at build time. It MUST NOT contain code, handler names to load dynamically, imports or executable expressions. A compiled Zig declaration MAY construct the same model; it MUST pass the same validator and produce identical generated artifacts. The build registers each handler against its exact wire name and fails for missing or extra handlers (including standard bundles).
Root members are format (integer, exactly 1), tool (RFC-0002 tool name),
summary (nonempty string) and verbs (tuple of verb records). Unknown
members at every level MUST fail generation. Duplicate names MUST fail.
Strings MUST be valid UTF-8; names and summaries MUST NOT contain control
characters. Summaries are plain text, escaped for each output format.
| Verb member | Meaning / default |
|---|---|
name, summary |
Required wire name and nonempty description |
params |
Field records in documentation order; default empty tuple |
result |
Required type descriptor for final ok |
errors |
Tool-specific error records; default empty tuple |
grant |
Required: none, read, write, respond, files, spawn, admin |
streams |
Boolean; default false |
item |
Type descriptor; required exactly when streams = true |
exit |
Default .{ .kind = .standard }; optional result status policy below |
examples |
Tuple of {params, result} records; default empty tuple |
grant names are enum literals in ZON. The daemon MUST enforce the grant
before invoking a handler, including for MCP and local dispatch. Metadata
never authorizes a caller. Unknown callers get at most read-only access;
identity, resource scope, issuance and revocation belong to RFC-0005
(COM-204). none is explicit, never inferred from a missing declaration.
Protocol feature capabilities from RFC-0002 are separate from grants.
Field records have required name, summary, type, plus optional
required (default false) and default (absent by default). required = true
and default together are invalid. Absence and explicit null differ:
defaults fill only absent fields, then recursive validation runs. Optional
fields without defaults remain absent; no implicit null is inserted.
Examples MUST validate after default insertion; example results MUST validate
against result. This format does not execute examples during generation.
3. Types and errors
Type descriptors are closed records selected by kind:
kind |
Additional members | JSON Schema mapping |
|---|---|---|
string |
Optional nonempty unique string tuple choices |
type: string, optional enum |
boolean |
None | type: boolean |
integer |
Optional inclusive min, max |
type: integer, minimum, maximum |
number |
Optional finite inclusive min, max |
type: number, minimum, maximum |
null |
None | type: null |
array |
Required items descriptor |
type: array, items |
object |
Required fields tuple of field records |
type: object, properties, required, additionalProperties: false |
Every kind also accepts an optional boolean nullable (default false).
A nullable descriptor accepts JSON null as well as its kind’s values;
non-null values are validated exactly as without nullable. It maps to
JSON Schema anyOf: [<kind's schema>, {type: null}], and generated
documentation renders it as “nullable is independent
of field presence (§2): a required nullable field must be present but
may be null; an optional one may also be absent. On a null kind it
changes nothing.
nullable is legal in result, item and error data descriptors at
any depth. It MUST NOT appear anywhere under params, including nested
array items and object fields: §4’s CLI spelling cannot tell the
string null from JSON null, and absence already expresses “no value”
for an input. Generators MUST reject it there.
Only listed members and nullable are legal per kind. Bounds MUST be ordered; integers
and integer bounds MUST be within ±9007199254740991 for lossless JSON
interchange. Numbers MUST be finite. ZON enum literals are metadata only;
wire enum values are strings. There are no unions, references, arbitrary
maps or executable validators in format 1. Empty object fields are legal.
Every object’s defaults apply recursively when that object is present.
Input validation is identical at CLI, MCP and daemon boundaries; the daemon
is authoritative. Unknown fields, missing required fields, wrong types,
invalid enum values and out-of-range values produce respectively
invalid_params.unknown, .missing, .type, .enum, .range, with
data.param a dotted field path (array indexes are decimal components).
No string-to-number coercion occurs on JSON inputs. Invalid handler results
or stream items produce internal.result, never success with invalid data.
Each errors record has code, summary, and optional data type descriptor
(default empty object). Codes MUST start with an RFC-0002 class and have
at least one refinement component, e.g. conflict.echo_busy. Components
match [a-z][a-z0-9_]*. The generator documents class-derived exits;
no per-error numeric override is permitted. Standard protocol, validation,
authorization, transport, cancellation and internal errors are implicit;
errors lists additional handler failures, not an exhaustive transport list.
exit = .{ .kind = .result, .field = "status" } permits successful CLI exit
status passthrough. field MUST name a required top-level integer result
field bounded 0–255. The status is used only on success; errors always use
RFC-0002 section 7.2. MCP returns that status as ordinary result data.
standard accepts no other members; result accepts only field.
4. Names and CLI mapping
Wire names MUST follow RFC-0002’s
^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$. Choose dotted noun.action
for grouped operations (pane.split, config.get, skill.install), and
bare names for standalone operations (echo, events, doctor).
CLI canonical spelling splits dots into space-separated subcommands and
replaces underscores within segments with hyphens: pane.set_title becomes
pane set-title. The exact dotted wire spelling MUST also be accepted as
one command token, for compatibility with RFC-0002 examples. Both routes
call the same handler. A verb may be a prefix of another (skill and
skill.install); a following non-option token selects a child, otherwise
the prefix verb executes. Unknown command tokens are usage errors.
Parameter names match [a-z][a-z0-9_]*. Each top-level parameter becomes
--name, underscores replaced with hyphens. All parameters are named:
format 1 has no positional parameters, short aliases or implicit stdin.
- Strings and numbers take a value, as
--text hior--text=hi. Values starting with-MUST use=. Integers use signed decimal digits; numbers use JSON number syntax. Non-finite and out-of-range values fail. - Booleans require
--enabled=trueor--enabled=false; there is no implicit negation alias. Null uses the literalnull. - Arrays and objects take one JSON-encoded value, shell-quoted as needed. Repeated flags fail rather than silently overwriting or appending.
- Options can follow any command token and precede or follow other options;
required values consume their next token.
--ends option parsing; since there are no positional parameters, further tokens are usage errors.
Generators MUST reject mapped-name collisions, including global flag names
json, socket, help, timeout and the reserved bridge verb.
--json is a boolean switch on every command, --socket takes a path;
--help prints generated help. These are client options, never params.
A parameter named timeout is reserved for a future shared deadline policy;
format 1 does not invent a deadline flag. Global options may precede the
command. Unknown flags and malformed values exit 2; with --json they emit
the standard error wrapper on stdout. Successful help with --json returns
{"help":"..."} with the generated text as its string value.
Non-streaming --json emits only the ok payload, or {"error":...}.
Streaming emits each item as NDJSON, suppresses the final successful ok,
and emits an error wrapper if the stream fails (RFC-0002 section 7.3).
The final ok is still validated and determines exit status. This means
stream summaries are available on the wire but not in CLI NDJSON; generators
MUST document this distinction. Interrupts use RFC-0002 cancellation.
5. MCP mapping
<tool> mcp is a generated MCP stdio adapter, with protocol negotiation
provided by the MCP implementation. This mapping targets the tools model
of MCP revision 2025-06-18 (including structured tool results). The tool
catalog is fixed for the adapter’s lifetime; changing schemas requires a
restart. Diagnostics go to stderr, never MCP stdout.
MCP tool names encode each _ as __ and each . as _d, leaving other
characters unchanged. The reversible mapping avoids collisions between
pane.split → pane_dsplit and pane_split → pane__split. Names longer
than 128 bytes after encoding MUST fail generation. The tool’s own name is
not prefixed: MCP server identity distinguishes binaries.
summary becomes description. inputSchema is the object formed from
params; field summaries become property descriptions, defaults become
JSON Schema default, and required fields populate required (including
an empty array when none). The adapter MUST insert defaults itself:
JSON Schema annotations do not perform insertion.
All successful structuredContent values use an object wrapper, so even
scalar wire results have an object output schema. Non-streaming output is
{"result": <ok>}. Streaming output is
{"items": [<item>, ...], "result": <final ok>}. Generate a closed object
outputSchema with those required properties and the corresponding type
descriptors. The adapter also serializes the wrapper into a text content
block. For the echo manifest, the generated catalog entry is:
{
"name": "echo",
"description": "Return the supplied text",
"inputSchema": {
"type": "object",
"properties": {
"text": {"type":"string","description":"Text to return"},
"style": {"type":"string","enum":["plain","upper"],"description":"Output casing","default":"plain"}
},
"required": ["text"],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"result": {
"type":"object",
"properties":{"text":{"type":"string","description":"Returned text"}},
"required":["text"],
"additionalProperties":false
}
},
"required":["result"],
"additionalProperties":false
}
}
A call and successful response (after MCP initialization):
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}
{"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"{\"result\":{\"text\":\"hi\"}}"}],"structuredContent":{"result":{"text":"hi"}},"isError":false}}
Invalid MCP envelopes use JSON-RPC errors; an unknown tool is -32602.
Once a known tool is selected, validation and execution failures return
isError: true with a text block containing the JSON {"error":...}
wrapper. Omit structuredContent on failure, since it would violate the
success output schema. Preserve the Venom error code and data verbatim.
MCP tool calls do not provide an unbounded item channel. The adapter buffers
streams to completion with hard limits of 1024 items and 1 MiB of compact
JSON item bytes per call. Crossing either limit cancels upstream, discards
the buffer and returns unsupported.stream_limit; it MUST NOT return a
truncated success. These limits MUST appear in streaming tool descriptions.
Unbounded events consumers SHOULD use events --json; MCP clients can
cancel outstanding calls, which maps to upstream cancellation. Adapter
connections MUST negotiate cancel; if unavailable, streaming tool calls
fail with unsupported.capability before dispatch.
6. Documentation, completions and skills
Generators MUST order verbs lexicographically by wire name and preserve
field, enum-choice and example declaration order. Markdown and man pages
include every verb’s canonical CLI spelling, wire/MCP name, summary,
parameter types/required/default/enum values, result and item shapes,
grant, errors with exit codes, exit policy and examples. Man-page command
paths use hyphens (venomx-pane-split); any filename collision MUST fail.
User-supplied strings MUST be escaped for Markdown, roff and shell contexts.
zsh, bash and fish completions MUST derive the same subcommand tree and flags, including the dotted aliases and global options. They offer enum values and boolean literals, identify value-taking options, and omit already used flags. They MUST NOT contact the daemon, execute handlers, suggest secrets, or invent filesystem completion for plain string fields. Shell-specific registration is generated for the manifest’s tool name.
<tool> skill renders UTF-8 SKILL.md deterministically, with YAML front
matter name: <tool> and a safely quoted description from root summary.
The body contains invocation conventions, the read-only default, a compact
verb/grant table, per-verb parameters/defaults/results/errors, streaming
limits, and CLI examples generated from examples. Example params become
flags using POSIX shell quoting; examples MUST NOT contain credentials.
Neither generation nor display executes example commands. Shared
skill.install uses this exact content; target locations and installation
policy are owned by RFC-0001 (COM-199). skill --json returns
{"content":"<complete SKILL.md text>"}. The shared skill handler’s
schema MUST describe that object; human output prints only content.
7. Shared verbs, digest and evolution
The compiler merges standard declarations with product declarations before
validation. RFC-0002’s hello, cancel, ping remain implemented by proto;
its versioned metadata is included in the model, not redefined by products.
The standard agent-contract bundle supplies serve, mcp, skill,
skill.install, doctor, and config.get|set|check|reload|schema; the event
bundle supplies events. Their detailed behavior belongs to RFC-0001 and
RFC-0003. Products MUST NOT override a shared wire name. Local bootstrap
handlers (serve, mcp) dispatch in-process and MUST NOT be exposed as
remote MCP tools or daemon requests. Shared bundle metadata identifies
these locally; product format-1 verbs always dispatch to the daemon.
Core hello and cancel are transport operations, not user MCP tools;
ping and the other remote shared verbs use the normal mappings.
For RFC-0002 hello.schema, compute sha256: followed by 64 lowercase hex
digits over canonical JSON of the complete normalized manifest, including
the shared metadata and examples. Normalization expands all defaults above,
represents enum literals as strings and tuples as arrays, and sorts verbs
by wire name. An absent optional member remains absent except where this
RFC defines a default. nullable is the exception: a descriptor includes
it only when true, so adding it changed no existing digest. JSON object keys sort by UTF-8 byte order; arrays
preserve order; there is no whitespace or trailing newline. Strings escape
only quotes, backslashes and controls (controls as lowercase \u00xx).
Integer values use decimal with no leading zero, negative zero becomes 0;
non-integral numbers use the shortest round-trip decimal representation
with lowercase e, no exponent plus sign or leading exponent zeros.
This digest identifies the whole generated surface, including documentation;
it is not an authorization token or a compatibility version.
A CLI MUST compare an advertised digest with its compiled digest and report
a mismatch through doctor; normal calls continue with daemon validation.
An absent digest is not itself an error. Documentation-only changes may
change the digest. Tool releases MUST treat removed/renamed verbs, narrowed
types, new required params, changed defaults, result changes and stronger
grants as breaking API changes. Additive optional inputs with unchanged
semantics are compatible. format changes only for incompatible manifest
syntax/meaning; generators MUST reject unsupported versions rather than
partially interpreting them. Shared declarations participate in the same
validation and digest rules.
Agent-contract impact
- Verbs: one declarative model generates every interface; dotted names align with RFC-0002. No new product behavior is introduced by this RFC.
--json/ exits: preserves RFC-0002’s payloads, streams and exit table; defines validation and optional successful result status passthrough.- Events:
eventsdeclares an item descriptor; its envelope, filters and replay remain RFC-0003’s responsibility. No new event types. skill,doctor,config: shared bundles use the same schema;skillis generated,doctorreports digest mismatch, config’s runtime schema remains separate from this build-time verb manifest.- Grants: explicit per-verb requirement, enforced by daemon dispatch; read-only by default for unknown callers. RFC-0005 defines scope/identity.
- Headless / OSC: generation and dispatch need no TUI. No change to OSC 7501 / OSC 133 ingestion or event conversion.
Alternatives
- Only comptime Zig: convenient for handlers, but makes extracting the contract depend on compiling arbitrary code. Accept the equivalent model, while standardizing a data-only ZON interchange manifest.
- JSON or YAML source: workable but duplicates the ecosystem’s ZON tooling; generated JSON Schema remains the MCP-facing representation.
- Hyphenated wire names: obscures groups; dotted names already appear
in RFC-0002. CLI spaces preserve familiar
config getusage. - MCP progress notifications for each stream item: progress is not a typed durable item channel. Bounded buffering has explicit failure and leaves NDJSON available for long-lived subscriptions.
- Do nothing / hand-maintain surfaces: violates the one-schema contract and allows completion and permission metadata to diverge from execution.
Open questions
- Naming: resolved by maintainer acceptance (2026-11-06): dotted wire names, spaced CLI paths, reversible MCP escaping.
- MCP streams: resolved by maintainer acceptance (2026-11-06): bounded buffering for v1; no new streaming transport is introduced.
- Shared verb semantics: deferred to existing COM-199 (RFC-0001) and COM-202 (RFC-0003); this RFC defines their integration, not their behavior.
- Grant scopes: deferred to existing COM-204 (RFC-0005); the manifest declares the required right, while the runtime checks resource scope.