Host a browser conversation

The native host exchanges records with independent browser participants. The operator chooses one issuer and scope, then changes reading and writer acceptance through signed records. Qualified selection defines those conventions; neither grant permits execution or patch application.

Commands run from the joinery checkout or extracted download. Native tools require a supported Unix/LMDB target; validation is on Linux. Hosting requires Python 3.11+ and uv, which installs the pinned WebSocket dependency on first use.

Start on one trusted machine

Use BIN=./bin for a download or BIN=./target/debug for a built checkout. Create a new private workspace outside the tools:

BIN=./bin
WORK="$HOME/joinery-browser-workspace"
SCOPE=work
"$BIN/joinery-browser" init "$WORK" "$SCOPE"
uv run --script ingredients/browser-conversation/serve.py "$WORK" --trust-local-trial

Initialization creates native and issuer keys, separate read/writer grants for the native participant, and a welcome message. Scope is an application field, not a path or globally unique group identity. A different scope needs a new workspace.

Open http://127.0.0.1:8787/. --trust-local-trial asserts confidentiality, integrity and confinement of the whole machine/channel. Loopback does not isolate other local users; do not use this mode for remote access or a shared/untrusted machine. Live ILTP proof still establishes participant keys.

The server finds bundled bin/joinery, then checkout target/debug/joinery; --joinery /absolute/path/to/joinery selects another build.

Enroll and change access

Follow the browser instructions to create and save a key. In another terminal, enroll only its public identifier:

BIN=./bin
WORK="$HOME/joinery-browser-workspace"
BROWSER='paste the full public V.…H3 identifier here'
"$BIN/joinery-browser" enroll "$WORK" "$BROWSER"

Enrollment issues separate reading (After=0, including shared history) and writer grants. Re-enrollment issues newer revisions for both. Run issuer commands one at a time; this is a local operator tool, not concurrent administration.

Change either grant independently:

"$BIN/joinery-browser" withdraw-read "$WORK" "$BROWSER"
"$BIN/joinery-browser" grant-read "$WORK" "$BROWSER"
"$BIN/joinery-browser" withdraw-writer "$WORK" "$BROWSER"
"$BIN/joinery-browser" grant-writer "$WORK" "$BROWSER"

Commands allocate u64 revisions through public retained-record queries. They change evidence, not policy or configuration, so connected participants need no restart. Withdrawals affect current qualification, not retained copies. Relevant read and writer evidence remains deliberately shareable with proved peers; withheld newer evidence cannot be detected.

Native creation and retained records

Keep SCOPE equal to its initialization value:

printf 'A later native contribution.\n' | \
  "$BIN/qualified-reading" content "$WORK/native" "$SCOPE" "$(date +%s)"
"$BIN/joinery" --state "$WORK/native" list
"$BIN/joinery" --state "$WORK/native" get HASH
"$BIN/joinery" --state "$WORK/native" export NEW_DIRECTORY

Creation takes declared Unix epoch seconds and writes Timezone: UTC. Generic list/get/export operate on retained records, not current qualification. Inspect exports before sharing. The thread reader selects qualified discussions from canonical exports and approved definitions.

Hosting and recovery

Ctrl-C stops the host and its native sessions. Restart serve.py with the same workspace; do not initialize again. Browser retention is tied to scheme, hostname, and port. Keep keys, records, and exact rule sources independently of the host.

For remote participants, supply a browser-trusted certificate and explicit complete-channel assurance:

uv run --script ingredients/browser-conversation/serve.py "$WORK" \
  --host 0.0.0.0 --port 8787 --origin https://joinery.example:8787 \
  --cert /private/fullchain.pem --key /private/privkey.pem \
  --trust-confidential-channel

Use the actual HTTPS origin without a trailing slash. Never share TLS private keys or disable certificate verification. The relative endpoint iltp preserves a hosted path prefix; endpoint addresses grant no record authority.

An external TLS terminator instead requires explicit private RFC1918 IPv4 addresses for the backend and its sole trusted proxy. Set BACKEND_IPV4, PROXY_IPV4, and PUBLIC_ORIGIN for that route, then run:

uv run --script ingredients/browser-conversation/serve.py "$WORK" \
  --host "$BACKEND_IPV4" --origin "$PUBLIC_ORIGIN" \
  --trusted-proxy "$PROXY_IPV4" --trust-confidential-channel

The proxy preserves Host/Origin and strips any configured public prefix. Admission checks the socket peer, never forwarded headers. Assurance covers the terminator, backend hop, hosts, and native pipes; do not assert it for an untrusted proxy. Do not combine external termination with --cert/--key.

Only public assets/configuration are served, not workspace keys or stores. The page code is trusted with signing secrets and plaintext; the TLS terminator and native host also see plaintext. This is not a public-service hardening guarantee.

Retained layout

WORK is private (0700), with native key files 0600:

Build and validation

A source checkout needs sibling Lace, its generated JS bundles, Rust, and Pandoc:

cargo build --locked --bin joinery --bin joinery-browser --bin qualified-reading
make -C site assets
timeout 55s cargo test --test browser_native
timeout 55s cargo test --test wss
timeout 55s uv run --script tests/browser_host.py

Downloads already contain binaries and assets. Validation lists the remaining browser and cross-tool checks.