liminal-cli 0.3.30

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation

liminal-cli

The liminal command serves a seat's discovery and service MCP over stdio. The seat's host runs liminal mcp --config <seat.toml>, or names the seat by flags alone (below).

Install the command with cargo install liminal-cli. The seat file's server is the address of the liminal server reachable from that laptop. The client wire is plain TCP, so use a named address on an encrypted network such as the tailnet.

server = "100.64.0.1:7600"
channel = "agents"

[lys]
issuer = "https://lys.example"

[limits]
frame_bytes = 1048576
record_bytes = 1048576
reply_timeout_ms = 30000
flow_steps = 32
request_bytes = 1048576

When Lys starts the host, it supplies LYS_AGENT and LYS_AGENT_PASS. With no key_file, the command uses that run pass to ask Lys for the seat's app passes. It holds each until its expiry and asks again when needed. The run pass and app passes are never printed or written to the seat file.

A certified agent can keep the signed route by adding agent = "agent-id" and key_file = "/absolute/path/to/agent-key.pem" to [lys]. A fixed pass can instead be supplied by a top-level token_file, with no [lys] table.

One registration, the identity at run time

A seat needs no file of its own. liminal mcp takes the seat from flags, and with no --config the channel (console.topology) and limits are those liminal seat init writes. One registration line serves every person and agent on an exchange, and only the identity differs:

  • Open exchange: liminal mcp --server 100.64.0.1:7600 --as <name>. The name is declared as open:<name>, and its first listen places agents.<name>. Off loopback the seat reaches the network, as seat init --open writes it.
  • Lys exchange: liminal mcp --agent <agent-id> --key-file /path/to/agent-key.pem (and --server when it is not the central exchange). The key is the identity; nothing is claimed by name.

--channel sets the channel. Beside --config each flag wins over the file's value. The seat's kind never changes by flag: --as beside [lys] or token_file is refused, --agent beside [open] is refused, and --agent and --key-file are refused alone. No error shows a key's or a pass's bytes.

A hosted assistant on an open exchange

liminal agent run --config <seat.toml> hosts an ACP harness (Claude Code, for one) as the station its seat names. On an open exchange the seat carries [open] name = "<name>" in place of [lys]: it is presented as open:<name>, holds no pass, and its first session places agents.<name>. Off loopback add reach = "network" to [open].

server = "127.0.0.1:7700"
channel = "console.topology"

[open]
name = "assistant"

[limits]
frame_bytes = 65536
record_bytes = 1048576
reply_timeout_ms = 10000
flow_steps = 8
request_bytes = 65536

[harness]
id = "claude-code"
program = "/absolute/path/to/the/acp/agent"
args = []
cwd = "/absolute/working/directory"

A seat with both [lys] and [open], or neither, is refused by name.

[harness] env sets variables on the harness and env_remove removes them, so a supervised host can say what the harness needs, for example env_remove = ["CLAUDECODE"] with [harness.env] naming CLAUDE_CODE_EXECUTABLE and CLAUDE_CONFIG_DIR. Values never appear in a refusal. An [open] seat runs whatever its stdin is; only a [lys] seat on a pipe ends when its launcher closes it.

Answers of any size

liminal mcp writes an answer inline up to [limits] answer_bytes (16 MiB when unnamed). A larger answer is handed over whole as a file the seat owns (under ~/.liminal/answers), named by its sha256, with its path, size and a preview in the answer; the seat removes the file when it ends. frame_bytes bounds only the lines the MCP host sends. flow takes answer: "last" and a per-step keep: false so large intermediate replies feed the next step without coming back.

The command also serves a server configuration with liminal --config <server.toml> and validates it without opening sockets or the store with liminal check --config <server.toml>. A server may declare extra_listen_addresses = ["100.64.0.1:7600"] beside its primary listen_address; each extra address must name an interface.