Testing tilo.sh on your machine
tilo.sh is tested on a developer’s machine (or the rcargo builder) before any release. It is never tested on the deployed site. One command starts the whole product from your checkout. It uses the same images, entrypoints and environment variables as production:
os/containers/tilo-sh/stack.sh up
The first run waits for t to build this checkout’s images (image-lounge,
image-front-door and image-web-entry) and downloads them. Later runs
start in seconds. stack.sh help lists every command.
What runs
Everything listens on 127.0.0.1. Ports start at TILO_SH_PORT_BASE
(default 24100).
| Service | Production | Locally |
|---|---|---|
| Lounge relay (room host and relay) | eu1.tilo.sh and live-eu1.tilo.sh, image lounge |
the same image, behind production’s Caddy at https://127.0.0.1:24100 and https://127.0.0.1:24106 |
Front door (MCP, /v1, keys, OAuth) |
mcp.tilo.sh, image front-door |
the same image at http://localhost:24110 |
Web entry (site, /sign-in, /install.sh, /releases) |
tilo.sh, image web-entry |
the same image at http://localhost:24120; /releases/latest serves this checkout’s tilo command |
| Linux VM host (Firecracker) | tilo-sh-vm-host, image vm-host |
not yet: skipped, with the reason (see below) |
| Hosted browser pool | runs on the VM host | arrives with the VM host (docs/hosted-browser.md) |
| Discord | discord.com | stack.sh up --discord starts crates/dev/fake-discord and points the Lounge’s Discord sign-in and bridge at it |
Each container gets the environment its manifest sets in tilo/infra
(apps/tilo-sh-relay, apps/tilo-front-door and apps/tilo-web). Only the public names change to loopback ports, and
the secrets become throwaway local ones. stack.sh lists the variables per
service in relay_env, door_env and web_env. A change to
a production manifest belongs there too.
What differs from production, and why
- TLS. In production, Cloudflare terminates TLS. Locally, Caddy’s own
local CA signs the relay’s certificate. The front door and the relay get
that CA in the trust bundle at the fixed path Tilo’s binaries read. The
door and the site are plain HTTP on
http://localhost. That is a development origin the door, the sign-in page and thetilocommand already accept, so clients need no CA to reach them. - The live plane is behind the same Caddy, at
https://127.0.0.1:24106(production:live-eu1.tilo.sh). The relay’s own endpoint uses it as its home relay. That endpoint trusts the local CA through the bundle, as every Tilo client does, iroh’s packet-relay client included. - No model keys. The moderation agent and the “who is here” summaries do not run.
- No VM host yet. In production it is a privileged pod: it formats a
raw block device for its state, delegates cgroups and gives its jail
helper capabilities. Rootless podman can do none of that. Even as root,
the stack would also need a member identity the relay operator certifies,
a state device and the placer, and it does not set those up yet.
stack.shsays it skipped the VM host and why. Workspaces, keys, the MCP and the visa flow work without it. Shell commands (terminal#run) and the hosted browser pool that runs on it do not. Testing those locally is the next step for this stack: as root with/dev/kvm, on the rcargo builder or in a VM.
Using it
eval "$(os/containers/tilo-sh/stack.sh env)"
This sets TILO_DOOR, TILO_SITE, and the local CA for Node and Python.
After that, everything points at the local stack:
- A fresh visitor:
stack.sh browseropens Chromium on a throwaway profile at the local site. Headless,integration/person.mjs SITE RELAY OUTopens it as a fresh visitor and prints the time until they are in the Lounge. - The
tilocommand:tilo keys create "My backend"prints a link. Approve it in the browser thatstack.sh browseropened. The command prints the key.--dooralso works. - Python SDK:
Tilo()readsTILO_DOORandTILO_API_KEY. For example:uv run --project sdk/python python sdk/python/examples/visa_proof.py. - TypeScript SDK:
new Tilo()reads the same two variables. - An agent’s MCP client:
claude mcp add --transport http tilo-local http://localhost:24110/mcp. For a one-workspace key, usehttp://localhost:24110/w/<workspace>/mcpwith--header "Authorization: Bearer <token>". - Discord: with
--discord, the fake’s control port (http://127.0.0.1:24142) posts and reads as Discord’s users do. The routes are documented incrates/dev/fake-discord/src/main.rs.
stack.sh status shows what answers. stack.sh logs relay (or tls,
door, web, discord, releases) shows a service’s log.
stack.sh down stops the stack, and down --wipe also deletes its state.
Images other than this checkout’s
TILO_SH_IMAGE_LOUNGE, TILO_SH_IMAGE_DOOR and TILO_SH_IMAGE_WEB each take an OCI archive or a registry reference.
To reproduce what is deployed, give the digests infra pins. To try a
release candidate, give its sha-<commit> images.
The end-to-end check
os/containers/tilo-sh/stack.sh check # or: t test tilo-sh-local
The check brings the stack up and uses it as people and backends do:
- A fresh startup identity, in its own headless Chromium, approves the
server key that
tilo keys createasks for on the local/sign-inpage. - With that key, the visa run (
sdk/python/examples/visa_proof.py) creates applicant workspaces (25 by default, set withVISA_APPLICANTS). - It mints a one-workspace key and its MCP URL.
- The agent fills the form and writes the letter.
- A reviewer, another person in their own browser
(
integration/reviewer.py), joins by the invitation, sees the filled form and who filled it, and undoes the agent’s run. - Revoking the keys refuses them afterwards.
- Three fresh visitors enter the Lounge, timed.
The report (report.json in the work directory) holds the bring-up time,
the visa run’s duration and each visitor’s entry time. When the hosted
browser pool lands, its steps join this check.
Entry-speed work
TILO_SH_SEED=1 stack.sh check, or stack.sh up --seed followed by
integration/person.mjs, times a fresh visitor against a Lounge that
production’s members and history have filled. Compare entry_ms before and
after a change, on the same machine.