Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/CLIENT_INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,42 @@ Compatibility exports refer to the same functions/constants. Shared protocol hel
`tests/test_client_boundary.py` checks implementation ownership, compatibility exports, proof context, atomic-write failure protection and missing-dependency errors. `scripts/check_client_install.py` starts a fresh interpreter with server imports blocked, loads client features, saves/reloads a private key, and sends the same signed request through four real client adapters. Its HTTP endpoints use MockTransport rather than production.

The client-boundary workflow builds wheel/sdist, installs the wheel in a new environment outside the source tree, checks dependencies, both help commands and all four transport probes. `--minimal-install` rejects server Python dependencies. Full PostgreSQL/Valkey, CLI/MCP, credential journal/recovery and conformance gates remain required separately. These checks do not establish physical-device, external-network, performance or production acceptance.

## One-command, user-level installation

```bash
curl -fsSL https://msg.lmm.best/install | bash
msg lightjunction@msg.lmm.best ""
```

The `/install` endpoint serves a packaged Bash script. It installs the client
without `sudo` on Linux glibc x86_64/ARM64 and macOS Intel/Apple Silicon.
It needs Bash, curl, tar, and a SHA-256 utility. No existing Python is required:
the script verifies a pinned uv 0.12.20 native archive and obtains managed
CPython 3.15.0rc2. CPython is currently a release candidate because this project
requires Python 3.15.

The client source is the published GitHub commit
`5554f93819478f4559aa226d1d94cfc6202a1cee`, verified against its embedded SHA-256;
there is no dependency on an unpublished MSG release asset. Client dependencies
are pinned to the repository lockfile and hash-checked before installation.
Only `msg` is exposed in `$HOME/.local/bin`; server dependencies and daemon
services are not installed. Install data stays under
`$XDG_DATA_HOME/msg/client-{releases,python,cache}` (default `~/.local/share`).
Identity, certificates, and service connection configuration are untouched.

If that bin directory is absent from PATH, the installer prints the line to add
to your shell configuration. It does not edit shell files. Repeating the
installation reuses the verified installed revision. An existing `msg` launcher
from a different installer is retained unless you explicitly replace it:

```bash
curl -fsSL https://msg.lmm.best/install | bash -s -- --force
```

