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-trialInitialization 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_DIRECTORYCreation 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-channelUse 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-channelThe 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:
native/{key,store/}: participant and records;issuer/{key,store/}: issuer and read/writer statements;issuer.by: public issuer;exchange.interlang: compiled policy;config.json: issuer, scope, and relative endpoint;starter.hash: welcome record identifier.
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.pyDownloads already contain binaries and assets. Validation lists the remaining browser and cross-tool checks.