{
"$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-base64"
},
{
"$ref": "#/$defs/plugin-unbase64"
},
{
"$ref": "#/$defs/plugin-frame"
},
{
"$ref": "#/$defs/plugin-unframe"
},
{
"$ref": "#/$defs/plugin-process"
},
{
"$ref": "#/$defs/plugin-rate"
},
{
"$ref": "#/$defs/plugin-limit"
},
{
"$ref": "#/$defs/plugin-throttle"
},
{
"$ref": "#/$defs/plugin-block"
},
{
"$ref": "#/$defs/plugin-hash"
},
{
"$ref": "#/$defs/plugin-timeout"
},
{
"$ref": "#/$defs/plugin-wasm"
},
{
"$ref": "#/$defs/plugin-other"
}
]
},
"plugin-direction": {
"type": "string",
"default": "source-to-sink",
"description": "Which path this entry is on. Omitted, it is the forward path, from the source to the sink. 'both' instantiates the plugin once per path, which is two of whatever it counts: a byte budget each way, a timer each way. An asymmetric stage such as compress is never 'both'; its inverse is declared separately with direction = 'reverse'.",
"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/pty-endpoint"
},
{
"$ref": "#/$defs/pty-exec-endpoint"
},
{
"$ref": "#/$defs/stdio-endpoint"
},
{
"$ref": "#/$defs/system-endpoint"
},
{
"$ref": "#/$defs/tcp-endpoint"
},
{
"$ref": "#/$defs/tcp-listen-endpoint"
},
{
"$ref": "#/$defs/tty-endpoint"
},
{
"$ref": "#/$defs/udp-endpoint"
},
{
"$ref": "#/$defs/udp-listen-endpoint"
},
{
"$ref": "#/$defs/unix-endpoint"
},
{
"$ref": "#/$defs/unix-listen-endpoint"
},
{
"$ref": "#/$defs/unix-seqpacket-endpoint"
},
{
"$ref": "#/$defs/unix-seqpacket-listen-endpoint"
},
{
"$ref": "#/$defs/unix-dgram-endpoint"
},
{
"$ref": "#/$defs/unix-dgram-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",
"unix-seqpacket:/run/app/app.sock",
"unix-seqpacket-listen:@tocat,fork",
"unix-dgram:/dev/log",
"unix-dgram-listen:/tmp/tocat.sock,unlink",
"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. Without fork the first sender becomes the peer for the rest of the run and everyone else is ignored. With fork the socket is demultiplexed by source address and every sender gets its own session.",
"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."
},
"fork": {
"type": "boolean",
"default": false,
"description": "Serve each sending address separately. Each session gets its own dialled peer and its own plugin instances; only side-channel writers are shared."
},
"max-connections": {
"type": "integer",
"minimum": 1,
"default": 1024,
"description": "Maximum number of concurrent sessions. Only applies when fork is true. Datagrams from a new sender past the ceiling are dropped, and a session ends only when a stage stops it, so pair fork with a timeout plugin on both directions."
},
"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."
}
}
},
"unix-seqpacket-endpoint": {
"title": "unix-seqpacket",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix-seqpacket",
"UNIX-SEQPACKET",
"unix-seqpkt",
"UNIX-SEQPKT",
"uds-seqpacket",
"UDS-SEQPACKET",
"seqpacket",
"SEQPACKET"
],
"description": "Unix seqpacket connect tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Path of the seqpacket socket to connect to. A leading @ names the Linux abstract namespace, which has no filesystem entry: unlink is unnecessary there and mode is rejected, since an abstract name carries no permissions.",
"examples": [
"/tmp/tocat.sock",
"/run/app/app.sock",
"@tocat"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"unix-seqpacket-listen-endpoint": {
"title": "unix-seqpacket-listen",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix-seqpacket-listen",
"UNIX-SEQPACKET-LISTEN",
"unix-seqpkt-listen",
"UNIX-SEQPKT-LISTEN",
"uds-seqpacket-listen",
"UDS-SEQPACKET-LISTEN",
"seqpacket-listen",
"SEQPACKET-LISTEN"
],
"description": "Unix seqpacket listen tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Address of the seqpacket socket to create and listen on. A leading @ names the Linux abstract namespace, which has no filesystem entry: unlink is unnecessary there and mode is rejected, since an abstract name carries no permissions.",
"examples": [
"/tmp/tocat.sock",
"/run/app/app.sock",
"@tocat"
]
},
"unlink": {
"type": "boolean",
"default": false,
"description": "Remove a stale socket before binding. tocat probes the address first and refuses to unlink one with a live owner."
},
"mode": {
"type": "string",
"pattern": "^0?[0-7]{3}$",
"description": "Permissions applied to the socket after bind, as an octal string. Rejected for an abstract address, which has no permissions to set.",
"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."
}
}
},
"unix-dgram-endpoint": {
"title": "unix-dgram",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix-dgram",
"UNIX-DGRAM",
"unix-datagram",
"UNIX-DATAGRAM",
"uds-dgram",
"UDS-DGRAM"
],
"description": "Unix datagram connect tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Address of the peer socket to send to. A leading @ names the Linux abstract namespace, which has no filesystem entry: unlink is unnecessary there and mode is rejected, since an abstract name carries no permissions.",
"examples": [
"/dev/log",
"/run/app/app.sock",
"@tocat"
]
},
"bind": {
"type": "string",
"description": "Local address to bind before connecting, so the peer's replies have somewhere to go. Defaults to a path in the temporary directory, created with mode 600 and removed when the relay ends.",
"examples": [
"/run/app/reply.sock",
"@tocat-reply"
]
},
"unlink": {
"type": "boolean",
"default": false,
"description": "Remove a stale local address before binding. Requires bind, since the generated address is fresh every run and the peer is not tocat's to remove."
},
"mode": {
"type": "string",
"pattern": "^0?[0-7]{3}$",
"description": "Permissions applied to the local address after bind, as an octal string. Rejected for an abstract address, which has no permissions to set.",
"examples": [
"600",
"660",
"666",
"0660"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"unix-dgram-listen-endpoint": {
"title": "unix-dgram-listen",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"type": "string",
"enum": [
"unix-dgram-listen",
"UNIX-DGRAM-LISTEN",
"unix-datagram-listen",
"UNIX-DATAGRAM-LISTEN",
"uds-dgram-listen",
"UDS-DGRAM-LISTEN"
],
"description": "Unix datagram listen tag supporting standard, uppercase, and alias variations."
},
"path": {
"type": "string",
"description": "Address of the socket to create and receive on. A leading @ names the Linux abstract namespace, which has no filesystem entry: unlink is unnecessary there and mode is rejected, since an abstract name carries no permissions.",
"examples": [
"/tmp/tocat.sock",
"/run/app/app.sock",
"@tocat"
]
},
"unlink": {
"type": "boolean",
"default": false,
"description": "Remove a stale socket before binding. tocat probes the address first and refuses to unlink one with a live owner."
},
"mode": {
"type": "string",
"pattern": "^0?[0-7]{3}$",
"description": "Permissions applied to the socket after bind, as an octal string. Rejected for an abstract address, which has no permissions to set.",
"examples": [
"600",
"660",
"666",
"0660"
]
},
"fork": {
"type": "boolean",
"default": false,
"description": "Serve each sending address separately, with its own dialled peer and plugin instances. Senders without an address of their own cannot be told apart or replied to, and their messages are dropped."
},
"max-connections": {
"type": "integer",
"minimum": 1,
"default": 1024,
"description": "Sessions served at once under fork. Messages from a new sender past the ceiling are dropped."
},
"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. On unless turned off, and off under `device`, where asking for both is an error rather than an override."
},
"truncate": {
"type": "boolean",
"default": false,
"description": "Truncate on open. Ignored when appending. Sink only."
},
"device": {
"type": "boolean",
"default": false,
"description": "Require the path to already exist and be a block or character device, and do not create it. Without this a wrong or unplugged device path becomes a regular file of that name when used as the sink. Contradicts create, truncate and append. A terminal is not this: use the tty endpoint, which is duplex and restores the settings it found."
},
"seek": {
"description": "Start at this offset rather than at the beginning: reading from it as the source, writing to it as the sink. A plain byte count or a binary suffix ('512', '64k', '1MiB'). Contradicts append, where every write goes to the end. An offset into a block device that is not a multiple of its block size is warned about rather than refused.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string",
"pattern": "^ *[0-9]+ *([KkMmGg][Ii]?[Bb]?|[Bb])? *$"
}
]
},
"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-base64": {
"title": "base64",
"type": "object",
"description": "Base64-encode this path, one complete message per chunk. Asymmetric: pair it with an unbase64 entry on the opposite direction rather than using direction = 'both'.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "base64",
"description": "Selects the base64 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."
},
"alphabet": {
"enum": [
"standard",
"url-safe"
],
"default": "standard",
"description": "RFC 4648 alphabet. 'standard' uses + and /, 'url-safe' uses - and _. Both ends of a hop must agree."
}
}
},
"plugin-unbase64": {
"title": "unbase64",
"type": "object",
"description": "Base64-decode this path, one complete message per chunk. A chunk that is not a whole number of base64 groups is a configuration error rather than a payload: message boundaries come from the datagram path or from a framing stage ahead of this one.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "unbase64",
"description": "Selects the unbase64 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."
},
"alphabet": {
"enum": [
"standard",
"url-safe"
],
"default": "standard",
"description": "RFC 4648 alphabet. 'standard' uses + and /, 'url-safe' uses - and _. Both ends of a hop must agree."
},
"accept-unpadded": {
"type": "boolean",
"default": false,
"description": "Restore padding the peer omitted instead of rejecting the message. Costs a diagnostic: a message truncated by 1 or 2 characters then decodes to a short payload rather than erroring. A length remainder of 1 is rejected either way."
}
}
},
"plugin-frame": {
"title": "frame",
"type": "object",
"description": "Mark message boundaries on this path. Takes the boundaries it is given, one unit in and one framed unit out, so it belongs on a datagram path or after a stage that declared boundaries of its own. Pair it with an unframe entry on the opposite direction.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "frame",
"description": "Selects the frame 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."
},
"mode": {
"enum": [
"delimiter",
"cobs",
"slip",
"length",
"netstring"
],
"default": "delimiter",
"description": "How messages are marked on the wire; both ends of a hop must agree. 'delimiter' terminates each message with the delimiter byte string. 'cobs' and 'slip' escape the payload so the terminator cannot appear in it, which is exact for arbitrary binary; prefer cobs unless the peer requires slip. 'length' prefixes each message with its length in a fixed-width header. 'netstring' wraps each message as LEN:payload, with a decimal length. The counted modes (length, netstring) can refuse an oversized message from its header, but cannot resynchronise a stream once it desynchronises."
},
"delimiter": {
"type": "string",
"default": "\\n",
"description": "Terminator in delimiter mode. Escapes are decoded here, so \\n, \\r, \\t, \\0, \\\\ and \\xNN all work on the command line as well as in this file. An option another mode ignores is an error rather than a no-op, so this is rejected outside delimiter mode."
},
"length-bytes": {
"enum": [
1,
2,
4,
8
],
"default": 4,
"description": "Width of the header in length mode, in bytes. Rejected outside length mode."
},
"endian": {
"enum": [
"big",
"little"
],
"default": "big",
"description": "Byte order of the header in length mode. Big is network order and what a peer means unless it says otherwise. Rejected outside length mode."
},
"check": {
"type": "boolean",
"default": true,
"description": "Reject a message that would frame as two, because it contains the delimiter or ends with a prefix of one. Costs one scan of each message. Turn it off only for a peer whose parser tolerates the ambiguity. Rejected outside delimiter mode."
}
}
},
"plugin-unframe": {
"title": "unframe",
"type": "object",
"description": "Split this path into messages on their boundaries, emitting each as one unit so that every stage below is called once per message. The counterpart to frame, and what a stage that needs whole messages (unbase64, anything with a per-message header) needs ahead of it on a byte stream.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "unframe",
"description": "Selects the unframe 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."
},
"mode": {
"enum": [
"delimiter",
"cobs",
"slip",
"length",
"netstring"
],
"default": "delimiter",
"description": "How messages are marked on the wire; both ends of a hop must agree. 'delimiter' terminates each message with the delimiter byte string. 'cobs' and 'slip' escape the payload so the terminator cannot appear in it, which is exact for arbitrary binary; prefer cobs unless the peer requires slip. 'length' prefixes each message with its length in a fixed-width header. 'netstring' wraps each message as LEN:payload, with a decimal length. The counted modes (length, netstring) can refuse an oversized message from its header, but cannot resynchronise a stream once it desynchronises."
},
"delimiter": {
"type": "string",
"default": "\\n",
"description": "Terminator in delimiter mode. Escapes are decoded here, so \\n, \\r, \\t, \\0, \\\\ and \\xNN all work on the command line as well as in this file. An option another mode ignores is an error rather than a no-op, so this is rejected outside delimiter mode."
},
"length-bytes": {
"enum": [
1,
2,
4,
8
],
"default": 4,
"description": "Width of the header in length mode, in bytes. Rejected outside length mode."
},
"endian": {
"enum": [
"big",
"little"
],
"default": "big",
"description": "Byte order of the header in length mode. Big is network order and what a peer means unless it says otherwise. Rejected outside length mode."
},
"max-message": {
"description": "Largest message to accept. A plain byte count or a binary suffix ('64k', '1MiB'). Defaults to 1MiB. 0 removes the limit, which hands a peer whose framing does not match an unbounded allocation. The cap is on one message rather than on the stream, and framing bytes do not count against it. A counted mode refuses an oversized message from its header, before the payload is read.",
"default": "1MiB"
},
"at-eof": {
"enum": [
"emit",
"error",
"drop"
],
"description": "What to do with bytes left over when the stream ends part way through a message. Defaults to emit in delimiter mode, where a last line without a newline is routine, and to error elsewhere, where a partial frame was cut off in transit. The counted modes (length, netstring) reject emit: what is left is a header and part of a payload, with no message in it."
}
}
},
"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",
"base64",
"unbase64",
"frame",
"unframe",
"process",
"rate",
"limit",
"throttle",
"block",
"hash",
"timeout",
"wasm"
]
},
"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-hash": {
"title": "hash",
"type": "object",
"description": "Digest the bytes crossing this stage. The payload is passed through untouched, so this can sit anywhere in a chain, including on a datagram path, and its position decides only what it sees: before a compress stage it digests the payload, after it digests the wire. The window is per path, so with direction 'both' each direction is digested separately.",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"const": "hash",
"description": "Selects the hash plugin."
},
"direction": {
"$ref": "#/$defs/plugin-direction"
},
"as": {
"type": "string",
"description": "Name for this instance, used in logs and in the bracket at the end of each digest line. 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."
},
"algorithm": {
"description": "Which digest to compute. Spelling is forgiving, so SHA-256 and sha_256 are the same, and sha2 and blake name the common member of their family. md5 and sha1 are for checking against a legacy tool rather than for deciding whether two things are the same. Aliases: algo, alg, hasher.",
"default": "sha256",
"enum": [
"md5",
"sha1",
"sha224",
"sha256",
"sha384",
"sha512",
"sha3-224",
"sha3-256",
"sha3-384",
"sha3-512",
"blake2",
"blake3"
]
},
"summary": {
"type": "boolean",
"default": true,
"description": "Write one line at end of stream, the digest of everything that crossed the stage. A datagram source and a held pipe never reach end of stream, so nothing is reported there; use chunks instead. Turning this off with chunks off too is rejected at startup."
},
"chunks": {
"type": "boolean",
"default": false,
"description": "Write one line per chunk, each the digest of that chunk alone rather than a running value, for locating where two captures diverge. Costs a finalisation and a write per chunk. Chunk boundaries on a byte stream are arbitrary, so the same bytes need not produce the same lines twice."
},
"file": {
"type": "string",
"description": "Where to write. Omitted, '-', 'stderr', '/dev/stderr' or '/dev/fd/2' all mean stderr. stdout is refused: on a stdio endpoint it carries relay payload."
},
"append": {
"type": "boolean",
"default": true,
"description": "Append to an existing file rather than truncating it."
}
}
},
"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])? *$"
}
]
}
}
},
"pty-endpoint": {
"title": "pty",
"type": "object",
"additionalProperties": false,
"required": [
"type"
],
"properties": {
"type": {
"const": "pty"
},
"link": {
"type": "string",
"description": "Symlink to create pointing at the allocated slave device, removed when the relay exits. This is how another program finds the device. Without one the path is only logged. A dangling link left by a previous run is replaced; a live one is not.",
"examples": [
"/tmp/ttyfake"
]
},
"raw": {
"type": "boolean",
"default": true,
"description": "Pass bytes through untouched. A terminal in cooked mode translates carriage returns, turns control characters into signals, and refuses a line longer than 4096 bytes, so leave this on for anything but text typed by a person."
},
"echo": {
"type": "boolean",
"default": false,
"description": "Echo input back at the writer. Off because on a relay that is a loop rather than a feature."
},
"size": {
"type": "string",
"pattern": "^ *[0-9]+ *[xX] *[0-9]+ *$",
"description": "Window size reported to the program, as ROWSxCOLS. Unset leaves whatever the kernel gave, which a fresh pty reports as 0x0. Fixed for the life of the connection: nothing relays a resize.",
"examples": [
"24x80",
"40x120"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"pty-exec-endpoint": {
"title": "pty-exec",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"argv"
],
"properties": {
"type": {
"enum": [
"pty-exec",
"pty_exec",
"ptyexec"
]
},
"argv": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "Program and arguments, run on a new pseudo-terminal with that terminal as the child's controlling terminal. Under `shell` this is a single command line instead. The pty is the child's stdin, stdout and stderr, so unlike exec its diagnostics are relayed.",
"examples": [
[
"bash"
],
[
"htop"
]
]
},
"shell": {
"type": "boolean",
"default": false,
"description": "Run argv[0] through $SHELL -c rather than executing it directly, which is what system is to exec."
},
"term": {
"type": "string",
"description": "What to set TERM to for the child. Unset leaves the relay's own value.",
"examples": [
"xterm-256color",
"dumb"
]
},
"raw": {
"type": "boolean",
"default": true,
"description": "Pass bytes through untouched. A terminal in cooked mode translates carriage returns, turns control characters into signals, and refuses a line longer than 4096 bytes, so leave this on for anything but text typed by a person."
},
"echo": {
"type": "boolean",
"default": false,
"description": "Echo input back at the writer. Off because on a relay that is a loop rather than a feature."
},
"size": {
"type": "string",
"pattern": "^ *[0-9]+ *[xX] *[0-9]+ *$",
"description": "Window size reported to the program, as ROWSxCOLS. Unset leaves whatever the kernel gave, which a fresh pty reports as 0x0. Fixed for the life of the connection: nothing relays a resize.",
"examples": [
"24x80",
"40x120"
]
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
},
"tty-endpoint": {
"title": "tty",
"type": "object",
"additionalProperties": false,
"required": [
"type",
"path"
],
"properties": {
"type": {
"enum": [
"tty",
"serial"
]
},
"path": {
"type": "string",
"description": "Terminal to open: a pts somebody else made, or a serial device. Refused if it is not a terminal, rather than opened or created as a regular file. The settings found at open are restored when the relay ends.",
"examples": [
"/dev/ttyUSB0",
"/dev/pts/5"
]
},
"raw": {
"type": "boolean",
"default": true,
"description": "Pass bytes through untouched. A terminal in cooked mode translates carriage returns, turns control characters into signals, and refuses a line longer than 4096 bytes, so leave this on for anything but text typed by a person."
},
"echo": {
"type": "boolean",
"default": false,
"description": "Echo input back at the writer. Off because on a relay that is a loop rather than a feature."
},
"size": {
"type": "string",
"pattern": "^ *[0-9]+ *[xX] *[0-9]+ *$",
"description": "Window size reported to the program, as ROWSxCOLS. Unset leaves whatever the kernel gave, which a fresh pty reports as 0x0. Fixed for the life of the connection: nothing relays a resize.",
"examples": [
"24x80",
"40x120"
]
},
"speed": {
"type": "integer",
"minimum": 1,
"description": "Line speed in baud. Unset leaves whatever the device had, which for a serial cable reads as noise rather than as an error.",
"examples": [
9600,
115200
]
},
"bits": {
"type": "integer",
"enum": [
5,
6,
7,
8
],
"default": 8,
"description": "Character size in bits."
},
"parity": {
"enum": [
"none",
"even",
"odd"
],
"default": "none",
"description": "Parity bit, if there is one."
},
"stop2": {
"type": "boolean",
"default": false,
"description": "Two stop bits rather than one."
},
"flow": {
"enum": [
"none",
"rts",
"xon"
],
"default": "none",
"description": "Flow control: rts for the hardware lines, xon for XON/XOFF bytes in the data."
},
"clocal": {
"type": "boolean",
"default": true,
"description": "Ignore modem control lines. On by default because without it the open itself waits for carrier detect, which a three-wire cable never asserts and which looks like a hang. Turn it off when you have modem control and want to know when the far end goes."
},
"exclusive": {
"type": "boolean",
"default": false,
"description": "Claim the device exclusively, so a second unprivileged open fails rather than interleaving its reads with the relay's. Worth setting on anything long-running: contention on a tty looks like corruption."
},
"name": {
"type": "string",
"description": "Custom display name for the endpoint."
}
}
}
}
}