pub const DEVICE_TEMPLATE: &str = r#"listen_address = "127.0.0.1:7700"
health_listen_address = "127.0.0.1:7701"
drain_timeout_ms = 0
routing_rules = []
persistence_path = "./store"
[[channels]]
name = "console.topology"
durable = false
[auth]
# Generate a fresh local bootstrap value; peer mode accepts Lys credentials.
token = "REPLACE_WITH_A_FRESH_RANDOM_LOCAL_VALUE"
require_verified_worker = true
[auth.lys]
issuer = "https://LYS_HOST"
api_url = "https://LYS_HOST/api/"
network_id = "OFFICE_NETWORK_ID"
keys_path = "./lys-jwks.json"
live = true
[auth.lys.machine]
identity_path = "./liminal-agent.key"
agent_id = "YOUR_DEVICE_AGENT_ID"
audience = "liminal"
[auth.lys.machine.machines]
"OFFICE_AGENT_ID" = "OFFICE_ED25519_PUBLIC_KEY_64_HEX"
[[auth.lys.machine.links]]
link_id = "OFFICE_LINK_ID"
peer_holder = "OFFICE_AGENT_ID"
# No client wire address is exposed; the broker chooses the punched path.
exports = []
[auth.lys.machine.carrier_choice]
# One in-flight carrier operation is bounded by the configured estate RTT budget.
crossing_deadline_ms = 2000
broker_address = "BROKER_HOST:7600"
broker_echo = "BROKER_IP:7603"
udp_listen = "0.0.0.0:0"
relay_holder = "OFFICE_AGENT_ID"
relay_address = "RELAY_HOST:7600"
[auth.lys.machine.carrier_choice.peers]
certificate_path = "./liminal-agent.der"
service_prefixes = ["embed", "jobs"]
# Bounds parsing of the signed device certificate.
max_certificate_bytes = 16384
# The service-key response carries one 32-byte public key as hex.
max_key_response_bytes = 512
# Bounds the nonce-bearing signature header before COSE decoding.
max_proof_bytes = 4096
# Bounds one issuer response containing a fresh app pass.
max_pass_response_bytes = 65536
# Bounds the exact certificate's current issuance/withdrawal view.
max_certificate_status_bytes = 1048576
# Matches the issuer's sixty-second signed-request window.
proof_window_ms = 60000
# Matches the issuer's default ten-minute app-pass lifetime.
max_certificate_pass_lifetime_secs = 600
# Bounds the full discovery document before parsing contracts.
max_topology_bytes = 1048576
# Bounds the station index retained from one far document.
max_stations = 256
# Bounds the number of far exchange generations retained locally.
max_peers = 8
# Bounds queued and in-flight service requests per link.
max_pending_calls = 32
# Leaves space for Noise and the call envelope in one sealed message.
max_call_bytes = 61440
[auth.lys.grant_stream]
log_identity = "LYS_GRANT_LOG_IDENTITY"
log_epoch = 1
credential_path = "./liminal-app-credential"
service_key_path = "./lys-service-key.hex"
page_positions = 16
max_frames = 18
max_response_bytes = 8388608
max_event_bytes = 65536
max_revoked_grants = 65536
max_credential_bytes = 65536
max_keyset_bytes = 65536
max_transport_reopens = 2
max_rights = 64
max_chain_grants = 16
max_chains = 8
enforce_revocation = true
[auth.lys.grant_stream.admission]
kind = "ADMISSION_RESOURCE_KIND"
id = "ADMISSION_RESOURCE_ID"
action = "ADMISSION_ACTION"
[[auth.lys.grant_stream.subjects]]
kind = "liminal.subject"
resource = "EMBED_SUBJECT_RESOURCE_ID"
prefix = "embed"
[[auth.lys.grant_stream.subjects]]
kind = "liminal.subject"
resource = "JOBS_SUBJECT_RESOURCE_ID"
prefix = "jobs"
[participant]
wire_frame_limit = 65536
attach_receipt_ttl_ms = 60000
receipt_provenance_ttl_ms = 600000
live_receipt_server_report_threshold = 1024
max_live_attach_receipts_per_participant = 8
receipt_provenance_server_report_threshold = 4096
receipt_provenance_per_conversation_report_threshold = 256
max_receipt_provenance_per_participant = 64
max_retired_identity_slots_server = 1024
identity_slots = 4
observer_recovery_max_entries = 64
max_semantic_conversations_per_connection = 32
max_ordinary_record_entries = 1
max_ordinary_record_bytes = 131072
max_generated_marker_entries = 1
max_generated_marker_bytes = 4096
mandatory_transaction_bound_entries = 4
mandatory_transaction_bound_bytes = 16384
full_recovery_claim_entries = 4
full_recovery_claim_bytes = 16384
retained_capacity_entries = 2048
retained_capacity_bytes = 16777216
max_retained_record_rows = 1024
closure_episode_churn_limit = 1024
"#;
pub const DEVICE_PAGE: &str = r#"---
title: Your own liminal
description: Run liminal on your device and reach the office through a Noise link.
order: 15
kind: how-to
draft: true
---
Your agents connect to your own liminal at `127.0.0.1`. Your liminal links to the
office. The office's services keep running there; their stations and contracts
appear on your map, with their device and link named.
## Install and enrol your device
Install the command from crates.io:
```sh
cargo install --locked liminal-cli
mkdir -p my-liminal
cd my-liminal
umask 077
```
In Lys, add an agent for this device, with you responsible. Get the office's
network id, link id, agent id and public Ed25519 key, and its broker and relay
addresses from the network's owner. Get the identified grant log, its current
epoch, admission target and placed subject mappings from that owner too. Issue
an app credential in Lys for this local exchange's grant reader and save it as
`liminal-app-credential` with mode `0600`. Have the owner approve the device's link
grant in Lys. Grant the device agent `read` on its own `agent` resource so its
signed certificate-status read can verify the exact certificate. Give your
agents only the service call grants they need.
Save the device template as `device.toml`. Fill the office values and your own
device agent id. Generate the fresh local bootstrap value without printing it:
```sh
python3 - <<'PY'
from pathlib import Path
import secrets
p = Path('device.toml')
p.write_text(p.read_text().replace('REPLACE_WITH_A_FRESH_RANDOM_LOCAL_VALUE', secrets.token_hex(32)))
PY
chmod 600 device.toml
liminal identity --config ./device.toml > device-csr.txt
```
The identity command creates `liminal-agent.key` with mode `0600` and prints the
public key and signed certificate request. The private key stays on your device.
## Issue the certificate yourself
Use your own Lys session with the `agent.certificate.issue` scope. Set
`LYS_API_BASE`, `LYS_PERSON_ACCESS_TOKEN` and `LIMINAL_DEVICE_AGENT` in your shell.
The program below posts the CSR to `POST /agents/{id}/certificates` and saves the
issued DER. It never prints your access token or puts it in a process argument.
```sh
python3 - <<'PY'
import base64, json, os, re, secrets, urllib.request
from pathlib import Path
from urllib.parse import urlparse
base = os.environ['LYS_API_BASE'].rstrip('/')
url = urlparse(base)
if url.username is not None or url.password is not None or url.query or url.fragment:
raise SystemExit('Lys API base must contain no credentials, query or fragment')
if url.scheme != 'https' and not (url.scheme == 'http' and url.hostname in ('127.0.0.1', 'localhost', '::1')):
raise SystemExit('Lys must use HTTPS or loopback HTTP')
agent = os.environ['LIMINAL_DEVICE_AGENT']
if not re.fullmatch(r'[A-Za-z0-9_-]+', agent):
raise SystemExit('device agent id is not a route segment')
csr = Path('device-csr.txt').read_text()
match = re.search(r'-----BEGIN CERTIFICATE REQUEST-----\s*(.*?)\s*-----END CERTIFICATE REQUEST-----', csr, re.S)
if match is None:
raise SystemExit('the identity command produced no CSR')
body = json.dumps({'operation': 'op-' + secrets.token_hex(16), 'request': ''.join(match.group(1).split())}).encode()
request = urllib.request.Request(base + '/agents/' + agent + '/certificates', data=body, method='POST',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + os.environ['LYS_PERSON_ACCESS_TOKEN']})
with urllib.request.urlopen(request) as response:
issued = json.load(response)
certificate = next((c for c in issued['certificates'] if c['serial'] == issued['recorded']), None)
if certificate is None or certificate['withdrawn'] is not None:
raise SystemExit('Lys returned no current issued certificate')
Path('liminal-agent.der').write_bytes(base64.b64decode(certificate['der'], validate=True))
PY
chmod 600 liminal-agent.der
```
Download Lys's pass keys from `/oauth/jwks` to the configured `lys-jwks.json`:
```sh
curl --fail --silent --show-error https://LYS_HOST/oauth/jwks --output lys-jwks.json
python3 - <<'PY'
import json, urllib.request
from pathlib import Path
with urllib.request.urlopen('https://LYS_HOST/api/service-key') as response:
key = json.load(response)['ed25519']
if len(key) != 64 or any(c not in '0123456789abcdef' for c in key):
raise SystemExit('Lys returned an invalid service key')
Path('lys-service-key.hex').write_text(key)
PY
liminal check --config ./device.toml
liminal --config ./device.toml
```
The server fetches the certificate signer from the separate unauthenticated
`GET /api/service-key`. JWKS verifies passes. The service key verifies
certificates. A signed MCP read checks that the exact presented certificate is
still unwithdrawn. A fresh signed agent-pass request proves that the certificate key
still stands; the pass decides the link and every service call.
## Point each ticket at your own liminal
Each agent uses its own Lys identity or run pass, and its own ticket file:
```toml
server = "127.0.0.1:7700"
channel = "console.topology"
[lys]
issuer = "https://LYS_HOST"
# Lys-launched seats obtain their own agent and run pass from Lys's environment.
[limits]
frame_bytes = 1048576
record_bytes = 1048576
reply_timeout_ms = 5000
flow_steps = 32
request_bytes = 61440
```
Run `liminal mcp --config /absolute/path/to/ticket.toml` as the agent's MCP ticket.
Use `list` to find the office stations, `describe` to read a service's actual
input and output schemas, and `call` to request `embed.bge` and `jobs.studio`.
## Read the result
Your map shows the office's worker and service stations under the office device,
with the link named. The ticket lists and describes those same contracts. A
service call returns the office worker's reply or the far decision's named
refusal. When the link ends, its far stations disappear in that same revision.
The link reports `wire` for a reachable wire path, `direct` for punched UDP and
`relay` when a relay carries the sealed bytes. A relay also names `relayed` and
`carried_by`; its recorded carrier refusals explain why the direct path failed.
## When only the relay is reachable
Keep the broker and relay values the office gave you. Leave `peer_address`
unset. The carrier chooser tries a reachable wire path, then the broker's
punch, then the relay. A granted relay carries only sealed endpoint bytes.
Check the link's recorded refusal and carrier instead of assuming it punched.
A held link needs its pending approval in Lys; a refused link needs the named
grant, certificate, issuer or key fault corrected. Your agents keep using
`127.0.0.1:7700` whichever carrier the link holds.
"#;