# 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.
```toml
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]`.
```toml
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.