liminal_server/link_certificate/
setup.rs1pub const DEVICE_TEMPLATE: &str = r#"listen_address = "127.0.0.1:7700"
5health_listen_address = "127.0.0.1:7701"
6drain_timeout_ms = 0
7routing_rules = []
8persistence_path = "./store"
9
10[[channels]]
11name = "console.topology"
12durable = false
13
14[auth]
15# Generate a fresh local bootstrap value; peer mode accepts Lys credentials.
16token = "REPLACE_WITH_A_FRESH_RANDOM_LOCAL_VALUE"
17require_verified_worker = true
18
19[auth.lys]
20issuer = "https://LYS_HOST"
21api_url = "https://LYS_HOST/api/"
22network_id = "OFFICE_NETWORK_ID"
23keys_path = "./lys-jwks.json"
24live = true
25
26[auth.lys.machine]
27identity_path = "./liminal-agent.key"
28agent_id = "YOUR_DEVICE_AGENT_ID"
29audience = "liminal"
30
31[auth.lys.machine.machines]
32"OFFICE_AGENT_ID" = "OFFICE_ED25519_PUBLIC_KEY_64_HEX"
33
34[[auth.lys.machine.links]]
35link_id = "OFFICE_LINK_ID"
36peer_holder = "OFFICE_AGENT_ID"
37# No client wire address is exposed; the broker chooses the punched path.
38exports = []
39
40[auth.lys.machine.carrier_choice]
41# One in-flight carrier operation is bounded by the configured estate RTT budget.
42crossing_deadline_ms = 2000
43broker_address = "BROKER_HOST:7600"
44broker_echo = "BROKER_IP:7603"
45udp_listen = "0.0.0.0:0"
46relay_holder = "OFFICE_AGENT_ID"
47relay_address = "RELAY_HOST:7600"
48
49[auth.lys.machine.carrier_choice.peers]
50certificate_path = "./liminal-agent.der"
51service_prefixes = ["embed", "jobs"]
52# Bounds parsing of the signed device certificate.
53max_certificate_bytes = 16384
54# The service-key response carries one 32-byte public key as hex.
55max_key_response_bytes = 512
56# Bounds the nonce-bearing signature header before COSE decoding.
57max_proof_bytes = 4096
58# Bounds one issuer response containing a fresh app pass.
59max_pass_response_bytes = 65536
60# Bounds the exact certificate's current issuance/withdrawal view.
61max_certificate_status_bytes = 1048576
62# Matches the issuer's sixty-second signed-request window.
63proof_window_ms = 60000
64# Matches the issuer's default ten-minute app-pass lifetime.
65max_certificate_pass_lifetime_secs = 600
66# Bounds the full discovery document before parsing contracts.
67max_topology_bytes = 1048576
68# Bounds the station index retained from one far document.
69max_stations = 256
70# Bounds the number of far exchange generations retained locally.
71max_peers = 8
72# Bounds queued and in-flight service requests per link.
73max_pending_calls = 32
74# Leaves space for Noise and the call envelope in one sealed message.
75max_call_bytes = 61440
76
77[auth.lys.grant_stream]
78log_identity = "LYS_GRANT_LOG_IDENTITY"
79log_epoch = 1
80credential_path = "./liminal-app-credential"
81service_key_path = "./lys-service-key.hex"
82page_positions = 16
83max_frames = 18
84max_response_bytes = 8388608
85max_event_bytes = 65536
86max_revoked_grants = 65536
87max_credential_bytes = 65536
88max_keyset_bytes = 65536
89max_transport_reopens = 2
90max_rights = 64
91max_chain_grants = 16
92max_chains = 8
93enforce_revocation = true
94
95[auth.lys.grant_stream.admission]
96kind = "ADMISSION_RESOURCE_KIND"
97id = "ADMISSION_RESOURCE_ID"
98action = "ADMISSION_ACTION"
99
100[[auth.lys.grant_stream.subjects]]
101kind = "liminal.subject"
102resource = "EMBED_SUBJECT_RESOURCE_ID"
103prefix = "embed"
104
105[[auth.lys.grant_stream.subjects]]
106kind = "liminal.subject"
107resource = "JOBS_SUBJECT_RESOURCE_ID"
108prefix = "jobs"
109
110[participant]
111wire_frame_limit = 65536
112attach_receipt_ttl_ms = 60000
113receipt_provenance_ttl_ms = 600000
114live_receipt_server_report_threshold = 1024
115max_live_attach_receipts_per_participant = 8
116receipt_provenance_server_report_threshold = 4096
117receipt_provenance_per_conversation_report_threshold = 256
118max_receipt_provenance_per_participant = 64
119max_retired_identity_slots_server = 1024
120identity_slots = 4
121observer_recovery_max_entries = 64
122max_semantic_conversations_per_connection = 32
123max_ordinary_record_entries = 1
124max_ordinary_record_bytes = 131072
125max_generated_marker_entries = 1
126max_generated_marker_bytes = 4096
127mandatory_transaction_bound_entries = 4
128mandatory_transaction_bound_bytes = 16384
129full_recovery_claim_entries = 4
130full_recovery_claim_bytes = 16384
131retained_capacity_entries = 2048
132retained_capacity_bytes = 16777216
133max_retained_record_rows = 1024
134closure_episode_churn_limit = 1024
135"#;
136
137pub const DEVICE_PAGE: &str = r#"---
139title: Your own liminal
140description: Run liminal on your device and reach the office through a Noise link.
141order: 15
142kind: how-to
143draft: true
144---
145
146Your agents connect to your own liminal at `127.0.0.1`. Your liminal links to the
147office. The office's services keep running there; their stations and contracts
148appear on your map, with their device and link named.
149
150## Install and enrol your device
151
152Install the command from crates.io:
153
154```sh
155cargo install --locked liminal-cli
156mkdir -p my-liminal
157cd my-liminal
158umask 077
159```
160
161In Lys, add an agent for this device, with you responsible. Get the office's
162network id, link id, agent id and public Ed25519 key, and its broker and relay
163addresses from the network's owner. Get the identified grant log, its current
164epoch, admission target and placed subject mappings from that owner too. Issue
165an app credential in Lys for this local exchange's grant reader and save it as
166`liminal-app-credential` with mode `0600`. Have the owner approve the device's link
167grant in Lys. Grant the device agent `read` on its own `agent` resource so its
168signed certificate-status read can verify the exact certificate. Give your
169agents only the service call grants they need.
170
171Save the device template as `device.toml`. Fill the office values and your own
172device agent id. Generate the fresh local bootstrap value without printing it:
173
174```sh
175python3 - <<'PY'
176from pathlib import Path
177import secrets
178p = Path('device.toml')
179p.write_text(p.read_text().replace('REPLACE_WITH_A_FRESH_RANDOM_LOCAL_VALUE', secrets.token_hex(32)))
180PY
181chmod 600 device.toml
182liminal identity --config ./device.toml > device-csr.txt
183```
184
185The identity command creates `liminal-agent.key` with mode `0600` and prints the
186public key and signed certificate request. The private key stays on your device.
187
188## Issue the certificate yourself
189
190Use your own Lys session with the `agent.certificate.issue` scope. Set
191`LYS_API_BASE`, `LYS_PERSON_ACCESS_TOKEN` and `LIMINAL_DEVICE_AGENT` in your shell.
192The program below posts the CSR to `POST /agents/{id}/certificates` and saves the
193issued DER. It never prints your access token or puts it in a process argument.
194
195```sh
196python3 - <<'PY'
197import base64, json, os, re, secrets, urllib.request
198from pathlib import Path
199from urllib.parse import urlparse
200base = os.environ['LYS_API_BASE'].rstrip('/')
201url = urlparse(base)
202if url.username is not None or url.password is not None or url.query or url.fragment:
203 raise SystemExit('Lys API base must contain no credentials, query or fragment')
204if url.scheme != 'https' and not (url.scheme == 'http' and url.hostname in ('127.0.0.1', 'localhost', '::1')):
205 raise SystemExit('Lys must use HTTPS or loopback HTTP')
206agent = os.environ['LIMINAL_DEVICE_AGENT']
207if not re.fullmatch(r'[A-Za-z0-9_-]+', agent):
208 raise SystemExit('device agent id is not a route segment')
209csr = Path('device-csr.txt').read_text()
210match = re.search(r'-----BEGIN CERTIFICATE REQUEST-----\s*(.*?)\s*-----END CERTIFICATE REQUEST-----', csr, re.S)
211if match is None:
212 raise SystemExit('the identity command produced no CSR')
213body = json.dumps({'operation': 'op-' + secrets.token_hex(16), 'request': ''.join(match.group(1).split())}).encode()
214request = urllib.request.Request(base + '/agents/' + agent + '/certificates', data=body, method='POST',
215 headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + os.environ['LYS_PERSON_ACCESS_TOKEN']})
216with urllib.request.urlopen(request) as response:
217 issued = json.load(response)
218certificate = next((c for c in issued['certificates'] if c['serial'] == issued['recorded']), None)
219if certificate is None or certificate['withdrawn'] is not None:
220 raise SystemExit('Lys returned no current issued certificate')
221Path('liminal-agent.der').write_bytes(base64.b64decode(certificate['der'], validate=True))
222PY
223chmod 600 liminal-agent.der
224```
225
226Download Lys's pass keys from `/oauth/jwks` to the configured `lys-jwks.json`:
227
228```sh
229curl --fail --silent --show-error https://LYS_HOST/oauth/jwks --output lys-jwks.json
230python3 - <<'PY'
231import json, urllib.request
232from pathlib import Path
233with urllib.request.urlopen('https://LYS_HOST/api/service-key') as response:
234 key = json.load(response)['ed25519']
235if len(key) != 64 or any(c not in '0123456789abcdef' for c in key):
236 raise SystemExit('Lys returned an invalid service key')
237Path('lys-service-key.hex').write_text(key)
238PY
239liminal check --config ./device.toml
240liminal --config ./device.toml
241```
242
243The server fetches the certificate signer from the separate unauthenticated
244`GET /api/service-key`. JWKS verifies passes. The service key verifies
245certificates. A signed MCP read checks that the exact presented certificate is
246still unwithdrawn. A fresh signed agent-pass request proves that the certificate key
247still stands; the pass decides the link and every service call.
248
249## Point each ticket at your own liminal
250
251Each agent uses its own Lys identity or run pass, and its own ticket file:
252
253```toml
254server = "127.0.0.1:7700"
255channel = "console.topology"
256
257[lys]
258issuer = "https://LYS_HOST"
259# Lys-launched seats obtain their own agent and run pass from Lys's environment.
260
261[limits]
262frame_bytes = 1048576
263record_bytes = 1048576
264reply_timeout_ms = 5000
265flow_steps = 32
266request_bytes = 61440
267```
268
269Run `liminal mcp --config /absolute/path/to/ticket.toml` as the agent's MCP ticket.
270Use `list` to find the office stations, `describe` to read a service's actual
271input and output schemas, and `call` to request `embed.bge` and `jobs.studio`.
272
273## Read the result
274
275Your map shows the office's worker and service stations under the office device,
276with the link named. The ticket lists and describes those same contracts. A
277service call returns the office worker's reply or the far decision's named
278refusal. When the link ends, its far stations disappear in that same revision.
279
280The link reports `wire` for a reachable wire path, `direct` for punched UDP and
281`relay` when a relay carries the sealed bytes. A relay also names `relayed` and
282`carried_by`; its recorded carrier refusals explain why the direct path failed.
283
284## When only the relay is reachable
285
286Keep the broker and relay values the office gave you. Leave `peer_address`
287unset. The carrier chooser tries a reachable wire path, then the broker's
288punch, then the relay. A granted relay carries only sealed endpoint bytes.
289Check the link's recorded refusal and carrier instead of assuming it punched.
290A held link needs its pending approval in Lys; a refused link needs the named
291grant, certificate, issuer or key fault corrected. Your agents keep using
292`127.0.0.1:7700` whichever carrier the link holds.
293"#;