To publish a newer client, update the source revision and archive SHA-256 in
`src/msg/data/install.sh`, regenerate its embedded client requirements with
`uv export --no-dev --no-emit-project --format requirements-txt`, and exercise a
fresh installation before deploying the `/install` endpoint. The installer UX
was inspired by [Termium](https://github.com/codr1/termium); its browser and SSH
implementation are not dependencies of MSG.
240 changes: 240 additions & 0 deletions src/msg/data/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env bash
# Parse the complete function before a piped download can execute installation.
# UX inspired by https://github.com/codr1/termium; implementation is MSG-specific.
main() {
set -euo pipefail
fail() { printf 'msg: %s\n' "$*" >&2; exit 1; }
force=0
case "${1:-}" in
'') ;;
--force) force=1; shift ;;
--help) printf '%s\n' 'Install the MSG client: curl -fsSL https://msg.lmm.best/install | bash' 'Use bash -s -- --force to replace an existing unmanaged msg command.'; return ;;
*) fail 'Unknown option; use --help.' ;;
esac
[ "$#" -eq 0 ] || fail 'Unexpected arguments.'
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) platform=x86_64-unknown-linux-gnu; uv_hash=6590717592ace991ff83a63fef799e3ad9d33ecc8f96c5d6bdd732496e79337f ;;
Linux-aarch64|Linux-arm64) platform=aarch64-unknown-linux-gnu; uv_hash=8a7aad7bc76a2fae5151566ff3e43eacce0b2a113d5e4de3e4afe3e58fa2441e ;;
Darwin-x86_64) platform=x86_64-apple-darwin; uv_hash=ac54283d211fd77cdc152b67606dbaf6406ff4ab03f3af4ae99468fa8e887141 ;;
Darwin-arm64) platform=aarch64-apple-darwin; uv_hash=848fdeb602ff1a1baacd4f6c8b7bdc6cf1ad026a6d9cf59475fda17c179743ca ;;
*) fail 'Supported: Linux glibc x86_64/ARM64 and macOS Intel/Apple Silicon.' ;;
esac
if [ "$(uname -s)" = Linux ]; then
getconf GNU_LIBC_VERSION >/dev/null 2>&1 || fail 'Linux requires glibc; musl is not supported by this installer.'
fi
for tool in curl tar mktemp; do command -v "$tool" >/dev/null || fail "$tool is required."; done
if command -v sha256sum >/dev/null; then
sha256() { sha256sum "$1" | awk '{print $1}'; }
elif command -v shasum >/dev/null; then
sha256() { shasum -a 256 "$1" | awk '{print $1}'; }
else fail 'SHA-256 verification is required.'; fi
revision=5554f93819478f4559aa226d1d94cfc6202a1cee
source_hash=7214ffbd73aaa247a91732f4f7229ee75f6d0d90cd86e73dbc4bbcbd96dbdc09
data_home=${XDG_DATA_HOME:-"$HOME/.local/share"}
case "$data_home" in /*) ;; *) fail 'XDG_DATA_HOME must be absolute.' ;; esac
bin_dir="$HOME/.local/bin"
release_root="$data_home/msg/client-releases"
release="$release_root/$revision"
if [ -e "$bin_dir/msg" ] || [ -L "$bin_dir/msg" ]; then
current=$(readlink "$bin_dir/msg" 2>/dev/null || true)
case "$current" in "$release_root"/*/bin/msg) ;; *) [ "$force" -eq 1 ] || fail 'An existing msg command is not managed by this installer. Re-run with --force to replace its launcher.' ;; esac
fi
work=$(mktemp -d "${TMPDIR:-/tmp}/msg-install.XXXXXXXX")
created=0
locked=0
launcher=''
cleanup() {
rm -rf -- "$work"
[ -z "$launcher" ] || rm -f -- "$launcher"
if [ "$created" -eq 1 ]; then rm -rf -- "$release"; fi
if [ "$locked" -eq 1 ]; then rmdir "$release_root/.install-lock"; fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
download() { curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 --connect-timeout 15 "$1" -o "$2"; }
mkdir -p "$release_root"
mkdir "$release_root/.install-lock" 2>/dev/null || fail 'Another installation is running (or a stale .install-lock remains).'
locked=1
[ ! -d "$bin_dir/msg" ] || fail 'The launcher path is a directory; refusing to replace it.'
if [ ! -x "$release/bin/msg" ]; then
[ ! -e "$release" ] || fail 'An incomplete release directory exists; remove it before retrying.'
printf 'Installing MSG client %s (no sudo)\n' "${revision:0:7}" >&2
download "https://github.com/astral-sh/uv/releases/download/0.12.20/uv-$platform.tar.gz" "$work/uv.tar.gz"
[ "$(sha256 "$work/uv.tar.gz")" = "$uv_hash" ] || fail 'uv checksum mismatch.'
tar -xzf "$work/uv.tar.gz" -C "$work"
uv="$work/uv-$platform/uv"
[ -x "$uv" ] || fail 'uv archive does not contain the expected executable.'
download "https://codeload.github.com/TokenNotIncluded/msg/zip/$revision" "$work/msg.zip"
[ "$(sha256 "$work/msg.zip")" = "$source_hash" ] || fail 'MSG source checksum mismatch.'
cat > "$work/requirements.txt" <<'MSG_CLIENT_REQUIREMENTS'
# This file was autogenerated by uv via the following command:
# uv export --no-dev --no-emit-project --format requirements-txt --output-file /tmp/msg-client-requirements.txt
anyio==4.15.1 \
--hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \
--hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94
# via httpx
attrs==26.1.0 \
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
# via
# jsonschema
# referencing
certifi==2026.7.22 \
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
# via
# httpcore
# httpx
cffi==2.1.1 ; platform_python_implementation != 'PyPy' \
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
--hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \
--hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \
--hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \
--hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \
--hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \
--hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \
--hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \
--hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
--hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \
--hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \
--hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \
--hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \
--hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \
--hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \
--hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \
--hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \
--hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \
--hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \
--hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
--hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \
--hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \
--hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4
# via cryptography
cryptography==46.0.7 \
--hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \
--hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \
--hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \
--hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \
--hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \
--hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \
--hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \
--hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \
--hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \
--hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \
--hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \
--hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \
--hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \
--hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \
--hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \
--hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \
--hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \
--hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \
--hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \
--hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \
--hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \
--hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \
--hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \
--hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \
--hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \
--hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \
--hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \
--hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \
--hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246
# via msgctl
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via msgctl
idna==3.20 \
--hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
--hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
# via
# anyio
# httpx
jsonschema==4.26.0 \
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
# via msgctl
jsonschema-specifications==2025.9.1 \
--hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \
--hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d
# via jsonschema
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
referencing==0.37.0 \
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
--hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8
# via
# jsonschema
# jsonschema-specifications
# msgctl
rpds-py==2026.6.3 \
--hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \
--hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \
--hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \
--hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \
--hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \
--hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \
--hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \
--hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \
--hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \
--hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \
--hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \
--hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \
--hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \
--hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \
--hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \
--hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \
--hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \
--hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \
--hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \
--hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \
--hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \
--hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \
--hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \
--hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \
--hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \
--hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \
--hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \
--hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \
--hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \
--hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef
# via
# jsonschema
# referencing
MSG_CLIENT_REQUIREMENTS
mkdir -p "$release_root"
created=1
# Keep the runtime/cache in MSG's installation area, away from identities.
export UV_PYTHON_INSTALL_DIR="$data_home/msg/client-python"
export UV_CACHE_DIR="$data_home/msg/client-cache"
"$uv" venv --managed-python --python 3.15.0rc2 "$release"
"$uv" pip install --python "$release/bin/python" --require-hashes --only-binary :all: -r "$work/requirements.txt"
"$uv" pip install --python "$release/bin/python" --no-deps "$work/msg.zip"
"$release/bin/msg" --help >/dev/null
fi
mkdir -p "$bin_dir"
# mv on the same filesystem atomically replaces only the launcher, after validation.
launcher=$(mktemp "$bin_dir/.msg-install.XXXXXXXX")
rm -f -- "$launcher"
ln -s "$release/bin/msg" "$launcher"
mv -f -- "$launcher" "$bin_dir/msg"
created=0
printf '\nMSG installed: %s/msg\nTry: msg lightjunction@msg.lmm.best ""\n' "$bin_dir"
# shellcheck disable=SC2016 # Print literal shell variables for the user.
case ":$PATH:" in *":$bin_dir:"*) ;; *) printf '\nAdd to your shell configuration:\n export PATH="$HOME/.local/bin:$PATH"\n' ;; esac
}
main "$@"
12 changes: 12 additions & 0 deletions src/msg/transports/http_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,18 @@ async def dispatch(request: Request):
if path == '/-' or path.startswith('/-/'):
raise Failure('not_found')
require(request.method in {'GET', 'HEAD'}, 'method_not_allowed')
if path == '/install':
require(not request.url.query, 'unknown_query_parameter')
payload = files('msg.data').joinpath('install.sh').read_bytes()
return Response(
b'' if request.method == 'HEAD' else payload,
media_type='text/plain',
headers={
**BASE_HEADERS,
'Content-Length': str(len(payload)),
'Cache-Control': 'no-cache',
},
)
if path in {'/robots.txt', '/sitemap.xml'}:
require(not request.url.query, 'unknown_query_parameter')
origin = service.settings.service_url.rstrip('/')
Expand Down
Loading
Loading