{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/liamk/tocat/tocat.schema.json",
"title": "tocat config",
"description": "Configuration for tocat. Values here are overridden by command-line flags.",
"type": "object",
"additionalProperties": false,
"properties": {
"source": {
"$ref": "#/$defs/endpoint",
"description": "Where to read from."
},
"sink": {
"$ref": "#/$defs/endpoint",
"description": "Where to write to."
},
"plugin": {
"type": "array",
"description": "Pipeline entries, in order. Each is instantiated once per direction it applies to. On the sink-to-source path the list is mirrored, so [a, b] nests as a(b(payload)) in both directions. Entries here run before any given on the command line.",
"items": {
"$ref": "#/$defs/plugin"
}
},
"buffer-size": {
"description": "Bytes per copy. A plain byte count or a binary suffix ('64k', '1MiB'). One buffer per direction per connection, so under `fork` this multiplies: 1 MiB across 1024 connections is 2 GiB resident. Defaults to 256 KiB. Overridden by -b. On a datagram endpoint this is also the maximum message size: a larger datagram is truncated by the kernel.",
"default": "256KiB",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"progress": {
"description": "When to draw the pv-style progress line on stderr. 'auto' draws it only when stderr is a terminal; 'always' draws it regardless, as `pv --force` does, which is what you want when stderr is redirected to a file. Defaults to 'never'. Overridden by -P/--progress. The line reports bytes moved at the endpoints, elapsed time and rate, and adds a bar, a percentage and an ETA when the source is a regular file and neither endpoint forks.",
"default": "never",
"type": "string",
"enum": [
"never",
"auto",
"always"
]
},
"log-level": {
"$ref": "#/$defs/log-level",
"description": "Log verbosity"
},
"log": {
"type": "array",
"description": "Log destinations. Defaults to compact output on stderr.",
"items": {
"$ref": "#/$defs/log-sink"
}
}
},
"$defs": {
"plugin": {
"title": "plugin",
"description": "A pipeline entry. One complete variant per known plugin rather than a shared base plus conditional fragments: editors only offer completions from `properties` on the schema they resolve, so anything behind allOf or if/then is invisible to them. The variants are mutually exclusive on `name`.",
"oneOf": [
{
"$ref": "#/$defs/plugin-tee"
},
{
"$ref": "#/$defs/plugin-compress"
},
{
"$ref": "#/$defs/plugin-decompress"
},
{
"$ref": "#/$defs/plugin-process"
},
{
"$ref": "#/$defs/plugin-rate"
},
{
"$ref": "#/$defs/plugin-limit"
},
{
"$ref": "#/$defs/plugin-throttle"
},
{
"$ref": "#/$defs/plugin-block"
},
{
"$ref": "#/$defs/plugin-timeout"
},
{
"$ref": "#/$defs/plugin-wasm"
},
{
"$ref": "#/$defs/plugin-other"
}
]
},
"plugin-direction": {
"type": "string",
"default": "both",
"description": "Which path this entry applies to. 'both' builds two independent instances, one per direction, so per-direction state never leaks across paths. Prefer 'forward'/'reverse' over the 'source'/'sink' aliases: 'sink' means sink-to-source, which reads like the opposite of what it does.",
"enum": [
"both",
"forward",
"reverse",
"source-to-sink",
"sink-to-source",
"src-to-sink",
"sink-to-src",
"source",
"sink",
"src",
"in",
"out",
"bidi",
"bidirectional",
"duplex",
"all"
]
},
"log-format": {
"type": "string",
"default": "compact",
"enum": [
"json",
"pretty",
"compact"
]
},
"log-rotation": {
"type": "string",
"enum": [
"minutely",
"hourly",
"daily",
"never"
],
"default": "never",
"description": "Log rotation interval. When set, 'path' is treated as a directory and log files will append timestamps."
},
"log-sink-stderr": {
"title": "stderr",
"type": "object",
"additionalProperties": false,
"required": [
"type"
],
"properties": {
"type": {
"const": "stderr"
},
"format": {
"$ref": "#/$defs/log-format"
},
"level": {
"$ref": "#/$defs/log-level"
}
}
},
"log-sink-file": {
"title": "file",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"const": "file"
},
"format": {
"$ref": "#/$defs/log-format"
},
"level": {
"$ref": "#/$defs/log-level"
},
"path": {
"type": "string"
},
"truncate": {
"type": "boolean",
"default": false,
"description": "Truncate existing log file. Default is to append. Ignored if not using file rotation."
},
"rotation": {
"$ref": "#/$defs/log-rotation"
},
"max_files": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of rotated log files to retain. Older logs are deleted automatically."
}
}
},
"log-sink": {
"oneOf": [
{
"$ref": "#/$defs/log-sink-stderr"
},
{
"$ref": "#/$defs/log-sink-file"
}
]
},
"log-level": {
"type": "string",
"default": "info",
"description": "Log verbosity. Overridden by -v on the command line if that asks for more, and by RUST_LOG entirely.",
"oneOf": [
{
"const": "off",
"description": "No output."
},
{
"const": "error",
"description": "Failures only."
},
{
"const": "warn",
"description": "Failures and recoverable problems."
},
{
"const": "info",
"description": "Connections opened and closed. The default."
},
{
"const": "debug",
"description": "Config resolution, resolved plugin chains, and per-transfer detail. Same as -v."
},
{
"const": "trace",
"description": "Everything, including per-chunk copying. Same as -vv."
}
]
},
"endpoint": {
"description": "Either a URL shorthand, or a table with an explicit `type`.",
"oneOf": [
{
"$ref": "#/$defs/endpoint-url"
},
{
"$ref": "#/$defs/exec-endpoint"
},
{
"$ref": "#/$defs/file-endpoint"
},
{
"$ref": "#/$defs/pipe-endpoint"
},
{
"$ref": "#/$defs/stdio-endpoint"
},
{
"$ref": "#/$defs/system-endpoint"
},
{
"$ref": "#/$defs/tcp-endpoint"
},
{
"$ref": "#/$defs/tcp-listen-endpoint"
},
{
"$ref": "#/$defs/udp-endpoint"
},
{
"$ref": "#/$defs/udp-listen-endpoint"
},
{
"$ref": "#/$defs/unix-endpoint"
},
{
"$ref": "#/$defs/unix-listen-endpoint"
}
]
},
"endpoint-url": {
"title": "shorthand",
"type": "string",
"description": "URL form, equivalent to the table form with defaults.",
"examples": [
"tcp:127.0.0.1:9000",
"tcplisten:8000",
"listen:8000,fork",
"udp:127.0.0.1:5353",
"udp-listen:9000",
"unix:/tmp/tocat.sock",
"unix-listen:/tmp/tocat.sock,fork,unlink,mode=660",
"pipe:/tmp/events,create",
"stdio",
"-"
]
},
"tcp-endpoint": {
"title": "tcp",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"addr"
],
"properties": {
"type": {
"type": "string",
"enum": [
"tcp",
"TCP",
"tcp-connect",
"TCP-CONNECT",
"connect",
"CONNECT"
],
"description": "tcp-connect type tag supporting standard, uppercase, and alias variations."
},
"addr": {
"type": "string",
"description": "Host and port, e.g. 127.0.0.1:9000.",
"examples": [
"127.0.0.1:9000",
"[::1]:9000"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"tcp-listen-endpoint": {
"title": "tcp-listen",
"type": "object",
"additionalProperties": false,
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"tcp-listen",
"TCP-LISTEN",
"tcplisten",
"TCPLISTEN",
"listen",
"LISTEN"
],
"description": "Listen type tag supporting standard, uppercase, and alias variations."
},
"host": {
"type": "string",
"default": "127.0.0.1",
"description": "Interface to bind. An address rather than a hostname: `localhost` resolves to a list and only the first entry is bound, which silently picks ::1 on some machines and 127.0.0.1 on others. Use 0.0.0.0 or [::] for all interfaces."
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"default": 8000,
"description": "Port number to listen on."
},
"fork": {
"type": "boolean",
"default": false,
"description": "Spawn a new task for each incoming client connection. Each connection gets its own plugin instances; only side-channel writers are shared."
},
"max-connections": {
"type": "integer",
"minimum": 1,
"default": 1024,
"description": "Maximum number of concurrent connections. Only applies when fork is true."
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"udp-endpoint": {
"title": "udp",
"type": "object",
"description": "Send datagrams to a fixed peer. UDP has no connection: this only fixes where messages go and which sender is accepted.",
"additionalProperties": false,
"required": [
"type",
"addr"
],
"properties": {
"type": {
"type": "string",
"enum": [
"udp",
"UDP",
"udp-connect",
"UDP-CONNECT"
],
"description": "udp-connect type tag supporting standard, uppercase, and alias variations."
},
"addr": {
"type": "string",
"description": "Host and port of the peer.",
"examples": [
"127.0.0.1:5353",
"[::1]:9000",
"collector.internal:9000"
]
},
"bind": {
"type": "string",
"description": "Local address to bind before fixing the peer. Defaults to an ephemeral port on the wildcard address of the peer's family, so a v6 peer gets a v6 socket.",
"examples": [
"0.0.0.0:0",
"127.0.0.1:5000"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"udp-listen-endpoint": {
"title": "udp-listen",
"type": "object",
"description": "Receive datagrams. The first sender becomes the peer for the rest of the run and everyone else is ignored: there is no per-sender demultiplexing, so `fork` does not apply here yet.",
"additionalProperties": false,
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"udp-listen",
"UDP-LISTEN",
"udplisten",
"UDPLISTEN"
],
"description": "udp-listen type tag supporting standard, uppercase, and alias variations."
},
"host": {
"type": "string",
"default": "127.0.0.1",
"description": "Interface to bind. An address rather than a hostname: `localhost` resolves to a list and only the first entry is bound, which silently picks ::1 on some machines and 127.0.0.1 on others. Use 0.0.0.0 or [::] for all interfaces."
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"default": 8000,
"description": "Port to bind."
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"unix-endpoint": {
"title": "unix",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix",
"UNIX",
"unix-connect",
"UNIX-CONNECT"
],
"description": "Unix domain socket connect tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Path to the socket to connect to.",
"examples": [
"/tmp/tocat.sock",
"/run/app/app.sock"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"unix-listen-endpoint": {
"title": "unix-listen",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix-listen",
"UNIX-LISTEN",
"unixlisten",
"UNIXLISTEN"
],
"description": "Unix domain socket listen tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Path of the socket to create and listen on.",
"examples": [
"/tmp/tocat.sock",
"/run/app/app.sock"
]
},
"unlink": {
"type": "boolean",
"default": false,
"description": "Remove a stale socket file before binding. tocat probes the path first and refuses to unlink a socket with a live listener."
},
"mode": {
"type": "string",
"pattern": "^0?[0-7]{3}$",
"description": "Permissions applied to the socket after bind, as an octal string.",
"examples": [
"600",
"660",
"666",
"0660"
]
},
"fork": {
"type": "boolean",
"default": false,
"description": "Spawn a new task for each incoming client connection. Each connection gets its own plugin instances; only side-channel writers are shared."
},
"max-connections": {
"type": "integer",
"minimum": 1,
"default": 1024,
"description": "Maximum number of concurrent connections. Only applies when fork is true."
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"stdio-endpoint": {
"title": "stdio",
"type": "object",
"additionalProperties": false,
"required": [
"type"
],
"properties": {
"type": {
"const": "stdio"
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"file-endpoint": {
"title": "file",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"file",
"FILE",
"open",
"OPEN"
],
"description": "File type tag. `open` is an alias; both name the same thing."
},
"path": {
"type": "string",
"description": "File to read from when used as the source, or write to when used as the sink."
},
"append": {
"type": "boolean",
"default": false,
"description": "Append rather than overwrite. Sink only."
},
"create": {
"type": "boolean",
"default": true,
"description": "Create the file if it does not exist. Sink only."
},
"truncate": {
"type": "boolean",
"default": false,
"description": "Truncate on open. Ignored when appending. Sink only."
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"pipe-endpoint": {
"title": "pipe",
"type": "object",
"description": "A named pipe (FIFO). Unidirectional, like `file:`, but a rendezvous rather than storage: it is read as the source and written as the sink.",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"pipe",
"PIPE",
"fifo",
"FIFO"
],
"description": "Pipe type tag. `fifo` is an alias; both name the same thing."
},
"path": {
"type": "string",
"description": "Path of the FIFO.",
"examples": [
"/tmp/events",
"/run/app/events"
]
},
"create": {
"type": "boolean",
"default": true,
"description": "mkfifo the path if it is missing. A path that exists but is not a FIFO is an error rather than a fallback."
},
"hold": {
"type": "boolean",
"default": true,
"description": "Keep the FIFO open across producers. tocat opens read-write, so it is its own writer: opening never blocks and the stream never ends, which is what you want for a log or event pipe whose producers come and go. Without it a source blocks until a writer appears and sees EOF when the last one leaves, so one producer, then done."
},
"size": {
"description": "Kernel FIFO capacity. Linux only, best-effort: rounded up to a power of two, and capped by /proc/sys/fs/pipe-max-size for unprivileged processes. Unrelated to `buffer-size`, which decides when the *producer* blocks. Left at the kernel default (64 KiB) when absent, because a named FIFO is shared with whoever else has it open.",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"unlink": {
"type": "boolean",
"default": false,
"description": "Remove the FIFO when the relay finishes."
},
"mode": {
"type": "string",
"pattern": "^0?[0-7]{3}$",
"description": "Permissions applied after creation, as an octal string. Applied explicitly, so it is not masked by umask.",
"examples": [
"600",
"660",
"666"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"exec-endpoint": {
"title": "exec",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"argv"
],
"properties": {
"type": {
"const": "exec"
},
"argv": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Program and arguments. Passed directly to the program: no shell, no globbing, no metacharacters.",
"examples": [
[
"/usr/bin/env",
"cat"
],
[
"python3",
"-u",
"server.py"
]
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"system-endpoint": {
"title": "system",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"command"
],
"properties": {
"type": {
"const": "system"
},
"command": {
"type": "string",
"description": "Program and arguments. Runs as the user's default shell.",
"examples": [
"echo $HOME",
"cat"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"plugin-tee": {
"title": "tee",
"type": "object",
"description": "Mirror this path's bytes to a file or stderr. Never modifies the payload.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "tee",
"description": "Selects the tee plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"file": {
"type": "string",
"description": "Where to write. Omit, or use '-', 'stderr' or '/dev/stderr', for stderr. Must not be stdout, which may carry relayed payload. Two entries naming the same file share one writer, so their output interleaves at chunk granularity rather than racing.",
"not": {
"enum": [
"stdout",
"/dev/stdout",
"/dev/fd/1"
]
}
},
"format": {
"type": "string",
"default": "raw-binary",
"description": "'hex' writes offset/hex/ASCII rows behind a '[source -> sink | stage]' header. 'raw-binary' writes the bytes unmodified, which is byte-exact and replayable but has no header, so two raw tees sharing a file cannot be separated.",
"enum": [
"hex",
"raw-binary",
"raw",
"binary"
]
},
"append": {
"type": "boolean",
"default": true,
"description": "Append to an existing file rather than truncating it."
},
"label": {
"type": "string",
"description": "Override the hex header label, which is otherwise '<source> -> <sink> | <stage name>'. Prefer 'as', which names the instance everywhere rather than only here."
},
"width": {
"type": "integer",
"minimum": 1,
"default": 16,
"description": "Bytes per row in hex mode."
}
}
},
"plugin-compress": {
"title": "compress",
"type": "object",
"description": "zstd-compress this path. Asymmetric: pair it with a decompress entry on the opposite direction rather than using direction = 'both'.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "compress",
"description": "Selects the compress plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"level": {
"type": "integer",
"minimum": 1,
"maximum": 22,
"default": 3,
"description": "zstd level. Higher is smaller and slower."
},
"flush": {
"type": "boolean",
"default": true,
"description": "Flush after every chunk so bytes reach the peer immediately. Costs ratio, because a flush closes the current block. Turn it off only for bulk transfers where nothing is waiting on a prompt."
},
"report": {
"type": "boolean",
"default": false,
"description": "Log the compression ratio when the stream ends."
}
}
},
"plugin-decompress": {
"title": "decompress",
"type": "object",
"description": "zstd-decompress this path.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "decompress",
"description": "Selects the decompress plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"report": {
"type": "boolean",
"default": false,
"description": "Log the expansion ratio when the stream ends."
}
}
},
"plugin-process": {
"title": "process",
"type": "object",
"description": "Pipe this path through a subprocess: the relay's bytes on its stdin, its stdout continuing downstream. Give one of `argv` or `command`. The most expensive stage available (two pipe crossings per chunk, and one child per connection per direction) so prefer a compiled plugin for anything hot.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "process",
"description": "Selects the process plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"const": true,
"description": "A subprocess always has its own task, so this cannot be false. Present only for symmetry with other entries."
},
"argv": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Program and arguments, passed directly: no shell, no globbing, no metacharacters. Alternative to `command`. Not expressible on the command line, where only `command` is available.",
"examples": [
[
"gzip",
"-c"
],
[
"jq",
"-c",
"."
],
[
"/usr/bin/env",
"cat"
]
]
},
"command": {
"type": "string",
"description": "A shell command line, as the `system:` endpoint. Runs with tocat's privileges, so do not build one from untrusted input, or accept one from a config file others can write. Alternative to `argv`.",
"examples": [
"gzip -c",
"grep -v DEBUG | sort -u"
]
},
"stderr": {
"type": "string",
"default": "log",
"description": "What to do with the child's stderr. 'log' captures it and re-emits each line as a warning tagged with the stage name. 'inherit' lets it reach tocat's own stderr, where it interleaves with logs and dumps. 'null' discards it.",
"enum": [
"log",
"inherit",
"null"
]
}
}
},
"plugin-rate": {
"title": "rate",
"type": "object",
"description": "Measure and report throughput at this point on the path. Never modifies the payload. Reports are driven by traffic rather than a clock, so a stalled stream is silent until it resumes; for a live display use the progress line instead.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "rate",
"description": "Selects the rate plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"interval": {
"description": "How often to report: a number of seconds, or a string with a suffix ('500ms', '30s', '2m'). Reports are driven by a timer, so they arrive on schedule and an idle stream is reported as stalled rather than staying silent. 0 disables the timer, leaving only the end-of-stream summary. Note that a ticking stage costs one timer per direction per connection, which multiplies under fork.",
"default": "5s",
"oneOf": [
{
"type": "number",
"minimum": 0
},
{
"type": "string",
"pattern": "^ *[0-9]+(\\.[0-9]+)? *(ms|s|sec|secs|m|min|mins)? *$"
}
]
},
"unit": {
"type": "string",
"default": "bytes",
"description": "'bytes' reports binary multiples of bytes (1.23GiB, 10.2MiB/s). 'bits' reports decimal multiples of bits (9.84Gbit, 81.6Mbit/s), the way network equipment is specified.",
"enum": [
"bytes",
"byte",
"bits",
"bit"
]
},
"summary": {
"type": "boolean",
"default": true,
"description": "Log a total, average and peak when the stream ends. A datagram source has no end of stream, so nothing is logged there however this is set; its periodic reports are unaffected."
},
"level": {
"type": "string",
"default": "info",
"description": "Level the reports are logged at. Reports are logs, not payload, so they follow the log sinks and the log level.",
"enum": [
"trace",
"debug",
"info",
"warn"
]
},
"file": {
"description": "Write the periodic samples here as CSV rather than logging them: one row per sample, 'stage,elapsed,total,delta,rate', no header. Idle windows are written too, since a gap in a time series reads as missing data rather than as a stall; the log suppresses repeats instead. The summary is logged either way. Use '-', 'stderr' or '/dev/stderr' for stderr. Must not be stdout, which may carry relayed payload.",
"type": "string",
"not": {
"enum": [
"stdout",
"/dev/stdout",
"/dev/fd/1"
]
}
},
"append": {
"type": "boolean",
"default": true,
"description": "Append to an existing sample file rather than truncating it."
}
}
},
"plugin-other": {
"title": "other",
"type": "object",
"description": "A plugin this schema does not describe. Its options are passed through unvalidated; the binary rejects unknown ones at startup. Run `tocat --list-plugins` to see what your build has.",
"additionalProperties": true,
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"not": {
"enum": [
"tee",
"compress",
"decompress",
"process",
"rate"
]
},
"description": "Plugin to instantiate."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
}
}
},
"plugin-block": {
"title": "block",
"type": "object",
"description": "Accumulate the path into fixed-size blocks. Each block is emitted as one unit, so it is delivered on its own rather than concatenated with its neighbours: on a byte sink that decides where the writes fall, on a datagram sink each block is one message, and across a detached boundary each block is one call to the segment below. This is `dd`'s `obs`, and it is what a tape drive, a raw device or an O_DIRECT file wants. Framing is not free: every stage below is called once per block rather than once per chunk, so a small size against a large buffer runs the rest of the pipeline many times per read.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "block",
"description": "Selects the block plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"size": {
"description": "Bytes to accumulate before emitting. A plain byte count or a binary suffix ('512', '64k', '1MiB'). Must be greater than zero. A full block never waits for `flush`: it goes out as soon as it fills.",
"default": 4096,
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"flush": {
"description": "How long a partial block may wait: a number of seconds, or a string with a suffix ('250ms', '2s', '1m'). Absent, a short block is held until end of stream, which is what a device wants and an interactive stream does not. 0 emits whatever is in hand on every write, which caps latency at the cost of short blocks. The bound is measured from the first byte held, because the stage restarts its own timer when a block starts filling. Note that a ticking stage costs one timer per direction per connection, which multiplies under fork.",
"oneOf": [
{
"type": "number",
"minimum": 0
},
{
"type": "string",
"pattern": "^ *[0-9]+ *(ns|us|ms|s|m|h|d|w)? *$"
}
]
},
"pad": {
"type": "boolean",
"default": false,
"description": "Pad a short block out to `size` with zero bytes. Only short blocks are affected, which in practice means the block at end of stream and any cut short by `flush`; a full block is already `size` bytes."
}
}
},
"plugin-limit": {
"title": "limit",
"type": "object",
"description": "End the transfer after a fixed number of bytes. Reaching the limit stops the read the way upstream end of stream would: emitted bytes are written, the remaining stages are finished and flushed, and tocat exits successfully. `at-limit` decides what happens to the one chunk that straddles the limit.",
"additionalProperties": false,
"required": [
"name",
"bytes"
],
"properties": {
"name": {
"const": "limit",
"description": "Selects the limit plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"bytes": {
"description": "How many bytes to let past before ending the stream. A plain byte count or a binary suffix ('64k', '1MiB'). Counted at this stage's own position, and per direction: with direction 'both' each path gets its own budget.",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"at-limit": {
"type": "string",
"default": "exact",
"description": "What to do with the one chunk that crosses the limit. 'drop' discards it whole, so at most `bytes` are passed on. 'exact' splits it, so exactly `bytes` are passed on. 'overshoot' forwards it whole, so at least `bytes` are passed on and no message is cut in half. Splitting is the only mode that is unsafe on a datagram path.",
"enum": [
"drop",
"exact",
"overshoot"
]
}
}
},
"plugin-wasm": {
"title": "wasm",
"type": "object",
"description": "Run a WebAssembly guest as a stage. The guest imports nothing: no clock, no files, no sockets, no host functions of any kind, and a module that imports so much as WASI is refused when it is loaded. It forwards, rewrites, frames, logs, paces and halts through the same effect queue every native stage uses. The module is compiled once per process; an instance and a linear memory are per direction per connection, so both multiply under fork.",
"additionalProperties": false,
"required": [
"name",
"module"
],
"properties": {
"name": {
"const": "wasm",
"description": "Selects the wasm plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"default": true,
"description": "Override the plugin's default placement. A guest call is expensive enough per byte that this defaults to true; false runs it inline on the reading task."
},
"module": {
"type": "string",
"description": "Path to the .wasm module. Compiled once per process however many stages and connections name it. Aliases: path, file."
},
"config": {
"type": "object",
"description": "Handed to the guest verbatim, as JSON, once. tocat does not look inside: the guest owns its own options, its own defaults and its own error messages, and a guest that rejects one fails at startup carrying its own message."
},
"fuel": {
"type": "integer",
"minimum": 0,
"default": 100000000,
"description": "Instructions one call may cost before the guest is trapped, which fails that direction. Per call rather than per connection, so a long transfer cannot exhaust it. 0 is unmetered, and gives up the guarantee that a guest cannot hang the relay: a stage runs on the copy task and nothing else on that task moves while a guest is inside it."
},
"memory-max": {
"description": "Ceiling on the guest's linear memory. Per instance, and an instance is per direction per connection, so this is the number that multiplies under fork.",
"default": "64MiB",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
}
}
},
"plugin-timeout": {
"title": "timeout",
"type": "object",
"description": "End this path when it has carried nothing for `timeout`. The payload is untouched and the stage never emits: reaching the timeout stops the read, which the relay treats as upstream end of stream arriving early, so buffered stages are drained, sinks are flushed and tocat exits successfully. The window is per path, so with direction 'both' each direction times out on its own traffic. Note that a ticking stage costs timers per direction per connection, which multiplies under fork.",
"additionalProperties": false,
"required": [
"name",
"timeout"
],
"properties": {
"name": {
"const": "timeout",
"description": "Selects the timeout plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"timeout": {
"description": "How long the path may go without carrying a byte before it is ended: a number of seconds, or a string with suffixes ('30s', '2m', '1m30s'). Must be greater than zero. The halt lands within a quarter of the timeout of where it was asked for, or within one whole timeout below 400ms, where the stage stops subdividing the window. Aliases: wait, inactivity, idle.",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *([0-9]+ *(ns|us|ms|s|m|h|d|w)? *)+$"
}
]
}
}
},
"plugin-throttle": {
"title": "throttle",
"type": "object",
"description": "Hold this path to a bandwidth ceiling. Nothing is buffered: chunks pass through untouched and the relay waits before reading again, so a slow reader closes the receive window and the sender slows at source. The wait lands between reads, so pacing is smooth only when the buffer size is at or below the per-second rate.",
"additionalProperties": false,
"required": [
"name",
"rate"
],
"properties": {
"name": {
"const": "throttle",
"description": "Selects the throttle plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and as the default label for stages that print one. Without it a stage is named after its plugin, with #1, #2 appended when the same plugin appears twice on one path."
},
"detach": {
"type": "boolean",
"description": "Override the plugin's default placement. True runs this stage on its own task behind a bounded channel: one copy and one wakeup per chunk, worth it only for stages that are expensive per byte."
},
"rate": {
"description": "The ceiling, in bytes per second. A plain byte count or a binary suffix ('64k', '1MiB'). Applied per direction: with direction 'both' each path gets its own ceiling.",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"burst": {
"description": "How much unused allowance may accumulate, so a path that has been idle can resume at full speed briefly rather than from nothing. Defaults to one second of `rate`.",
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
}
}
}
}
}